Loading quickstep/src/com/android/quickstep/util/SplitAnimationController.kt 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.quickstep.util import android.animation.ObjectAnimator import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.view.View import com.android.launcher3.DeviceProfile import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource import com.android.quickstep.views.TaskThumbnailView import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskView.TaskIdAttributeContainer import java.util.function.Supplier /** * Utils class to help run animations for initiating split screen from launcher. * Will be expanded with future refactors. Works in conjunction with the state stored in * [SplitSelectStateController] */ class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) { companion object { // Break this out into maybe enums? Abstractions into its own classes? Tbd. data class SplitAnimInitProps( val originalView: View, val originalBitmap: Bitmap?, val iconDrawable: Drawable, val fadeWithThumbnail: Boolean, val isStagedTask: Boolean, val iconView: View? ) } /** * Returns different elements to animate for the initial split selection animation * depending on the state of the surface from which the split was initiated */ fun getFirstAnimInitViews(taskViewSupplier: Supplier<TaskView>, splitSelectSourceSupplier: Supplier<SplitSelectSource>) : SplitAnimInitProps { if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { // Initiating from home val splitSelectSource = splitSelectSourceSupplier.get() return SplitAnimInitProps(splitSelectSource.view, originalBitmap = null, splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, iconView = null) } else if (splitSelectStateController.isDismissingFromSplitPair) { // Initiating split from overview, but on a split pair val taskView = taskViewSupplier.get() for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) { if (container.task.key.id == splitSelectStateController.initialTaskId) { return SplitAnimInitProps(container.thumbnailView, container.thumbnailView.thumbnail, container.iconView.drawable!!, fadeWithThumbnail = true, isStagedTask = true, iconView = container.iconView ) } } throw IllegalStateException("Attempting to init split from existing split pair " + "without a valid taskIdAttributeContainer") } else { // Initiating split from overview on fullscreen task TaskView val taskView = taskViewSupplier.get() return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail, taskView.iconView.drawable!!, fadeWithThumbnail = true, isStagedTask = true, taskView.iconView ) } } /** * When selecting first app from split pair, second app's thumbnail remains. This animates * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying * it with [TaskThumbnailView]'s splashView. Adds animations to the provided builder. * Note: The app that **was not** selected as the first split app should be the container that's * passed through. * * @param builder Adds animation to this * @param taskIdAttributeContainer container of the app that **was not** selected * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair * (opposite of that representing [taskIdAttributeContainer]) */ fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer, builder: PendingAnimation, deviceProfile: DeviceProfile, taskViewWidth: Int, taskViewHeight: Int, isPrimaryTaskSplitting: Boolean) { val thumbnail = taskIdAttributeContainer.thumbnailView val iconView: View = taskIdAttributeContainer.iconView builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f)) thumbnail.setShowSplashForSplitSelection(true) if (deviceProfile.isLandscape) { // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX)) // icons are anchored from Gravity.END, so need to use negative translation builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX)) builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) // Reset other dimensions // TODO(b/271468547), can't set Y translate to 0, need to account for top space thumbnail.scaleY = 1f val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, translateYResetVal)) } else { val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 // primary thumbnail has layout margin above it, so secondary thumbnail needs to take // that into account. We should migrate to only using translations otherwise this // asymmetry causes problems.. // Icon defaults to center | horizontal, we add additional translation for split val centerIconTranslationX = 0f var centerThumbnailTranslationY: Float // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary // thumbnail needs to take that into account. We should migrate to only using // translations otherwise this asymmetry causes problems.. if (isPrimaryTaskSplitting) { centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx .toFloat() } else { centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f } val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY)) // icons are anchored from Gravity.END, so need to use negative translation builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, centerIconTranslationX)) builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) // Reset other dimensions thumbnail.scaleX = 1f builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f)) } } } quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +24 −0 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ public class SplitSelectStateController { private final Context mContext; private final Handler mHandler; private final RecentsModel mRecentTasksModel; private final SplitAnimationController mSplitAnimationController; private StatsLogManager mStatsLogManager; private final SystemUiProxy mSystemUiProxy; private final StateManager mStateManager; Loading @@ -96,6 +97,11 @@ public class SplitSelectStateController { private boolean mRecentsAnimationRunning; /** If {@code true}, animates the existing task view split placeholder view */ private boolean mAnimateCurrentTaskDismissal; /** * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a * split pair task view without wanting to animate current task dismissal overall */ private boolean mDismissingFromSplitPair; @Nullable private UserHandle mUser; /** If not null, this is the TaskView we want to launch from */ Loading @@ -116,6 +122,7 @@ public class SplitSelectStateController { mStateManager = stateManager; mDepthController = depthController; mRecentTasksModel = recentsModel; mSplitAnimationController = new SplitAnimationController(this); } /** Loading Loading @@ -399,6 +406,18 @@ public class SplitSelectStateController { mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal; } public boolean isDismissingFromSplitPair() { return mDismissingFromSplitPair; } public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) { mDismissingFromSplitPair = dismissingFromSplitPair; } public SplitAnimationController getSplitAnimationController() { return mSplitAnimationController; } /** * Requires Shell Transitions */ Loading Loading @@ -506,6 +525,7 @@ public class SplitSelectStateController { mItemInfo = null; mSplitEvent = null; mAnimateCurrentTaskDismissal = false; mDismissingFromSplitPair = false; } /** Loading @@ -532,6 +552,10 @@ public class SplitSelectStateController { return mInitialTaskId; } public int getSecondTaskId() { return mSecondTaskId; } private boolean isSecondTaskIntentSet() { return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null); } Loading quickstep/src/com/android/quickstep/views/DesktopTaskView.java +1 −1 Original line number Diff line number Diff line Loading @@ -496,7 +496,7 @@ public class DesktopTaskView extends TaskView { } @Override void setThumbnailVisibility(int visibility) { void setThumbnailVisibility(int visibility, int taskId) { for (int i = 0; i < mSnapshotViewMap.size(); i++) { mSnapshotViewMap.valueAt(i).setVisibility(visibility); } Loading quickstep/src/com/android/quickstep/views/GroupedTaskView.java +52 −9 Original line number Diff line number Diff line package com.android.quickstep.views; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; Loading @@ -25,6 +27,7 @@ import com.android.quickstep.TaskIconCache; import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.util.CancellableTask; import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.TaskViewSimulator; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; Loading Loading @@ -267,6 +270,19 @@ public class GroupedTaskView extends TaskView { @Override protected int getLastSelectedChildTaskIndex() { SplitSelectStateController splitSelectController = getRecentsView().getSplitSelectController(); if (splitSelectController.isDismissingFromSplitPair()) { // return the container index of the task that wasn't initially selected to split with // because that is the only remaining app that can be selected. The coordinate checks // below aren't reliable since both of those views may be gone/transformed int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); if (initSplitTaskId != INVALID_TASK_ID) { return initSplitTaskId == mTask.key.id ? 1 : 0; } } // Check which of the two apps was selected if (isCoordInView(mIconView2, mLastTouchDownPosition) || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) { return 1; Loading Loading @@ -296,9 +312,30 @@ public class GroupedTaskView extends TaskView { if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) { return; } int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); if (initSplitTaskId == INVALID_TASK_ID) { getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL); // Should we be having a separate translation step apart from the measuring above? // The following only applies to large screen for now, but for future reference // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary // translation directions mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX()); mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY()); mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX()); mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY()); } else { // Currently being split with this taskView, let the non-split selected thumbnail // take up full thumbnail area TaskIdAttributeContainer container = mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0]; container.getThumbnailView().measure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec( heightSize - mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx, MeasureSpec.EXACTLY)); } updateIconPlacement(); } Loading Loading @@ -379,6 +416,12 @@ public class GroupedTaskView extends TaskView { mSnapshotView2.refreshSplashView(); } @Override protected void resetViewTransforms() { super.resetViewTransforms(); mSnapshotView2.resetViewTransforms(); } /** * Sets visibility for thumbnails and associated elements (DWB banners). * IconView is unaffected. Loading @@ -387,13 +430,13 @@ public class GroupedTaskView extends TaskView { * When setting VISIBLE (as a reset), sets the visibility for both tasks. */ @Override void setThumbnailVisibility(int visibility) { void setThumbnailVisibility(int visibility, int taskId) { if (visibility == VISIBLE) { mSnapshotView.setVisibility(visibility); mDigitalWellBeingToast.setBannerVisibility(visibility); mSnapshotView2.setVisibility(visibility); mDigitalWellBeingToast2.setBannerVisibility(visibility); } else if (getLastSelectedChildTaskIndex() == 0) { } else if (taskId == getTaskIds()[0]) { mSnapshotView.setVisibility(visibility); mDigitalWellBeingToast.setBannerVisibility(visibility); } else { Loading quickstep/src/com/android/quickstep/views/RecentsView.java +53 −25 Original line number Diff line number Diff line Loading @@ -184,6 +184,7 @@ import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps; import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SurfaceTransaction; Loading @@ -192,6 +193,7 @@ import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.VibrationConstants; import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; import com.android.systemui.plugins.ResourceProvider; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; Loading Loading @@ -935,7 +937,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T if (mHandleTaskStackChanges) { TaskView taskView = getTaskViewByTaskId(taskId); if (taskView != null) { for (TaskView.TaskIdAttributeContainer container : for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) { if (container == null || taskId != container.getTask().key.id) { continue; Loading Loading @@ -3099,28 +3101,25 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T RectF startingTaskRect = new RectF(); safeRemoveDragLayerView(mFirstFloatingTaskView); SplitAnimInitProps splitAnimInitProps = mSplitSelectStateController.getSplitAnimationController().getFirstAnimInitViews( () -> mSplitHiddenTaskView, () -> mSplitSelectSource); if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) { // Create the split select animation from Overview mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE); anim.setViewAlpha(mSplitHiddenTaskView.getIconView(), 0, clampToProgress(LINEAR, mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE, mSplitSelectStateController.getInitialTaskId()); anim.setViewAlpha(splitAnimInitProps.getIconView(), 0, clampToProgress(LINEAR, timings.getIconFadeStartOffset(), timings.getIconFadeEndOffset())); } mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, mSplitHiddenTaskView.getThumbnail(), mSplitHiddenTaskView.getThumbnail().getThumbnail(), mSplitHiddenTaskView.getIconView().getDrawable(), startingTaskRect); mFirstFloatingTaskView.setAlpha(1); mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, true /* fadeWithThumbnail */, true /* isStagedTask */); } else { // Create the split select animation from Home mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, mSplitSelectSource.view, null /* thumbnail */, mSplitSelectSource.drawable, startingTaskRect); splitAnimInitProps.getOriginalView(), splitAnimInitProps.getOriginalBitmap(), splitAnimInitProps.getIconDrawable(), startingTaskRect); mFirstFloatingTaskView.setAlpha(1); mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); } splitAnimInitProps.getFadeWithThumbnail(), splitAnimInitProps.isStagedTask()); // Allow user to click staged app to launch into fullscreen if (ENABLE_LAUNCH_FROM_STAGED_APP.get()) { Loading Loading @@ -4450,7 +4449,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T mTaskViewsSecondarySplitTranslation = translation; for (int i = 0; i < getTaskViewCount(); i++) { TaskView taskView = requireTaskViewAt(i); if (taskView == mSplitHiddenTaskView) { if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) { continue; } taskView.getSecondarySplitTranslationProperty().set(taskView, translation); Loading Loading @@ -4501,6 +4500,10 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView); mSplitSelectStateController .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal); // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null && mSplitHiddenTaskView.containsMultipleTasks()); mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent, splitSelectSource.position.stagePosition, splitSelectSource.itemInfo, splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId); Loading @@ -4519,8 +4522,32 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T * Modifies a PendingAnimation with the animations for entering split staging */ public void createSplitSelectInitAnimation(PendingAnimation builder, int duration) { if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) { // Splitting from Overview boolean isInitiatingSplitFromTaskView = mSplitSelectStateController.isAnimateCurrentTaskDismissal(); boolean isInitiatingTaskViewSplitPair = mSplitSelectStateController.isDismissingFromSplitPair(); if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair) { // Splitting from Overview for split pair task createInitialSplitSelectAnimation(builder); // Animate pair thumbnail into full thumbnail boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIdAttributeContainers()[0].getTask().key.id == mSplitSelectStateController.getInitialTaskId(); TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView .getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0]; TaskThumbnailView thumbnail = taskIdAttributeContainer.getThumbnailView(); mSplitSelectStateController.getSplitAnimationController() .addInitialSplitFromPair(taskIdAttributeContainer, builder, mActivity.getDeviceProfile(), mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(), primaryTaskSelected); builder.addOnFrameCallback(() ->{ thumbnail.refreshSplashView(); mSplitHiddenTaskView.updateSnapshotRadius(); }); } else if (isInitiatingSplitFromTaskView) { // Splitting from Overview for fullscreen task createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, true /* dismissingForSplitSelection*/); } else { Loading Loading @@ -4608,7 +4635,8 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T mSecondSplitHiddenView = containerTaskView; if (mSecondSplitHiddenView != null) { mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE); mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE, mSplitSelectStateController.getSecondTaskId()); } InteractionJankMonitorWrapper.begin(this, Loading @@ -4634,7 +4662,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T } if (mSecondSplitHiddenView != null) { mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE); mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID); mSecondSplitHiddenView = null; } Loading @@ -4660,7 +4688,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T resetTaskVisuals(); mSplitHiddenTaskViewIndex = -1; if (mSplitHiddenTaskView != null) { mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE); mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID); mSplitHiddenTaskView = null; } if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { Loading Loading @@ -5530,7 +5558,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T } taskView.setShowScreenshot(true); for (TaskView.TaskIdAttributeContainer container : for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) { if (container == null) { continue; Loading Loading
quickstep/src/com/android/quickstep/util/SplitAnimationController.kt 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.quickstep.util import android.animation.ObjectAnimator import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.view.View import com.android.launcher3.DeviceProfile import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource import com.android.quickstep.views.TaskThumbnailView import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskView.TaskIdAttributeContainer import java.util.function.Supplier /** * Utils class to help run animations for initiating split screen from launcher. * Will be expanded with future refactors. Works in conjunction with the state stored in * [SplitSelectStateController] */ class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) { companion object { // Break this out into maybe enums? Abstractions into its own classes? Tbd. data class SplitAnimInitProps( val originalView: View, val originalBitmap: Bitmap?, val iconDrawable: Drawable, val fadeWithThumbnail: Boolean, val isStagedTask: Boolean, val iconView: View? ) } /** * Returns different elements to animate for the initial split selection animation * depending on the state of the surface from which the split was initiated */ fun getFirstAnimInitViews(taskViewSupplier: Supplier<TaskView>, splitSelectSourceSupplier: Supplier<SplitSelectSource>) : SplitAnimInitProps { if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { // Initiating from home val splitSelectSource = splitSelectSourceSupplier.get() return SplitAnimInitProps(splitSelectSource.view, originalBitmap = null, splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, iconView = null) } else if (splitSelectStateController.isDismissingFromSplitPair) { // Initiating split from overview, but on a split pair val taskView = taskViewSupplier.get() for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) { if (container.task.key.id == splitSelectStateController.initialTaskId) { return SplitAnimInitProps(container.thumbnailView, container.thumbnailView.thumbnail, container.iconView.drawable!!, fadeWithThumbnail = true, isStagedTask = true, iconView = container.iconView ) } } throw IllegalStateException("Attempting to init split from existing split pair " + "without a valid taskIdAttributeContainer") } else { // Initiating split from overview on fullscreen task TaskView val taskView = taskViewSupplier.get() return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail, taskView.iconView.drawable!!, fadeWithThumbnail = true, isStagedTask = true, taskView.iconView ) } } /** * When selecting first app from split pair, second app's thumbnail remains. This animates * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying * it with [TaskThumbnailView]'s splashView. Adds animations to the provided builder. * Note: The app that **was not** selected as the first split app should be the container that's * passed through. * * @param builder Adds animation to this * @param taskIdAttributeContainer container of the app that **was not** selected * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair * (opposite of that representing [taskIdAttributeContainer]) */ fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer, builder: PendingAnimation, deviceProfile: DeviceProfile, taskViewWidth: Int, taskViewHeight: Int, isPrimaryTaskSplitting: Boolean) { val thumbnail = taskIdAttributeContainer.thumbnailView val iconView: View = taskIdAttributeContainer.iconView builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f)) thumbnail.setShowSplashForSplitSelection(true) if (deviceProfile.isLandscape) { // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX)) // icons are anchored from Gravity.END, so need to use negative translation builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX)) builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) // Reset other dimensions // TODO(b/271468547), can't set Y translate to 0, need to account for top space thumbnail.scaleY = 1f val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, translateYResetVal)) } else { val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 // primary thumbnail has layout margin above it, so secondary thumbnail needs to take // that into account. We should migrate to only using translations otherwise this // asymmetry causes problems.. // Icon defaults to center | horizontal, we add additional translation for split val centerIconTranslationX = 0f var centerThumbnailTranslationY: Float // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary // thumbnail needs to take that into account. We should migrate to only using // translations otherwise this asymmetry causes problems.. if (isPrimaryTaskSplitting) { centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx .toFloat() } else { centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f } val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY)) // icons are anchored from Gravity.END, so need to use negative translation builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, centerIconTranslationX)) builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) // Reset other dimensions thumbnail.scaleX = 1f builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f)) } } }
quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +24 −0 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ public class SplitSelectStateController { private final Context mContext; private final Handler mHandler; private final RecentsModel mRecentTasksModel; private final SplitAnimationController mSplitAnimationController; private StatsLogManager mStatsLogManager; private final SystemUiProxy mSystemUiProxy; private final StateManager mStateManager; Loading @@ -96,6 +97,11 @@ public class SplitSelectStateController { private boolean mRecentsAnimationRunning; /** If {@code true}, animates the existing task view split placeholder view */ private boolean mAnimateCurrentTaskDismissal; /** * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a * split pair task view without wanting to animate current task dismissal overall */ private boolean mDismissingFromSplitPair; @Nullable private UserHandle mUser; /** If not null, this is the TaskView we want to launch from */ Loading @@ -116,6 +122,7 @@ public class SplitSelectStateController { mStateManager = stateManager; mDepthController = depthController; mRecentTasksModel = recentsModel; mSplitAnimationController = new SplitAnimationController(this); } /** Loading Loading @@ -399,6 +406,18 @@ public class SplitSelectStateController { mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal; } public boolean isDismissingFromSplitPair() { return mDismissingFromSplitPair; } public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) { mDismissingFromSplitPair = dismissingFromSplitPair; } public SplitAnimationController getSplitAnimationController() { return mSplitAnimationController; } /** * Requires Shell Transitions */ Loading Loading @@ -506,6 +525,7 @@ public class SplitSelectStateController { mItemInfo = null; mSplitEvent = null; mAnimateCurrentTaskDismissal = false; mDismissingFromSplitPair = false; } /** Loading @@ -532,6 +552,10 @@ public class SplitSelectStateController { return mInitialTaskId; } public int getSecondTaskId() { return mSecondTaskId; } private boolean isSecondTaskIntentSet() { return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null); } Loading
quickstep/src/com/android/quickstep/views/DesktopTaskView.java +1 −1 Original line number Diff line number Diff line Loading @@ -496,7 +496,7 @@ public class DesktopTaskView extends TaskView { } @Override void setThumbnailVisibility(int visibility) { void setThumbnailVisibility(int visibility, int taskId) { for (int i = 0; i < mSnapshotViewMap.size(); i++) { mSnapshotViewMap.valueAt(i).setVisibility(visibility); } Loading
quickstep/src/com/android/quickstep/views/GroupedTaskView.java +52 −9 Original line number Diff line number Diff line package com.android.quickstep.views; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; Loading @@ -25,6 +27,7 @@ import com.android.quickstep.TaskIconCache; import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.util.CancellableTask; import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.TaskViewSimulator; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; Loading Loading @@ -267,6 +270,19 @@ public class GroupedTaskView extends TaskView { @Override protected int getLastSelectedChildTaskIndex() { SplitSelectStateController splitSelectController = getRecentsView().getSplitSelectController(); if (splitSelectController.isDismissingFromSplitPair()) { // return the container index of the task that wasn't initially selected to split with // because that is the only remaining app that can be selected. The coordinate checks // below aren't reliable since both of those views may be gone/transformed int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); if (initSplitTaskId != INVALID_TASK_ID) { return initSplitTaskId == mTask.key.id ? 1 : 0; } } // Check which of the two apps was selected if (isCoordInView(mIconView2, mLastTouchDownPosition) || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) { return 1; Loading Loading @@ -296,9 +312,30 @@ public class GroupedTaskView extends TaskView { if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) { return; } int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); if (initSplitTaskId == INVALID_TASK_ID) { getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL); // Should we be having a separate translation step apart from the measuring above? // The following only applies to large screen for now, but for future reference // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary // translation directions mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX()); mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY()); mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX()); mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY()); } else { // Currently being split with this taskView, let the non-split selected thumbnail // take up full thumbnail area TaskIdAttributeContainer container = mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0]; container.getThumbnailView().measure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec( heightSize - mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx, MeasureSpec.EXACTLY)); } updateIconPlacement(); } Loading Loading @@ -379,6 +416,12 @@ public class GroupedTaskView extends TaskView { mSnapshotView2.refreshSplashView(); } @Override protected void resetViewTransforms() { super.resetViewTransforms(); mSnapshotView2.resetViewTransforms(); } /** * Sets visibility for thumbnails and associated elements (DWB banners). * IconView is unaffected. Loading @@ -387,13 +430,13 @@ public class GroupedTaskView extends TaskView { * When setting VISIBLE (as a reset), sets the visibility for both tasks. */ @Override void setThumbnailVisibility(int visibility) { void setThumbnailVisibility(int visibility, int taskId) { if (visibility == VISIBLE) { mSnapshotView.setVisibility(visibility); mDigitalWellBeingToast.setBannerVisibility(visibility); mSnapshotView2.setVisibility(visibility); mDigitalWellBeingToast2.setBannerVisibility(visibility); } else if (getLastSelectedChildTaskIndex() == 0) { } else if (taskId == getTaskIds()[0]) { mSnapshotView.setVisibility(visibility); mDigitalWellBeingToast.setBannerVisibility(visibility); } else { Loading
quickstep/src/com/android/quickstep/views/RecentsView.java +53 −25 Original line number Diff line number Diff line Loading @@ -184,6 +184,7 @@ import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps; import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SurfaceTransaction; Loading @@ -192,6 +193,7 @@ import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.VibrationConstants; import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; import com.android.systemui.plugins.ResourceProvider; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; Loading Loading @@ -935,7 +937,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T if (mHandleTaskStackChanges) { TaskView taskView = getTaskViewByTaskId(taskId); if (taskView != null) { for (TaskView.TaskIdAttributeContainer container : for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) { if (container == null || taskId != container.getTask().key.id) { continue; Loading Loading @@ -3099,28 +3101,25 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T RectF startingTaskRect = new RectF(); safeRemoveDragLayerView(mFirstFloatingTaskView); SplitAnimInitProps splitAnimInitProps = mSplitSelectStateController.getSplitAnimationController().getFirstAnimInitViews( () -> mSplitHiddenTaskView, () -> mSplitSelectSource); if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) { // Create the split select animation from Overview mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE); anim.setViewAlpha(mSplitHiddenTaskView.getIconView(), 0, clampToProgress(LINEAR, mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE, mSplitSelectStateController.getInitialTaskId()); anim.setViewAlpha(splitAnimInitProps.getIconView(), 0, clampToProgress(LINEAR, timings.getIconFadeStartOffset(), timings.getIconFadeEndOffset())); } mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, mSplitHiddenTaskView.getThumbnail(), mSplitHiddenTaskView.getThumbnail().getThumbnail(), mSplitHiddenTaskView.getIconView().getDrawable(), startingTaskRect); mFirstFloatingTaskView.setAlpha(1); mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, true /* fadeWithThumbnail */, true /* isStagedTask */); } else { // Create the split select animation from Home mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, mSplitSelectSource.view, null /* thumbnail */, mSplitSelectSource.drawable, startingTaskRect); splitAnimInitProps.getOriginalView(), splitAnimInitProps.getOriginalBitmap(), splitAnimInitProps.getIconDrawable(), startingTaskRect); mFirstFloatingTaskView.setAlpha(1); mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); } splitAnimInitProps.getFadeWithThumbnail(), splitAnimInitProps.isStagedTask()); // Allow user to click staged app to launch into fullscreen if (ENABLE_LAUNCH_FROM_STAGED_APP.get()) { Loading Loading @@ -4450,7 +4449,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T mTaskViewsSecondarySplitTranslation = translation; for (int i = 0; i < getTaskViewCount(); i++) { TaskView taskView = requireTaskViewAt(i); if (taskView == mSplitHiddenTaskView) { if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) { continue; } taskView.getSecondarySplitTranslationProperty().set(taskView, translation); Loading Loading @@ -4501,6 +4500,10 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView); mSplitSelectStateController .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal); // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null && mSplitHiddenTaskView.containsMultipleTasks()); mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent, splitSelectSource.position.stagePosition, splitSelectSource.itemInfo, splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId); Loading @@ -4519,8 +4522,32 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T * Modifies a PendingAnimation with the animations for entering split staging */ public void createSplitSelectInitAnimation(PendingAnimation builder, int duration) { if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) { // Splitting from Overview boolean isInitiatingSplitFromTaskView = mSplitSelectStateController.isAnimateCurrentTaskDismissal(); boolean isInitiatingTaskViewSplitPair = mSplitSelectStateController.isDismissingFromSplitPair(); if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair) { // Splitting from Overview for split pair task createInitialSplitSelectAnimation(builder); // Animate pair thumbnail into full thumbnail boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIdAttributeContainers()[0].getTask().key.id == mSplitSelectStateController.getInitialTaskId(); TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView .getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0]; TaskThumbnailView thumbnail = taskIdAttributeContainer.getThumbnailView(); mSplitSelectStateController.getSplitAnimationController() .addInitialSplitFromPair(taskIdAttributeContainer, builder, mActivity.getDeviceProfile(), mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(), primaryTaskSelected); builder.addOnFrameCallback(() ->{ thumbnail.refreshSplashView(); mSplitHiddenTaskView.updateSnapshotRadius(); }); } else if (isInitiatingSplitFromTaskView) { // Splitting from Overview for fullscreen task createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, true /* dismissingForSplitSelection*/); } else { Loading Loading @@ -4608,7 +4635,8 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T mSecondSplitHiddenView = containerTaskView; if (mSecondSplitHiddenView != null) { mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE); mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE, mSplitSelectStateController.getSecondTaskId()); } InteractionJankMonitorWrapper.begin(this, Loading @@ -4634,7 +4662,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T } if (mSecondSplitHiddenView != null) { mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE); mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID); mSecondSplitHiddenView = null; } Loading @@ -4660,7 +4688,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T resetTaskVisuals(); mSplitHiddenTaskViewIndex = -1; if (mSplitHiddenTaskView != null) { mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE); mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID); mSplitHiddenTaskView = null; } if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { Loading Loading @@ -5530,7 +5558,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T } taskView.setShowScreenshot(true); for (TaskView.TaskIdAttributeContainer container : for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) { if (container == null) { continue; Loading