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