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.ObjectAnimator 21 import android.graphics.Bitmap 22 import android.graphics.drawable.Drawable 23 import android.view.View 24 import com.android.launcher3.DeviceProfile 25 import com.android.launcher3.anim.PendingAnimation 26 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource 27 import com.android.quickstep.views.IconView 28 import com.android.quickstep.views.TaskThumbnailView 29 import com.android.quickstep.views.TaskView 30 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer 31 import java.util.function.Supplier 32 33 /** 34 * Utils class to help run animations for initiating split screen from launcher. 35 * Will be expanded with future refactors. Works in conjunction with the state stored in 36 * [SplitSelectStateController] 37 */ 38 class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) { 39 companion object { 40 // Break this out into maybe enums? Abstractions into its own classes? Tbd. 41 data class SplitAnimInitProps( 42 val originalView: View, 43 val originalBitmap: Bitmap?, 44 val iconDrawable: Drawable, 45 val fadeWithThumbnail: Boolean, 46 val isStagedTask: Boolean, 47 val iconView: View? 48 ) 49 } 50 51 /** 52 * Returns different elements to animate for the initial split selection animation 53 * depending on the state of the surface from which the split was initiated 54 */ getFirstAnimInitViewsnull55 fun getFirstAnimInitViews(taskViewSupplier: Supplier<TaskView>, 56 splitSelectSourceSupplier: Supplier<SplitSelectSource?>) 57 : SplitAnimInitProps { 58 val splitSelectSource = splitSelectSourceSupplier.get() 59 if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { 60 // Initiating from home 61 return SplitAnimInitProps(splitSelectSource!!.view, originalBitmap = null, 62 splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, 63 iconView = null) 64 } else if (splitSelectStateController.isDismissingFromSplitPair) { 65 // Initiating split from overview, but on a split pair 66 val taskView = taskViewSupplier.get() 67 for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) { 68 if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) { 69 val drawable = getDrawable(container.iconView, splitSelectSource) 70 return SplitAnimInitProps(container.thumbnailView, 71 container.thumbnailView.thumbnail, drawable!!, 72 fadeWithThumbnail = true, isStagedTask = true, 73 iconView = container.iconView 74 ) 75 } 76 } 77 throw IllegalStateException("Attempting to init split from existing split pair " + 78 "without a valid taskIdAttributeContainer") 79 } else { 80 // Initiating split from overview on fullscreen task TaskView 81 val taskView = taskViewSupplier.get() 82 val drawable = getDrawable(taskView.iconView, splitSelectSource) 83 return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail, 84 drawable!!, fadeWithThumbnail = true, isStagedTask = true, 85 taskView.iconView 86 ) 87 } 88 } 89 90 /** 91 * Returns the drawable that's provided in iconView, however if that 92 * is null it falls back to the drawable that's in splitSelectSource. 93 * TaskView's icon drawable can be null if the TaskView is scrolled far enough off screen 94 * @return [Drawable] 95 */ getDrawablenull96 fun getDrawable(iconView: IconView, splitSelectSource: SplitSelectSource?) : Drawable? { 97 if (iconView.drawable == null && splitSelectSource != null) { 98 return splitSelectSource.drawable 99 } 100 return iconView.drawable 101 } 102 103 /** 104 * When selecting first app from split pair, second app's thumbnail remains. This animates 105 * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying 106 * it with [TaskThumbnailView]'s splashView. Adds animations to the provided builder. 107 * Note: The app that **was not** selected as the first split app should be the container that's 108 * passed through. 109 * 110 * @param builder Adds animation to this 111 * @param taskIdAttributeContainer container of the app that **was not** selected 112 * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair 113 * (opposite of that representing [taskIdAttributeContainer]) 114 */ addInitialSplitFromPairnull115 fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer, 116 builder: PendingAnimation, deviceProfile: DeviceProfile, 117 taskViewWidth: Int, taskViewHeight: Int, 118 isPrimaryTaskSplitting: Boolean) { 119 val thumbnail = taskIdAttributeContainer.thumbnailView 120 val iconView: View = taskIdAttributeContainer.iconView 121 builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f)) 122 thumbnail.setShowSplashForSplitSelection(true) 123 if (deviceProfile.isLandscape) { 124 // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 125 val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f 126 val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f 127 val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width 128 builder.add(ObjectAnimator.ofFloat(thumbnail, 129 TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX)) 130 // icons are anchored from Gravity.END, so need to use negative translation 131 builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 132 -centerIconTranslationX)) 133 builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) 134 135 // Reset other dimensions 136 // TODO(b/271468547), can't set Y translate to 0, need to account for top space 137 thumbnail.scaleY = 1f 138 val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else 139 deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() 140 builder.add(ObjectAnimator.ofFloat(thumbnail, 141 TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, 142 translateYResetVal)) 143 } else { 144 val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx 145 // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 146 // primary thumbnail has layout margin above it, so secondary thumbnail needs to take 147 // that into account. We should migrate to only using translations otherwise this 148 // asymmetry causes problems.. 149 150 // Icon defaults to center | horizontal, we add additional translation for split 151 val centerIconTranslationX = 0f 152 var centerThumbnailTranslationY: Float 153 154 // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary 155 // thumbnail needs to take that into account. We should migrate to only using 156 // translations otherwise this asymmetry causes problems.. 157 if (isPrimaryTaskSplitting) { 158 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f 159 centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx 160 .toFloat() 161 } else { 162 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f 163 } 164 val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height 165 builder.add(ObjectAnimator.ofFloat(thumbnail, 166 TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY)) 167 168 // icons are anchored from Gravity.END, so need to use negative translation 169 builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 170 centerIconTranslationX)) 171 builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) 172 173 // Reset other dimensions 174 thumbnail.scaleX = 1f 175 builder.add(ObjectAnimator.ofFloat(thumbnail, 176 TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f)) 177 } 178 } 179 } 180