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