1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.views 18 19 import android.content.Context 20 import android.graphics.Canvas 21 import android.graphics.ColorFilter 22 import android.graphics.Paint 23 import android.graphics.PixelFormat 24 import android.graphics.RectF 25 import android.graphics.drawable.Drawable 26 import android.os.Build 27 import android.view.animation.Interpolator 28 import com.android.app.animation.Interpolators 29 import com.android.launcher3.R 30 import com.android.launcher3.Utilities 31 import com.android.quickstep.util.AnimUtils 32 import com.android.systemui.shared.system.QuickStepContract 33 34 /** 35 * A Drawable that is drawn onto [FloatingAppPairView] every frame during the app pair launch 36 * animation. Consists of a rectangular background that splits into two, and two app icons that 37 * increase in size during the animation. 38 */ 39 open class FloatingAppPairBackground( 40 context: Context, 41 // the view that we will draw this background on 42 protected val floatingView: FloatingAppPairView, 43 private val appIcon1: Drawable, 44 private val appIcon2: Drawable?, 45 dividerPos: Int 46 ) : Drawable() { 47 companion object { 48 // Design specs -- app icons start small and expand during the animation 49 internal val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f) 50 internal val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f) 51 52 // Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other 53 // API for drawing rectangles with 4 different corner radii. 54 private val EMPTY_RECT = RectF() 55 private val ARRAY_OF_ZEROES = FloatArray(8) 56 } 57 58 private val container: RecentsViewContainer 59 private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) 60 61 // Animation interpolators 62 protected val expandXInterpolator: Interpolator 63 protected val expandYInterpolator: Interpolator 64 private val cellSplitInterpolator: Interpolator 65 protected val iconFadeInterpolator: Interpolator 66 67 // Device-specific measurements 68 protected val deviceCornerRadius: Float 69 private val deviceHalfDividerSize: Float 70 private val desiredSplitRatio: Float 71 72 init { 73 container = RecentsViewContainer.containerFromContext(context) 74 val dp = container.deviceProfile 75 // Set up background paint color 76 val ta = context.theme.obtainStyledAttributes(R.styleable.FolderIconPreview) 77 backgroundPaint.style = Paint.Style.FILL 78 backgroundPaint.color = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0) 79 ta.recycle() 80 // Set up timings and interpolators 81 val timings = AnimUtils.getDeviceAppPairLaunchTimings(container.deviceProfile.isTablet) 82 expandXInterpolator = 83 Interpolators.clampToProgress( 84 timings.getStagedRectScaleXInterpolator(), 85 timings.stagedRectSlideStartOffset, 86 timings.stagedRectSlideEndOffset 87 ) 88 expandYInterpolator = 89 Interpolators.clampToProgress( 90 timings.getStagedRectScaleYInterpolator(), 91 timings.stagedRectSlideStartOffset, 92 timings.stagedRectSlideEndOffset 93 ) 94 cellSplitInterpolator = 95 Interpolators.clampToProgress( 96 timings.cellSplitInterpolator, 97 timings.cellSplitStartOffset, 98 timings.cellSplitEndOffset 99 ) 100 iconFadeInterpolator = 101 Interpolators.clampToProgress( 102 timings.iconFadeInterpolator, 103 timings.iconFadeStartOffset, 104 timings.iconFadeEndOffset 105 ) 106 107 // Find device-specific measurements 108 deviceCornerRadius = QuickStepContract.getWindowCornerRadius(container.asContext()) 109 deviceHalfDividerSize = 110 container.asContext().resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f 111 val dividerCenterPos = dividerPos + deviceHalfDividerSize 112 desiredSplitRatio = 113 if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx 114 else dividerCenterPos / dp.heightPx 115 } 116 drawnull117 override fun draw(canvas: Canvas) { 118 if (container.deviceProfile.isLeftRightSplit) { 119 drawLeftRightSplit(canvas) 120 } else { 121 drawTopBottomSplit(canvas) 122 } 123 } 124 125 /** When device is in landscape, we draw the rectangles with a left-right split. */ drawLeftRightSplitnull126 private fun drawLeftRightSplit(canvas: Canvas) { 127 val progress = floatingView.progress 128 129 // Since the entire floating app pair surface is scaling up during this animation, we 130 // scale down most of these drawn elements so that they appear the proper size on-screen. 131 val scaleFactorX = floatingView.scaleX 132 val scaleFactorY = floatingView.scaleY 133 134 // Get the bounds where we will draw the background image 135 val width = bounds.width().toFloat() 136 val height = bounds.height().toFloat() 137 138 // Get device-specific measurements 139 val cornerRadiusX = deviceCornerRadius / scaleFactorX 140 val cornerRadiusY = deviceCornerRadius / scaleFactorY 141 val halfDividerSize = deviceHalfDividerSize / scaleFactorX 142 143 // Calculate changing measurements for background 144 // We add one pixel to some measurements to create a smooth edge with no gaps 145 val onePixel = 1f / scaleFactorX 146 val changingDividerSize = 147 (cellSplitInterpolator.getInterpolation(progress) * halfDividerSize) - onePixel 148 val changingInnerRadiusX = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusX 149 val changingInnerRadiusY = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusY 150 val dividerCenterPos = width * desiredSplitRatio 151 152 // The left half of the background image 153 val leftSide = RectF(0f, 0f, dividerCenterPos - changingDividerSize, height) 154 // The right half of the background image 155 val rightSide = RectF(dividerCenterPos + changingDividerSize, 0f, width, height) 156 157 // Draw background 158 drawCustomRoundedRect( 159 canvas, 160 leftSide, 161 floatArrayOf( 162 cornerRadiusX, 163 cornerRadiusY, 164 changingInnerRadiusX, 165 changingInnerRadiusY, 166 changingInnerRadiusX, 167 changingInnerRadiusY, 168 cornerRadiusX, 169 cornerRadiusY, 170 ) 171 ) 172 drawCustomRoundedRect( 173 canvas, 174 rightSide, 175 floatArrayOf( 176 changingInnerRadiusX, 177 changingInnerRadiusY, 178 cornerRadiusX, 179 cornerRadiusY, 180 cornerRadiusX, 181 cornerRadiusY, 182 changingInnerRadiusX, 183 changingInnerRadiusY, 184 ) 185 ) 186 187 // Calculate changing measurements for icons. 188 val changingIconSizeX = 189 (STARTING_ICON_SIZE_PX + 190 ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * 191 expandXInterpolator.getInterpolation(progress))) / scaleFactorX 192 val changingIconSizeY = 193 (STARTING_ICON_SIZE_PX + 194 ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * 195 expandYInterpolator.getInterpolation(progress))) / scaleFactorY 196 197 val changingIcon1Left = ((width / 2f - halfDividerSize) / 2f) - (changingIconSizeX / 2f) 198 val changingIcon2Left = 199 (width - ((width / 2f - halfDividerSize) / 2f)) - (changingIconSizeX / 2f) 200 val changingIconTop = (height / 2f) - (changingIconSizeY / 2f) 201 val changingIconScaleX = changingIconSizeX / appIcon1.bounds.width() 202 val changingIconScaleY = changingIconSizeY / appIcon1.bounds.height() 203 val changingIconAlpha = 204 (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt() 205 206 // Draw first icon 207 canvas.save() 208 canvas.translate(changingIcon1Left, changingIconTop) 209 canvas.scale(changingIconScaleX, changingIconScaleY) 210 appIcon1.alpha = changingIconAlpha 211 appIcon1.draw(canvas) 212 canvas.restore() 213 214 // Draw second icon 215 canvas.save() 216 canvas.translate(changingIcon2Left, changingIconTop) 217 canvas.scale(changingIconScaleX, changingIconScaleY) 218 appIcon2!!.alpha = changingIconAlpha 219 appIcon2.draw(canvas) 220 canvas.restore() 221 } 222 223 /** When device is in portrait, we draw the rectangles with a top-bottom split. */ drawTopBottomSplitnull224 private fun drawTopBottomSplit(canvas: Canvas) { 225 val progress = floatingView.progress 226 227 // Since the entire floating app pair surface is scaling up during this animation, we 228 // scale down most of these drawn elements so that they appear the proper size on-screen. 229 val scaleFactorX = floatingView.scaleX 230 val scaleFactorY = floatingView.scaleY 231 232 // Get the bounds where we will draw the background image 233 val width = bounds.width().toFloat() 234 val height = bounds.height().toFloat() 235 236 // Get device-specific measurements 237 val cornerRadiusX = deviceCornerRadius / scaleFactorX 238 val cornerRadiusY = deviceCornerRadius / scaleFactorY 239 val halfDividerSize = deviceHalfDividerSize / scaleFactorY 240 241 // Calculate changing measurements for background 242 // We add one pixel to some measurements to create a smooth edge with no gaps 243 val onePixel = 1f / scaleFactorY 244 val changingDividerSize = 245 (cellSplitInterpolator.getInterpolation(progress) * halfDividerSize) - onePixel 246 val changingInnerRadiusX = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusX 247 val changingInnerRadiusY = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusY 248 val dividerCenterPos = height * desiredSplitRatio 249 250 // The top half of the background image 251 val topSide = RectF(0f, 0f, width, dividerCenterPos - changingDividerSize) 252 // The bottom half of the background image 253 val bottomSide = RectF(0f, dividerCenterPos + changingDividerSize, width, height) 254 255 // Draw background 256 drawCustomRoundedRect( 257 canvas, 258 topSide, 259 floatArrayOf( 260 cornerRadiusX, 261 cornerRadiusY, 262 cornerRadiusX, 263 cornerRadiusY, 264 changingInnerRadiusX, 265 changingInnerRadiusY, 266 changingInnerRadiusX, 267 changingInnerRadiusY 268 ) 269 ) 270 drawCustomRoundedRect( 271 canvas, 272 bottomSide, 273 floatArrayOf( 274 changingInnerRadiusX, 275 changingInnerRadiusY, 276 changingInnerRadiusX, 277 changingInnerRadiusY, 278 cornerRadiusX, 279 cornerRadiusY, 280 cornerRadiusX, 281 cornerRadiusY 282 ) 283 ) 284 285 // Calculate changing measurements for icons. 286 val changingIconSizeX = 287 (STARTING_ICON_SIZE_PX + 288 ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * 289 expandXInterpolator.getInterpolation(progress))) / scaleFactorX 290 val changingIconSizeY = 291 (STARTING_ICON_SIZE_PX + 292 ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * 293 expandYInterpolator.getInterpolation(progress))) / scaleFactorY 294 295 val changingIconLeft = (width / 2f) - (changingIconSizeX / 2f) 296 val changingIcon1Top = (((height / 2f) - halfDividerSize) / 2f) - (changingIconSizeY / 2f) 297 val changingIcon2Top = 298 (height - (((height / 2f) - halfDividerSize) / 2f)) - (changingIconSizeY / 2f) 299 val changingIconScaleX = changingIconSizeX / appIcon1.bounds.width() 300 val changingIconScaleY = changingIconSizeY / appIcon1.bounds.height() 301 val changingIconAlpha = 302 (255 - 255 * iconFadeInterpolator.getInterpolation(progress)).toInt() 303 304 // Draw first icon 305 canvas.save() 306 canvas.translate(changingIconLeft, changingIcon1Top) 307 canvas.scale(changingIconScaleX, changingIconScaleY) 308 appIcon1.alpha = changingIconAlpha 309 appIcon1.draw(canvas) 310 canvas.restore() 311 312 // Draw second icon 313 canvas.save() 314 canvas.translate(changingIconLeft, changingIcon2Top) 315 canvas.scale(changingIconScaleX, changingIconScaleY) 316 appIcon2!!.alpha = changingIconAlpha 317 appIcon2.draw(canvas) 318 canvas.restore() 319 } 320 321 /** 322 * Draws a rectangle with custom rounded corners. 323 * 324 * @param c The Canvas to draw on. 325 * @param rect The bounds of the rectangle. 326 * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top 327 * right y, bottom right x, and so on. 328 */ drawCustomRoundedRectnull329 protected fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) { 330 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 331 // Canvas.drawDoubleRoundRect is supported from Q onward 332 c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint) 333 } else { 334 // Fallback rectangle with uniform rounded corners 335 val scaleFactorX = floatingView.scaleX 336 val scaleFactorY = floatingView.scaleY 337 val cornerRadiusX = 338 QuickStepContract.getWindowCornerRadius(container.asContext()) / scaleFactorX 339 val cornerRadiusY = 340 QuickStepContract.getWindowCornerRadius(container.asContext()) / scaleFactorY 341 c.drawRoundRect(rect, cornerRadiusX, cornerRadiusY, backgroundPaint) 342 } 343 } 344 getOpacitynull345 override fun getOpacity(): Int { 346 return PixelFormat.OPAQUE 347 } 348 setAlphanull349 override fun setAlpha(i: Int) { 350 // Required by Drawable but not used. 351 } 352 setColorFilternull353 override fun setColorFilter(colorFilter: ColorFilter?) { 354 // Required by Drawable but not used. 355 } 356 } 357