• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.quickstep.util
19 
20 import android.animation.Animator
21 import android.animation.AnimatorListenerAdapter
22 import android.animation.AnimatorSet
23 import android.animation.ObjectAnimator
24 import android.graphics.Bitmap
25 import android.graphics.Rect
26 import android.graphics.RectF
27 import android.graphics.drawable.Drawable
28 import android.view.View
29 import com.android.app.animation.Interpolators
30 import com.android.launcher3.DeviceProfile
31 import com.android.launcher3.Utilities
32 import com.android.launcher3.anim.PendingAnimation
33 import com.android.launcher3.statemanager.StatefulActivity
34 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
35 import com.android.launcher3.views.BaseDragLayer
36 import com.android.quickstep.views.FloatingTaskView
37 import com.android.quickstep.views.IconView
38 import com.android.quickstep.views.RecentsView
39 import com.android.quickstep.views.SplitInstructionsView
40 import com.android.quickstep.views.TaskThumbnailView
41 import com.android.quickstep.views.TaskView
42 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
43 import java.util.function.Supplier
44 
45 /**
46  * Utils class to help run animations for initiating split screen from launcher.
47  * Will be expanded with future refactors. Works in conjunction with the state stored in
48  * [SplitSelectStateController]
49  */
50 class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) {
51     companion object {
52         // Break this out into maybe enums? Abstractions into its own classes? Tbd.
53         data class SplitAnimInitProps(
54                 val originalView: View,
55                 val originalBitmap: Bitmap?,
56                 val iconDrawable: Drawable,
57                 val fadeWithThumbnail: Boolean,
58                 val isStagedTask: Boolean,
59                 val iconView: View?
60         )
61     }
62 
63     /**
64      * Returns different elements to animate for the initial split selection animation
65      * depending on the state of the surface from which the split was initiated
66      */
getFirstAnimInitViewsnull67     fun getFirstAnimInitViews(taskViewSupplier: Supplier<TaskView>,
68                               splitSelectSourceSupplier: Supplier<SplitSelectSource?>)
69             : SplitAnimInitProps {
70         val splitSelectSource = splitSelectSourceSupplier.get()
71         if (!splitSelectStateController.isAnimateCurrentTaskDismissal) {
72             // Initiating from home
73             return SplitAnimInitProps(splitSelectSource!!.view, originalBitmap = null,
74                     splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true,
75                     iconView = null)
76         } else if (splitSelectStateController.isDismissingFromSplitPair) {
77             // Initiating split from overview, but on a split pair
78             val taskView = taskViewSupplier.get()
79             for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) {
80                 if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) {
81                     val drawable = getDrawable(container.iconView, splitSelectSource)
82                     return SplitAnimInitProps(container.thumbnailView,
83                             container.thumbnailView.thumbnail, drawable!!,
84                             fadeWithThumbnail = true, isStagedTask = true,
85                             iconView = container.iconView
86                     )
87                 }
88             }
89             throw IllegalStateException("Attempting to init split from existing split pair " +
90                     "without a valid taskIdAttributeContainer")
91         } else {
92             // Initiating split from overview on fullscreen task TaskView
93             val taskView = taskViewSupplier.get()
94             val drawable = getDrawable(taskView.iconView, splitSelectSource)
95             return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail,
96                     drawable!!, fadeWithThumbnail = true, isStagedTask = true,
97                     taskView.iconView
98             )
99         }
100     }
101 
102     /**
103      * Returns the drawable that's provided in iconView, however if that
104      * is null it falls back to the drawable that's in splitSelectSource.
105      * TaskView's icon drawable can be null if the TaskView is scrolled far enough off screen
106      * @return [Drawable]
107      */
getDrawablenull108     fun getDrawable(iconView: IconView, splitSelectSource: SplitSelectSource?) : Drawable? {
109         if (iconView.drawable == null && splitSelectSource != null) {
110             return splitSelectSource.drawable
111         }
112         return iconView.drawable
113     }
114 
115     /**
116      * When selecting first app from split pair, second app's thumbnail remains. This animates
117      * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying
118      * it with [TaskThumbnailView]'s splashView. Adds animations to the provided builder.
119      * Note: The app that **was not** selected as the first split app should be the container that's
120      * passed through.
121      *
122      * @param builder Adds animation to this
123      * @param taskIdAttributeContainer container of the app that **was not** selected
124      * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair
125      *                               (opposite of that representing [taskIdAttributeContainer])
126      */
addInitialSplitFromPairnull127     fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer,
128                                 builder: PendingAnimation, deviceProfile: DeviceProfile,
129                                 taskViewWidth: Int, taskViewHeight: Int,
130                                 isPrimaryTaskSplitting: Boolean) {
131         val thumbnail = taskIdAttributeContainer.thumbnailView
132         val iconView: View = taskIdAttributeContainer.iconView
133         builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f))
134         thumbnail.setShowSplashForSplitSelection(true)
135         if (deviceProfile.isLandscape) {
136             // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
137             val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f
138             val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f
139             val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width
140             builder.add(ObjectAnimator.ofFloat(thumbnail,
141                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX))
142             // icons are anchored from Gravity.END, so need to use negative translation
143             builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
144                     -centerIconTranslationX))
145             builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX))
146 
147             // Reset other dimensions
148             // TODO(b/271468547), can't set Y translate to 0, need to account for top space
149             thumbnail.scaleY = 1f
150             val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else
151                 deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
152             builder.add(ObjectAnimator.ofFloat(thumbnail,
153                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y,
154                     translateYResetVal))
155         } else {
156             val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
157             // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0
158             // primary thumbnail has layout margin above it, so secondary thumbnail needs to take
159             // that into account. We should migrate to only using translations otherwise this
160             // asymmetry causes problems..
161 
162             // Icon defaults to center | horizontal, we add additional translation for split
163             val centerIconTranslationX = 0f
164             var centerThumbnailTranslationY: Float
165 
166             // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary
167             //  thumbnail needs to take that into account. We should migrate to only using
168             //  translations otherwise this asymmetry causes problems..
169             if (isPrimaryTaskSplitting) {
170                 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
171                 centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx
172                         .toFloat()
173             } else {
174                 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
175             }
176             val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height
177             builder.add(ObjectAnimator.ofFloat(thumbnail,
178                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY))
179 
180             // icons are anchored from Gravity.END, so need to use negative translation
181             builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
182                     centerIconTranslationX))
183             builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY))
184 
185             // Reset other dimensions
186             thumbnail.scaleX = 1f
187             builder.add(ObjectAnimator.ofFloat(thumbnail,
188                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f))
189         }
190     }
191 
192     /** Does not play any animation if user is not currently in split selection state. */
playPlaceholderDismissAnimnull193     fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>) {
194         if (!splitSelectStateController.isSplitSelectActive) {
195             return
196         }
197 
198         val anim = createPlaceholderDismissAnim(launcher)
199         anim.start()
200     }
201 
202     /** Returns [AnimatorSet] which slides initial split placeholder view offscreen. */
createPlaceholderDismissAnimnull203     fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>) : AnimatorSet {
204         val animatorSet = AnimatorSet()
205         val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
206         val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
207                 ?: return animatorSet
208 
209         // We are in split selection state currently, transitioning to another state
210         val dragLayer: BaseDragLayer<*> = launcher.dragLayer
211         val onScreenRectF = RectF()
212         Utilities.getBoundsForViewInDragLayer(dragLayer, floatingTask,
213                 Rect(0, 0, floatingTask.width, floatingTask.height),
214                 false, null, onScreenRectF)
215         // Get the part of the floatingTask that intersects with the DragLayer (i.e. the
216         // on-screen portion)
217         onScreenRectF.intersect(
218                 dragLayer.left.toFloat(),
219                 dragLayer.top.toFloat(),
220                 dragLayer.right.toFloat(),
221                 dragLayer.bottom
222                         .toFloat()
223         )
224         animatorSet.play(ObjectAnimator.ofFloat(floatingTask,
225                 FloatingTaskView.PRIMARY_TRANSLATE_OFFSCREEN,
226                 recentsView.pagedOrientationHandler
227                         .getFloatingTaskOffscreenTranslationTarget(
228                                 floatingTask,
229                                 onScreenRectF,
230                                 floatingTask.stagePosition,
231                                 launcher.deviceProfile
232                         )))
233         animatorSet.addListener(object : AnimatorListenerAdapter() {
234             override fun onAnimationEnd(animation: Animator) {
235                 splitSelectStateController.resetState()
236                 safeRemoveViewFromDragLayer(launcher,
237                         splitSelectStateController.splitInstructionsView)
238             }
239         })
240         return animatorSet
241     }
242 
243     /**
244      * Returns a [PendingAnimation] to animate in the chip to instruct a user to select a second
245      * app for splitscreen
246      */
getShowSplitInstructionsAnimnull247     fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>) : PendingAnimation {
248         safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
249         val splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
250         splitSelectStateController.splitInstructionsView = splitInstructionsView
251         val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
252         val anim = PendingAnimation(100 /*duration */)
253         anim.setViewAlpha(splitInstructionsView, 1f,
254                 Interpolators.clampToProgress(Interpolators.LINEAR,
255                         timings.instructionsContainerFadeInStartOffset,
256                         timings.instructionsContainerFadeInEndOffset))
257         anim.setViewAlpha(splitInstructionsView!!.textView, 1f,
258                 Interpolators.clampToProgress(Interpolators.LINEAR,
259                         timings.instructionsTextFadeInStartOffset,
260                         timings.instructionsTextFadeInEndOffset))
261         anim.addFloat(splitInstructionsView, SplitInstructionsView.UNFOLD, 0.1f, 1f,
262                 Interpolators.clampToProgress(Interpolators.EMPHASIZED_DECELERATE,
263                         timings.instructionsUnfoldStartOffset,
264                         timings.instructionsUnfoldEndOffset))
265         return anim
266     }
267 
268     /** Removes the split instructions view from [launcher] drag layer. */
removeSplitInstructionsViewnull269     fun removeSplitInstructionsView(launcher: StatefulActivity<*>) {
270         safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
271     }
272 
safeRemoveViewFromDragLayernull273     private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
274         if (view != null) {
275             launcher.dragLayer.removeView(view)
276         }
277     }
278 }
279