()
+
+ init {
+ // Add AddDesktopButton and lage tiles to both rows.
+ if (hasAddDesktopButton) {
+ topRowIds += ADD_DESK_PLACEHOLDER_ID
+ bottomRowIds += ADD_DESK_PLACEHOLDER_ID
+ }
+ topRowIds += largeTileIds
+ bottomRowIds += largeTileIds
+
+ // Add row ids to their respective rows.
+ topRowIds += topIds
+ bottomRowIds += bottomIds
+
+ // Fill in the shorter array with the ids from the longer one.
+ topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0))
+ bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0))
+
+ // Add the clear all button to the end of both arrays.
+ topRowIds += CLEAR_ALL_PLACEHOLDER_ID
+ bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID
+ }
+
+ /** Returns the id of the next page in the grid or -1 for the clear all button. */
+ fun getNextGridPage(
+ currentPageTaskViewId: Int,
+ delta: Int,
+ direction: TaskNavDirection,
+ cycle: Boolean,
+ ): Int {
+ val inTop = topRowIds.contains(currentPageTaskViewId)
+ val index =
+ if (inTop) topRowIds.indexOf(currentPageTaskViewId)
+ else bottomRowIds.indexOf(currentPageTaskViewId)
+ val maxSize = max(topRowIds.size, bottomRowIds.size)
+ val nextIndex = index + delta
+
+ return when (direction) {
+ TaskNavDirection.UP,
+ TaskNavDirection.DOWN -> {
+ if (inTop) bottomRowIds[index] else topRowIds[index]
+ }
+ TaskNavDirection.LEFT -> {
+ val boundedIndex =
+ if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1)
+ if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+ }
+ TaskNavDirection.RIGHT -> {
+ val boundedIndex =
+ if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex)
+ else nextIndex.coerceAtLeast(0)
+ val inOriginalTop = topIds.contains(currentPageTaskViewId)
+ if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+ }
+ TaskNavDirection.TAB -> {
+ val boundedIndex =
+ if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize)
+ else nextIndex.coerceAtMost(maxSize - 1)
+ if (delta >= 0) {
+ if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index]
+ else topRowIds[boundedIndex]
+ } else {
+ if (topRowIds.contains(currentPageTaskViewId)) {
+ if (boundedIndex < 0) {
+ // If no cycling, always return the first task.
+ topRowIds[0]
+ } else {
+ bottomRowIds[boundedIndex]
+ }
+ } else {
+ // Go up to top if there is task above
+ if (topRowIds[index] != bottomRowIds[index]) topRowIds[index]
+ else bottomRowIds[boundedIndex]
+ }
+ }
+ }
+ else -> currentPageTaskViewId
+ }
+ }
+
+ /**
+ * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab
+ * navigation, starting from the initial TaskView ID, towards the start or end of the grid.
+ *
+ * A positive delta moves forward in the tab order towards the end of the grid, while a
+ * negative value moves backward towards the beginning. The offset is the distance between
+ * columns the tasks are in.
+ */
+ fun gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId: Int,
+ towardsStart: Boolean,
+ ): Sequence> = sequence {
+ val draggedTaskViewColumn = getColumn(initialTaskViewId)
+ var nextTaskViewId: Int = initialTaskViewId
+ var previousTaskViewId: Int = Int.MIN_VALUE
+ while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) {
+ previousTaskViewId = nextTaskViewId
+ nextTaskViewId =
+ getNextGridPage(
+ nextTaskViewId,
+ if (towardsStart) -1 else 1,
+ TaskNavDirection.TAB,
+ cycle = false,
+ )
+ if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) {
+ val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn)
+ yield(Pair(nextTaskViewId, columnOffset))
+ }
+ }
+ }
+
+ /** Returns the column of a task's id in the grid. */
+ private fun getColumn(taskViewId: Int): Int =
+ if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId)
+ else bottomRowIds.indexOf(taskViewId)
+
+ enum class TaskNavDirection {
+ UP,
+ DOWN,
+ LEFT,
+ RIGHT,
+ TAB,
+ }
+
+ companion object {
+ const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1
+ const val ADD_DESK_PLACEHOLDER_ID: Int = -2
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java
index 69137cc1b6a..43ef39c2568 100644
--- a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java
@@ -17,6 +17,7 @@
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.shared.recents.model.Task;
@@ -94,6 +95,7 @@ public synchronized void removeAll(Predicate keyCheck) {
* Gets the entry if it is still valid
*/
@Override
+ @Nullable
public synchronized V getAndInvalidateIfModified(Task.TaskKey key) {
Entry entry = mMap.get(key.id);
if (entry != null && entry.mKey.windowingMode == key.windowingMode
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java
index 8ee78ab0bab..9df0993a7bd 100644
--- a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java
@@ -15,6 +15,8 @@
*/
package com.android.quickstep.util;
+import androidx.annotation.Nullable;
+
import com.android.systemui.shared.recents.model.Task;
import java.util.function.Predicate;
@@ -44,6 +46,7 @@ public interface TaskKeyCache {
/**
* Gets the entry if it is still valid.
*/
+ @Nullable
V getAndInvalidateIfModified(Task.TaskKey key);
/**
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
index 89f5d41dad7..9fe8cc954db 100644
--- a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
@@ -17,6 +17,8 @@
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.systemui.shared.recents.model.Task.TaskKey;
import java.util.LinkedHashMap;
@@ -59,6 +61,7 @@ public synchronized void removeAll(Predicate keyCheck) {
/**
* Gets the entry if it is still valid
*/
+ @Nullable
public synchronized V getAndInvalidateIfModified(TaskKey key) {
Entry entry = mMap.get(key.id);
diff --git a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
index e80d2a6d3f8..40a328ccfc8 100644
--- a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
@@ -98,10 +98,7 @@ private void checkTaskLaunchFailed() {
final Runnable taskLaunchFailedCallback = mTaskLaunchFailedCallback;
RecentsModel.INSTANCE.get(mContext).isTaskRemoved(mLaunchedTaskId, (taskRemoved) -> {
if (taskRemoved) {
- ActiveGestureLog.INSTANCE.addLog(
- new ActiveGestureLog.CompoundString("Launch failed, task (id=")
- .append(launchedTaskId)
- .append(") finished mid transition"));
+ ActiveGestureProtoLogProxy.logTaskLaunchFailed(launchedTaskId);
taskLaunchFailedCallback.run();
}
}, (task) -> true /* filter */);
diff --git a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
index 91e83769901..6e2d469d185 100644
--- a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
@@ -16,16 +16,11 @@
package com.android.quickstep.util;
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-
-import android.app.Activity;
import android.app.ActivityManager;
import android.util.Log;
import androidx.annotation.NonNull;
-import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
-import com.android.quickstep.RecentsModel;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index fcf303fdd98..58d8b78962e 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -24,10 +24,8 @@
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
-import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
-import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell;
import android.animation.TimeInterpolator;
import android.content.Context;
@@ -43,7 +41,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.app.animation.Interpolators;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
@@ -52,9 +49,10 @@
import com.android.launcher3.util.TraceHelper;
import com.android.quickstep.BaseActivityInterface;
import com.android.quickstep.BaseContainerInterface;
+import com.android.quickstep.DesktopFullscreenDrawParams;
+import com.android.quickstep.FullscreenDrawParams;
import com.android.quickstep.TaskAnimationManager;
import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
-import com.android.quickstep.views.TaskView.FullscreenDrawParams;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.recents.utilities.PreviewPositionHelper;
@@ -99,11 +97,11 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy {
private final FullscreenDrawParams mCurrentFullscreenParams;
public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
+ public final AnimatedFloat taskGridTranslationX = new AnimatedFloat();
+ public final AnimatedFloat taskGridTranslationY = new AnimatedFloat();
// Carousel properties
public final AnimatedFloat carouselScale = new AnimatedFloat();
- public final AnimatedFloat carouselPrimaryTranslation = new AnimatedFloat();
- public final AnimatedFloat carouselSecondaryTranslation = new AnimatedFloat();
// RecentsView properties
public final AnimatedFloat recentsViewScale = new AnimatedFloat();
@@ -118,19 +116,28 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy {
private SplitBounds mSplitBounds;
private Boolean mDrawsBelowRecents = null;
private boolean mIsGridTask;
- private boolean mIsDesktopTask;
- private boolean mScaleToCarouselTaskSize = false;
+ private final boolean mIsDesktopTask;
+ private boolean mIsAnimatingToCarousel = false;
private int mTaskRectTranslationX;
private int mTaskRectTranslationY;
+ private int mDesktopTaskIndex = 0;
- public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) {
+ @Nullable
+ private Matrix mTaskRectTransform = null;
+
+ public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy,
+ boolean isDesktop, int desktopTaskIndex) {
mContext = context;
mSizeStrategy = sizeStrategy;
+ mIsDesktopTask = isDesktop;
+ mDesktopTaskIndex = desktopTaskIndex;
mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init",
() -> new RecentsOrientedState(context, sizeStrategy, i -> { }));
mOrientationState.setGestureActive(true);
- mCurrentFullscreenParams = new FullscreenDrawParams(context);
+ mCurrentFullscreenParams = mIsDesktopTask
+ ? new DesktopFullscreenDrawParams(context)
+ : new FullscreenDrawParams(context);
mOrientationStateId = mOrientationState.getStateId();
Resources resources = context.getResources();
mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
@@ -144,6 +151,9 @@ public void setDp(DeviceProfile dp) {
mDp = dp;
mLayoutValid = false;
mOrientationState.setDeviceProfile(dp);
+ if (enableGridOnlyOverview()) {
+ mIsGridTask = dp.isTablet && !mIsDesktopTask;
+ }
calculateTaskSize();
}
@@ -155,14 +165,16 @@ private void calculateTaskSize() {
if (mIsGridTask) {
mSizeStrategy.calculateGridTaskSize(mContext, mDp, mFullTaskSize,
mOrientationState.getOrientationHandler());
+ if (enableGridOnlyOverview()) {
+ mSizeStrategy.calculateTaskSize(mContext, mDp, mCarouselTaskSize,
+ mOrientationState.getOrientationHandler());
+ }
} else {
mSizeStrategy.calculateTaskSize(mContext, mDp, mFullTaskSize,
mOrientationState.getOrientationHandler());
- }
-
- if (enableGridOnlyOverview()) {
- mSizeStrategy.calculateCarouselTaskSize(mContext, mDp, mCarouselTaskSize,
- mOrientationState.getOrientationHandler());
+ if (enableGridOnlyOverview()) {
+ mCarouselTaskSize.set(mFullTaskSize);
+ }
}
if (mSplitBounds != null) {
@@ -172,7 +184,6 @@ private void calculateTaskSize() {
mTaskRect.set(mFullTaskSize);
mOrientationState.getOrientationHandler()
.setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mStagePosition);
- mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
} else if (mIsDesktopTask) {
// For desktop, tasks can take up only part of the screen size.
// Full task size represents the whole screen size, but scaled down to fit in recents.
@@ -186,10 +197,19 @@ private void calculateTaskSize() {
mTaskRect.scale(scale);
// Ensure the task rect is inside the full task rect
mTaskRect.offset(mFullTaskSize.left, mFullTaskSize.top);
+
+ Rect taskDimension = new Rect(0, 0, (int) fullscreenTaskDimension.x,
+ (int) fullscreenTaskDimension.y);
+ mTmpCropRect.set(mThumbnailPosition);
+ if (mTmpCropRect.setIntersect(taskDimension, mThumbnailPosition)) {
+ mTmpCropRect.offset(-mThumbnailPosition.left, -mThumbnailPosition.top);
+ } else {
+ mTmpCropRect.setEmpty();
+ }
} else {
mTaskRect.set(mFullTaskSize);
- mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
}
+ mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
}
/**
@@ -209,12 +229,7 @@ public float getFullScreenScale() {
}
// Copy mFullTaskSize instead of updating it directly so it could be reused next time
// without recalculating
- Rect scaleRect = new Rect();
- if (mScaleToCarouselTaskSize) {
- scaleRect.set(mCarouselTaskSize);
- } else {
- scaleRect.set(mFullTaskSize);
- }
+ Rect scaleRect = new Rect(mIsAnimatingToCarousel ? mCarouselTaskSize : mFullTaskSize);
scaleRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
float scale = mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot);
if (mPivotOverride != null) {
@@ -248,8 +263,6 @@ public void setPreview(RemoteAnimationTarget runningTarget, SplitBounds splitInf
} else {
mStagePosition = runningTarget.taskId == splitInfo.leftTopTaskId
? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT;
- mPositionHelper.setSplitBounds(convertLauncherSplitBoundsToShell(mSplitBounds),
- mStagePosition);
}
calculateTaskSize();
}
@@ -284,13 +297,6 @@ public void setIsGridTask(boolean isGridTask) {
mIsGridTask = isGridTask;
}
- /**
- * Sets whether this task is part of desktop tasks in overview.
- */
- public void setIsDesktopTask(boolean desktop) {
- mIsDesktopTask = desktop;
- }
-
/**
* Apply translations on TaskRect's starting location.
*/
@@ -302,66 +308,24 @@ public void setTaskRectTranslation(int taskRectTranslationX, int taskRectTransla
}
/**
- * Adds animation for all the components corresponding to transition from an app to overview.
+ * Override the pivot used to apply scale changes.
+ */
+ public void setPivotOverride(PointF pivotOverride) {
+ mPivotOverride = pivotOverride;
+ getFullScreenScale();
+ }
+
+ /**
+ * Adds animation for all the components corresponding to transition from an app to carousel.
*/
- public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) {
+ public void addAppToCarouselAnim(PendingAnimation pa, Interpolator interpolator) {
pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
- float fullScreenScale;
if (enableGridOnlyOverview() && mDp.isTablet && mDp.isGestureMode) {
- // Move pivot to top right edge of the screen, to avoid task scaling down in opposite
- // direction of app window movement, otherwise the animation will wiggle left and right.
- // Also translate the app window to top right edge of the screen to simplify
- // calculations.
- taskPrimaryTranslation.value = mIsRecentsRtl
- ? mDp.widthPx - mFullTaskSize.right
- : -mFullTaskSize.left;
- taskSecondaryTranslation.value = -mFullTaskSize.top;
- mPivotOverride = new PointF(mIsRecentsRtl ? mDp.widthPx : 0, 0);
-
- // Scale down to the carousel and use the carousel Rect to calculate fullScreenScale.
- mScaleToCarouselTaskSize = true;
+ mIsAnimatingToCarousel = true;
carouselScale.value = mCarouselTaskSize.width() / (float) mFullTaskSize.width();
- fullScreenScale = getFullScreenScale();
-
- float carouselPrimaryTranslationTarget = mIsRecentsRtl
- ? mCarouselTaskSize.right - mDp.widthPx
- : mCarouselTaskSize.left;
- float carouselSecondaryTranslationTarget = mCarouselTaskSize.top;
-
- // Expected carousel position's center is in the middle, and invariant of
- // recentsViewScale.
- float exceptedCarouselCenterX = mCarouselTaskSize.centerX();
- // Animating carousel translations linearly will result in a curved path, therefore
- // we'll need to calculate the expected translation at each recentsView scale. Luckily
- // primary and secondary follow the same translation, and primary is used here due to
- // it being simpler.
- Interpolator carouselTranslationInterpolator = t -> {
- // recentsViewScale is calculated rather than using recentsViewScale.value, so that
- // this interpolator works independently even if recentsViewScale don't animate.
- float recentsViewScale =
- Utilities.mapToRange(t, 0, 1, fullScreenScale, 1, Interpolators.LINEAR);
- // Without the translation, the app window will animate from fullscreen into top
- // right corner.
- float expectedTaskCenterX = mIsRecentsRtl
- ? mDp.widthPx - mCarouselTaskSize.width() * recentsViewScale / 2f
- : mCarouselTaskSize.width() * recentsViewScale / 2f;
- // Calculate the expected translation, then work back the animatedFraction that
- // results in this value.
- float carouselPrimaryTranslation =
- (exceptedCarouselCenterX - expectedTaskCenterX) / recentsViewScale;
- return carouselPrimaryTranslation / carouselPrimaryTranslationTarget;
- };
-
- // Use addAnimatedFloat so this animation can later be canceled and animate to a
- // different value in RecentsView.onPrepareGestureEndAnimation.
- pa.addAnimatedFloat(carouselPrimaryTranslation, 0, carouselPrimaryTranslationTarget,
- carouselTranslationInterpolator);
- pa.addAnimatedFloat(carouselSecondaryTranslation, 0, carouselSecondaryTranslationTarget,
- carouselTranslationInterpolator);
- } else {
- fullScreenScale = getFullScreenScale();
}
- pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, fullScreenScale, 1, interpolator);
+ pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1,
+ interpolator);
}
/**
@@ -405,6 +369,38 @@ public Matrix getCurrentMatrix() {
return mMatrix;
}
+ /**
+ * Sets a matrix used to transform the position of tasks. If set, this matrix is applied to
+ * the task rect after the task has been scaled and positioned inside the fulltask, but
+ * before scaling and translation of the whole recents view is performed.
+ */
+ public void setTaskRectTransform(@Nullable Matrix taskRectTransform) {
+ mTaskRectTransform = taskRectTransform;
+ }
+
+ /**
+ * Calculates the crop rect for desktop tasks given the current matrix.
+ */
+ private void calculateDesktopTaskCropRect() {
+ // The approach here is to map a rect that represents the untransformed thumbnail position
+ // using the current matrix. This will give us a rect that can be intersected with
+ // [mFullTaskSize]. Using the intersection, we then compute how much of the task window that
+ // needs to be cropped (which will be nothing if the window is entirely within the desktop).
+ mTempRectF.set(0, 0, mThumbnailPosition.width(), mThumbnailPosition.height());
+ mMatrix.mapRect(mTempRectF);
+
+ float offsetX = mTempRectF.left;
+ float offsetY = mTempRectF.top;
+ float scale = mThumbnailPosition.width() / mTempRectF.width();
+
+ if (mTempRectF.intersect(mFullTaskSize.left, mFullTaskSize.top, mFullTaskSize.right,
+ mFullTaskSize.bottom)) {
+ mTempRectF.offset(-offsetX, -offsetY);
+ mTempRectF.scale(scale);
+ mTempRectF.round(mTmpCropRect);
+ }
+ }
+
/**
* Applies the rotation on the matrix to so that it maps from launcher coordinate space to
* window coordinate space.
@@ -466,18 +462,29 @@ public void apply(TransformParams params, @Nullable SurfaceTransaction surfaceTr
mMatrix.set(mPositionHelper.getMatrix());
- // Apply TaskView matrix: taskRect, translate
+ // Apply TaskView matrix: taskRect, optional transform, translate
mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+ if (mTaskRectTransform != null) {
+ mMatrix.postConcat(mTaskRectTransform);
+
+ // Calculate cropping for desktop tasks. The order is important since it uses the
+ // current matrix. Therefore we calculate it here, after applying the task rect
+ // transform, but before applying scaling/translation that affects the whole
+ // recentsview.
+ if (mIsDesktopTask) {
+ calculateDesktopTaskCropRect();
+ }
+ }
+
mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE,
taskPrimaryTranslation.value);
mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
taskSecondaryTranslation.value);
+ mMatrix.postTranslate(taskGridTranslationX.value, taskGridTranslationY.value);
- mMatrix.postScale(carouselScale.value, carouselScale.value, mPivot.x, mPivot.y);
- mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE,
- carouselPrimaryTranslation.value);
- mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
- carouselSecondaryTranslation.value);
+ mMatrix.postScale(carouselScale.value, carouselScale.value,
+ mIsRecentsRtl ? mCarouselTaskSize.right : mCarouselTaskSize.left,
+ mCarouselTaskSize.top);
mOrientationState.getOrientationHandler().setPrimary(
mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value);
@@ -490,10 +497,12 @@ public void apply(TransformParams params, @Nullable SurfaceTransaction surfaceTr
recentsViewPrimaryTranslation.value);
applyWindowToHomeRotation(mMatrix);
- // Crop rect is the inverse of thumbnail matrix
- mTempRectF.set(0, 0, taskWidth, taskHeight);
- mInversePositionMatrix.mapRect(mTempRectF);
- mTempRectF.roundOut(mTmpCropRect);
+ if (!mIsDesktopTask) {
+ // Crop rect is the inverse of thumbnail matrix
+ mTempRectF.set(0, 0, taskWidth, taskHeight);
+ mInversePositionMatrix.mapRect(mTempRectF);
+ mTempRectF.roundOut(mTmpCropRect);
+ }
params.setProgress(1f - fullScreenProgress);
params.applySurfaceParams(surfaceTransaction == null
@@ -511,8 +520,8 @@ public void apply(TransformParams params, @Nullable SurfaceTransaction surfaceTr
+ " taskRect: " + mTaskRect
+ " taskPrimaryT: " + taskPrimaryTranslation.value
+ " taskSecondaryT: " + taskSecondaryTranslation.value
- + " carouselPrimaryT: " + carouselPrimaryTranslation.value
- + " carouselSecondaryT: " + carouselSecondaryTranslation.value
+ + " taskGridTranslationX: " + taskGridTranslationX.value
+ + " taskGridTranslationY: " + taskGridTranslationY.value
+ " recentsPrimaryT: " + recentsViewPrimaryTranslation.value
+ " recentsSecondaryT: " + recentsViewSecondaryTranslation.value
+ " recentsScroll: " + recentsViewScroll.value
@@ -529,21 +538,12 @@ public void onBuildTargetParams(
// If mDrawsBelowRecents is unset, no reordering will be enforced.
if (mDrawsBelowRecents != null) {
- // In legacy transitions, the animation leashes remain in same hierarchy in the
- // TaskDisplayArea, so we don't want to bump the layer too high otherwise it will
- // conflict with layers that WM core positions (ie. the input consumers). For shell
- // transitions, the animation leashes are reparented to an animation container so we
- // can bump layers as needed.
- if (ENABLE_SHELL_TRANSITIONS) {
- builder.setLayer(mDrawsBelowRecents
- ? Integer.MIN_VALUE + app.prefixOrderIndex
- // 1000 is an arbitrary number to give room for multiple layers.
- : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex);
- } else {
- builder.setLayer(mDrawsBelowRecents
- ? Integer.MIN_VALUE + app.prefixOrderIndex
- : 0);
- }
+ // In shell transitions, the animation leashes are reparented to an animation container
+ // so we can bump layers as needed.
+ builder.setLayer(mDrawsBelowRecents
+ // 1000 is an arbitrary number to give room for multiple layers.
+ ? Integer.MIN_VALUE + 1000 + app.prefixOrderIndex - mDesktopTaskIndex
+ : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex - mDesktopTaskIndex);
}
}
@@ -552,7 +552,7 @@ public void onBuildTargetParams(
* TaskView
*/
public float getCurrentCornerRadius() {
- float visibleRadius = mCurrentFullscreenParams.getCurrentDrawnCornerRadius();
+ float visibleRadius = mCurrentFullscreenParams.getCurrentCornerRadius();
mTempPoint[0] = visibleRadius;
mTempPoint[1] = 0;
mInversePositionMatrix.mapVectors(mTempPoint);
diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
index 66bff730bfc..519ef60ecae 100644
--- a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java
@@ -16,6 +16,7 @@
package com.android.quickstep.util;
+import android.annotation.NonNull;
import android.os.UserHandle;
import com.android.systemui.shared.recents.model.Task;
@@ -36,7 +37,7 @@ default Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
/**
* Called when the icon for a task changes
*/
- default void onTaskIconChanged(String pkg, UserHandle user) {}
+ default void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {}
/**
* Called when the icon for a task changes
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index 9bad1108bb8..cb591ed0358 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -19,9 +19,16 @@
import android.util.FloatProperty;
import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import androidx.annotation.VisibleForTesting;
import com.android.quickstep.RemoteAnimationTargets;
import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
+import com.android.window.flags.Flags;
+
+import java.util.function.Supplier;
public class TransformParams {
@@ -56,15 +63,24 @@ public Float get(TransformParams params) {
private float mTargetAlpha;
private float mCornerRadius;
private RemoteAnimationTargets mTargetSet;
+ private TransitionInfo mTransitionInfo;
+ private boolean mCornerRadiusIsOverridden;
private SurfaceTransactionApplier mSyncTransactionApplier;
+ private Supplier mSurfaceTransactionSupplier;
private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
public TransformParams() {
+ this(SurfaceTransaction::new);
+ }
+
+ @VisibleForTesting
+ public TransformParams(Supplier surfaceTransactionSupplier) {
mProgress = 0;
mTargetAlpha = 1;
mCornerRadius = -1;
+ mSurfaceTransactionSupplier = surfaceTransactionSupplier;
}
/**
@@ -106,6 +122,15 @@ public TransformParams setTargetSet(RemoteAnimationTargets targetSet) {
return this;
}
+ /**
+ * Provides the {@code TransitionInfo} of the transition that this transformation stems from.
+ */
+ public TransformParams setTransitionInfo(TransitionInfo transitionInfo) {
+ mTransitionInfo = transitionInfo;
+ mCornerRadiusIsOverridden = false;
+ return this;
+ }
+
/**
* Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that
* are computed based on these TransformParams.
@@ -136,26 +161,31 @@ public TransformParams setHomeBuilderProxy(BuilderProxy proxy) {
/** Builds the SurfaceTransaction from the given BuilderProxy params. */
public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) {
RemoteAnimationTargets targets = mTargetSet;
- SurfaceTransaction transaction = new SurfaceTransaction();
+ SurfaceTransaction transaction = mSurfaceTransactionSupplier.get();
if (targets == null) {
return transaction;
}
for (int i = 0; i < targets.unfilteredApps.length; i++) {
RemoteAnimationTarget app = targets.unfilteredApps[i];
SurfaceProperties builder = transaction.forSurface(app.leash);
+ BuilderProxy targetProxy =
+ app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME
+ ? mHomeBuilderProxy
+ : (app.mode == targets.targetMode ? proxy : mBaseBuilderProxy);
if (app.mode == targets.targetMode) {
- int activityType = app.windowConfiguration.getActivityType();
- if (activityType == ACTIVITY_TYPE_HOME) {
- mHomeBuilderProxy.onBuildTargetParams(builder, app, this);
- } else {
- builder.setAlpha(getTargetAlpha());
- proxy.onBuildTargetParams(builder, app, this);
- }
- } else {
- mBaseBuilderProxy.onBuildTargetParams(builder, app, this);
+ builder.setAlpha(getTargetAlpha());
+ }
+ targetProxy.onBuildTargetParams(builder, app, this);
+ // Override the corner radius for {@code app} with the leash used by Shell, so that it
+ // doesn't interfere with the window clip and corner radius applied here.
+ // Only override the corner radius once - so that we don't accidentally override at the
+ // end of transition after WM Shell has reset the corner radius of the task.
+ if (!mCornerRadiusIsOverridden) {
+ overrideFreeformChangeLeashCornerRadiusToZero(app, transaction.getTransaction());
}
}
+ mCornerRadiusIsOverridden = true;
// always put wallpaper layer to bottom.
final int wallpaperLength = targets.wallpapers != null ? targets.wallpapers.length : 0;
@@ -166,7 +196,33 @@ public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) {
return transaction;
}
- // Pubic getters so outside packages can read the values.
+ private void overrideFreeformChangeLeashCornerRadiusToZero(
+ RemoteAnimationTarget app, SurfaceControl.Transaction transaction) {
+ if (!Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+ return;
+ }
+ if (app.taskInfo == null || !app.taskInfo.isFreeform()) {
+ return;
+ }
+
+ SurfaceControl changeLeash = getChangeLeashForApp(app);
+ if (changeLeash != null) {
+ transaction.setCornerRadius(changeLeash, 0);
+ }
+ }
+
+ private SurfaceControl getChangeLeashForApp(RemoteAnimationTarget app) {
+ if (mTransitionInfo == null) return null;
+ for (TransitionInfo.Change change : mTransitionInfo.getChanges()) {
+ if (change.getTaskInfo() == null) continue;
+ if (change.getTaskInfo().taskId == app.taskId) {
+ return change.getLeash();
+ }
+ }
+ return null;
+ }
+
+ // Public getters so outside packages can read the values.
public float getProgress() {
return mProgress;
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
index 0a97793b289..32e0e132e05 100644
--- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
@@ -30,6 +30,7 @@
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.util.FloatProperty;
+import android.util.Log;
import android.view.View;
import com.android.app.animation.Interpolators;
@@ -51,6 +52,8 @@
*/
public class WorkspaceRevealAnim {
+ private static final String TAG = "WorkspaceRevealAnim";
+
// Should be used for animations running alongside this WorkspaceRevealAnim.
public static final int DURATION_MS = 350;
private static final FloatProperty> WORKSPACE_SCALE_PROPERTY =
@@ -97,6 +100,19 @@ public WorkspaceRevealAnim(Launcher launcher, boolean animateOverviewScrim) {
mAnimators.setDuration(DURATION_MS);
mAnimators.setInterpolator(Interpolators.DECELERATED_EASE);
+ mAnimators.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ Log.d(TAG, "onAnimationCancel");
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ Log.d(TAG, "onAnimationEnd: workspace alpha = " + workspace.getAlpha());
+ }
+ });
}
private void addRevealAnimatorsForView(T v, FloatProperty scaleProperty) {
diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
index 09563f55279..915c9e5305d 100644
--- a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
+++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
@@ -22,7 +22,6 @@ import com.android.launcher3.Alarm
import com.android.launcher3.DeviceProfile
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
import com.android.launcher3.anim.PendingAnimation
-import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.uioverrides.QuickstepLauncher
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
@@ -30,7 +29,7 @@ import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionPr
/** Controls animations that are happening during unfolding foldable devices */
class LauncherUnfoldTransitionController(
private val launcher: QuickstepLauncher,
- private val progressProvider: ProxyUnfoldTransitionProvider
+ private val progressProvider: ProxyUnfoldTransitionProvider,
) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener {
private var isTablet: Boolean? = null
@@ -57,10 +56,6 @@ class LauncherUnfoldTransitionController(
}
override fun onDeviceProfileChanged(dp: DeviceProfile) {
- if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
- return
- }
-
if (isTablet != null && dp.isTablet != isTablet) {
// We should preemptively start the animation only if:
// - We changed to the unfolded screen
@@ -93,7 +88,7 @@ class LauncherUnfoldTransitionController(
provider = this,
factory = this::onPrepareUnfoldAnimation,
duration =
- 1000L // The expected duration for the animation. Then only comes to play if we have
+ 1000L, // The expected duration for the animation. Then only comes to play if we have
// to run the animation ourselves in case sysui misses the end signal
)
timeoutAlarm.cancelAlarm()
@@ -119,7 +114,7 @@ class LauncherUnfoldTransitionController(
launcher,
isVertical,
dp.displayInfo.currentSize,
- anim
+ anim,
)
}
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
new file mode 100644
index 00000000000..37359a1b356
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.util.FloatProperty
+import android.widget.ImageButton
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.R
+import com.android.launcher3.util.KFloatProperty
+import com.android.launcher3.util.MultiPropertyDelegate
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.quickstep.util.BorderAnimator
+import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator
+
+/**
+ * Button for supporting multiple desktop sessions. The button will be next to the first TaskView
+ * inside overview, while clicking this button will create a new desktop session.
+ */
+class AddDesktopButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ ImageButton(context, attrs) {
+
+ private val addDeskButtonAlpha = MultiValueAlpha(this, Alpha.entries.size)
+ var contentAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.CONTENT)
+ var visibilityAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.VISIBILITY)
+
+ private val multiTranslationX =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_X, TranslationX.entries.size) { a: Float, b: Float
+ ->
+ a + b
+ }
+ var gridTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.GRID)
+ var offsetTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.OFFSET)
+
+ private val focusBorderAnimator: BorderAnimator =
+ createSimpleBorderAnimator(
+ context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_size),
+ context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width),
+ this::getBorderBounds,
+ this,
+ context
+ .obtainStyledAttributes(attrs, R.styleable.AddDesktopButton)
+ .getColor(
+ R.styleable.AddDesktopButton_focusBorderColor,
+ BorderAnimator.DEFAULT_BORDER_COLOR,
+ ),
+ )
+
+ var borderEnabled = false
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ focusBorderAnimator.setBorderVisibility(visible = field && isFocused, animated = true)
+ }
+
+ public override fun onFocusChanged(
+ gainFocus: Boolean,
+ direction: Int,
+ previouslyFocusedRect: Rect?,
+ ) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+ if (borderEnabled) {
+ focusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true)
+ }
+ }
+
+ private fun getBorderBounds(bounds: Rect) {
+ bounds.set(0, 0, width, height)
+ val outlinePadding =
+ context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_outline_padding)
+ bounds.inset(-outlinePadding, -outlinePadding)
+ }
+
+ override fun draw(canvas: Canvas) {
+ focusBorderAnimator.drawBorder(canvas)
+ super.draw(canvas)
+ }
+
+ companion object {
+ private enum class Alpha {
+ CONTENT,
+ VISIBILITY,
+ }
+
+ private enum class TranslationX {
+ GRID,
+ OFFSET,
+ }
+
+ @JvmField
+ val VISIBILITY_ALPHA: FloatProperty =
+ KFloatProperty(AddDesktopButton::visibilityAlpha)
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/BlurUtils.kt b/quickstep/src/com/android/quickstep/views/BlurUtils.kt
new file mode 100644
index 00000000000..d6b2a055b7b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/BlurUtils.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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.views
+
+import com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur
+import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle
+
+/** Applies blur either behind launcher surface or live tile app. */
+class BlurUtils(private val recentsView: RecentsView<*, *>) {
+
+ fun setDrawLiveTileBelowRecents(drawBelowRecents: Boolean) {
+ val liveTileRemoteTargetHandles =
+ if (
+ recentsView.remoteTargetHandles != null &&
+ recentsView.recentsAnimationController != null
+ )
+ recentsView.remoteTargetHandles
+ else null
+ setDrawBelowRecents(drawBelowRecents, liveTileRemoteTargetHandles)
+ }
+
+ /**
+ * Set surface in [remoteTargetHandles] to be above or below Recents layer, and update the base
+ * layer to apply blur to in BaseDepthController.
+ */
+ fun setDrawBelowRecents(
+ drawBelowRecents: Boolean,
+ remoteTargetHandles: Array? = null,
+ ) {
+ remoteTargetHandles?.forEach { it.taskViewSimulator.setDrawsBelowRecents(drawBelowRecents) }
+ if (enableOverviewBackgroundWallpaperBlur()) {
+ recentsView.depthController?.setBaseSurfaceOverride(
+ // Blurs behind launcher layer.
+ if (!drawBelowRecents || remoteTargetHandles == null) {
+ null
+ } else {
+ // Blurs behind live tile. blur will be applied behind window
+ // which farthest from user in case of desktop and split apps.
+ remoteTargetHandles
+ .maxByOrNull { it.transformParams.targetSet.firstAppTarget.leash.layerId }
+ ?.transformParams
+ ?.targetSet
+ ?.firstAppTarget
+ ?.leash
+ }
+ )
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
deleted file mode 100644
index a5db439b4c3..00000000000
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ /dev/null
@@ -1,345 +0,0 @@
-/*
- * Copyright (C) 2018 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.views;
-
-import static com.android.launcher3.Flags.enableGridOnlyOverview;
-import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.widget.Button;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Flags;
-import com.android.launcher3.R;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.BorderAnimator;
-
-import kotlin.Unit;
-
-import app.lawnchair.font.FontManager;
-import app.lawnchair.theme.drawable.DrawableTokens;
-
-public class ClearAllButton extends Button {
-
- public static final FloatProperty VISIBILITY_ALPHA = new FloatProperty(
- "visibilityAlpha") {
- @Override
- public Float get(ClearAllButton view) {
- return view.mVisibilityAlpha;
- }
-
- @Override
- public void setValue(ClearAllButton view, float v) {
- view.setVisibilityAlpha(v);
- }
- };
-
- public static final FloatProperty DISMISS_ALPHA = new FloatProperty(
- "dismissAlpha") {
- @Override
- public Float get(ClearAllButton view) {
- return view.mDismissAlpha;
- }
-
- @Override
- public void setValue(ClearAllButton view, float v) {
- view.setDismissAlpha(v);
- }
- };
-
- private final RecentsViewContainer mContainer;
- private float mScrollAlpha = 1;
- private float mContentAlpha = 1;
- private float mVisibilityAlpha = 1;
- private float mDismissAlpha = 1;
- private float mFullscreenProgress = 1;
- private float mGridProgress = 1;
-
- private boolean mIsRtl;
- private float mNormalTranslationPrimary;
- private float mFullscreenTranslationPrimary;
- private float mGridTranslationPrimary;
- private float mGridScrollOffset;
- private float mScrollOffsetPrimary;
-
- private int mSidePadding;
- private int mOutlinePadding;
- private boolean mBorderEnabled;
- @Nullable
- private final BorderAnimator mFocusBorderAnimator;
-
- public ClearAllButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- mContainer = RecentsViewContainer.containerFromContext(context);
- FontManager.INSTANCE.get(context).overrideFont(this, attrs);
- setBackground(DrawableTokens.BgOverviewClearAllButton.resolve(context));
- if (Flags.enableFocusOutline()) {
- TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
- R.styleable.ClearAllButton);
- Resources resources = getResources();
- mOutlinePadding = resources.getDimensionPixelSize(
- R.dimen.recents_clear_all_outline_padding);
- mFocusBorderAnimator = BorderAnimator.createSimpleBorderAnimator(
- /* borderRadiusPx= */ resources.getDimensionPixelSize(
- R.dimen.recents_clear_all_outline_radius),
- /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
- R.dimen.keyboard_quick_switch_border_width),
- /* boundsBuilder= */ this::updateBorderBounds,
- /* targetView= */ this,
- /* borderColor= */ styledAttrs.getColor(
- R.styleable.ClearAllButton_focusBorderColor,
- DEFAULT_BORDER_COLOR));
- styledAttrs.recycle();
- } else {
- mFocusBorderAnimator = null;
- }
- }
-
- private Unit updateBorderBounds(@NonNull Rect bounds) {
- bounds.set(0, 0, getWidth(), getHeight());
- // Make the value negative to form a padding between button and outline
- bounds.inset(-mOutlinePadding, -mOutlinePadding);
- return Unit.INSTANCE;
- }
-
- @Override
- public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
- super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- if (mFocusBorderAnimator != null && mBorderEnabled) {
- mFocusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true);
- }
- }
-
- /**
- * Enable or disable showing border on focus change
- */
- public void setBorderEnabled(boolean enabled) {
- if (mBorderEnabled == enabled) {
- return;
- }
-
- mBorderEnabled = enabled;
- if (mFocusBorderAnimator != null) {
- mFocusBorderAnimator.setBorderVisibility(/* visible= */
- enabled && isFocused(), /* animated= */true);
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mFocusBorderAnimator != null) {
- mFocusBorderAnimator.drawBorder(canvas);
- }
- super.draw(canvas);
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- RecentsPagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
- mSidePadding = orientationHandler.getClearAllSidePadding(getRecentsView(), mIsRtl);
- }
-
- private RecentsView getRecentsView() {
- return (RecentsView) getParent();
- }
-
- @Override
- public void onRtlPropertiesChanged(int layoutDirection) {
- super.onRtlPropertiesChanged(layoutDirection);
- mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- public float getScrollAlpha() {
- return mScrollAlpha;
- }
-
- public void setContentAlpha(float alpha) {
- if (mContentAlpha != alpha) {
- mContentAlpha = alpha;
- updateAlpha();
- }
- }
-
- public void setVisibilityAlpha(float alpha) {
- if (mVisibilityAlpha != alpha) {
- mVisibilityAlpha = alpha;
- updateAlpha();
- }
- }
-
- public void setDismissAlpha(float alpha) {
- if (mDismissAlpha != alpha) {
- mDismissAlpha = alpha;
- updateAlpha();
- }
- }
-
- public void onRecentsViewScroll(int scroll, boolean gridEnabled) {
- RecentsView recentsView = getRecentsView();
- if (recentsView == null) {
- return;
- }
-
- RecentsPagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
- float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
- if (orientationSize == 0) {
- return;
- }
-
- int clearAllScroll = recentsView.getClearAllScroll();
- int adjustedScrollFromEdge = Math.abs(scroll - clearAllScroll);
- float shift = Math.min(adjustedScrollFromEdge, orientationSize);
- mNormalTranslationPrimary = mIsRtl ? -shift : shift;
- if (!gridEnabled) {
- mNormalTranslationPrimary += mSidePadding;
- }
- applyPrimaryTranslation();
- applySecondaryTranslation();
- float clearAllSpacing = recentsView.getPageSpacing() + recentsView.getClearAllExtraPageSpacing();
- clearAllSpacing = mIsRtl ? -clearAllSpacing : clearAllSpacing;
- mScrollAlpha = Math.max((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing, 0);
- updateAlpha();
- }
-
- private void updateAlpha() {
- final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha * mDismissAlpha;
- setAlpha(alpha);
- setClickable(Math.min(alpha, 1) == 1);
- }
-
- public void setFullscreenTranslationPrimary(float fullscreenTranslationPrimary) {
- mFullscreenTranslationPrimary = fullscreenTranslationPrimary;
- applyPrimaryTranslation();
- }
-
- public void setGridTranslationPrimary(float gridTranslationPrimary) {
- mGridTranslationPrimary = gridTranslationPrimary;
- applyPrimaryTranslation();
- }
-
- public void setGridScrollOffset(float gridScrollOffset) {
- mGridScrollOffset = gridScrollOffset;
- }
-
- public void setScrollOffsetPrimary(float scrollOffsetPrimary) {
- mScrollOffsetPrimary = scrollOffsetPrimary;
- }
-
- public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
- float scrollAdjustment = 0;
- if (fullscreenEnabled) {
- scrollAdjustment += mFullscreenTranslationPrimary;
- }
- if (gridEnabled) {
- scrollAdjustment += mGridTranslationPrimary + mGridScrollOffset;
- }
- scrollAdjustment += mScrollOffsetPrimary;
- return scrollAdjustment;
- }
-
- public float getOffsetAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
- return getScrollAdjustment(fullscreenEnabled, gridEnabled);
- }
-
- /**
- * Adjust translation when this TaskView is about to be shown fullscreen.
- *
- * @param progress: 0 = no translation; 1 = translate according to TaskVIew
- * translations.
- */
- public void setFullscreenProgress(float progress) {
- mFullscreenProgress = progress;
- applyPrimaryTranslation();
- }
-
- /**
- * Moves ClearAllButton between carousel and 2 row grid.
- *
- * @param gridProgress 0 = carousel; 1 = 2 row grid.
- */
- public void setGridProgress(float gridProgress) {
- mGridProgress = gridProgress;
- applyPrimaryTranslation();
- }
-
- private void applyPrimaryTranslation() {
- RecentsView recentsView = getRecentsView();
- if (recentsView == null) {
- return;
- }
-
- RecentsPagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
- orientationHandler.getPrimaryViewTranslate().set(this,
- orientationHandler.getPrimaryValue(0f, getOriginalTranslationY())
- + mNormalTranslationPrimary + getFullscreenTrans(
- mFullscreenTranslationPrimary)
- + getGridTrans(mGridTranslationPrimary));
- }
-
- private void applySecondaryTranslation() {
- RecentsView recentsView = getRecentsView();
- if (recentsView == null) {
- return;
- }
-
- RecentsPagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
- orientationHandler.getSecondaryViewTranslate().set(this,
- orientationHandler.getSecondaryValue(0f, getOriginalTranslationY()));
- }
-
- private float getFullscreenTrans(float endTranslation) {
- return mFullscreenProgress > 0 ? endTranslation : 0;
- }
-
- private float getGridTrans(float endTranslation) {
- return mGridProgress > 0 ? endTranslation : 0;
- }
-
- /**
- * Get the Y translation that is set in the original layout position, before
- * scrolling.
- */
- private float getOriginalTranslationY() {
- DeviceProfile deviceProfile = mContainer.getDeviceProfile();
- if (deviceProfile.isTablet) {
- if (enableGridOnlyOverview()) {
- return (getRecentsView().getLastComputedTaskSize().height()
- + deviceProfile.overviewTaskThumbnailTopMarginPx) / 2.0f
- + deviceProfile.overviewRowSpacing;
- } else {
- return deviceProfile.overviewRowSpacing;
- }
- }
- return deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.kt b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt
new file mode 100644
index 00000000000..69c85eeaeb0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2025 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.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.util.FloatProperty
+import android.widget.Button
+import com.android.launcher3.Flags.enableFocusOutline
+import com.android.launcher3.R
+import com.android.launcher3.util.KFloatProperty
+import com.android.launcher3.util.MultiPropertyDelegate
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.quickstep.util.BorderAnimator
+import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator
+import kotlin.math.abs
+import kotlin.math.min
+
+class ClearAllButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ Button(context, attrs) {
+
+ private val clearAllButtonAlpha =
+ object : MultiValueAlpha(this, Alpha.entries.size) {
+ override fun apply(value: Float) {
+ super.apply(value)
+ isClickable = value >= 1f
+ }
+ }
+ var scrollAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.SCROLL)
+ var contentAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.CONTENT)
+ var visibilityAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.VISIBILITY)
+ var dismissAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.DISMISS)
+
+ var fullscreenProgress = 1f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ applyPrimaryTranslation()
+ }
+
+ /**
+ * Moves ClearAllButton between carousel and 2 row grid.
+ *
+ * 0 = carousel; 1 = 2 row grid.
+ */
+ var gridProgress = 1f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ applyPrimaryTranslation()
+ }
+
+ private var normalTranslationPrimary = 0f
+ var fullscreenTranslationPrimary = 0f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ applyPrimaryTranslation()
+ }
+
+ var gridTranslationPrimary = 0f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ applyPrimaryTranslation()
+ }
+
+ /** Used to put the button at the middle in the secondary coordinate. */
+ var taskAlignmentTranslationY = 0f
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ applySecondaryTranslation()
+ }
+
+ var gridScrollOffset = 0f
+ var scrollOffsetPrimary = 0f
+
+ private var sidePadding = 0
+ var borderEnabled = false
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true)
+ }
+
+ private val focusBorderAnimator: BorderAnimator? =
+ if (enableFocusOutline())
+ createSimpleBorderAnimator(
+ context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_radius),
+ context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width),
+ this::getBorderBounds,
+ this,
+ context
+ .obtainStyledAttributes(attrs, R.styleable.ClearAllButton)
+ .getColor(
+ R.styleable.ClearAllButton_focusBorderColor,
+ BorderAnimator.DEFAULT_BORDER_COLOR,
+ ),
+ )
+ else null
+
+ private fun getBorderBounds(bounds: Rect) {
+ bounds.set(0, 0, width, height)
+ val outlinePadding =
+ context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_padding)
+ // Make the value negative to form a padding between button and outline
+ bounds.inset(-outlinePadding, -outlinePadding)
+ }
+
+ public override fun onFocusChanged(
+ gainFocus: Boolean,
+ direction: Int,
+ previouslyFocusedRect: Rect?,
+ ) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+ if (borderEnabled) {
+ focusBorderAnimator?.setBorderVisibility(gainFocus, /* animated= */ true)
+ }
+ }
+
+ override fun draw(canvas: Canvas) {
+ focusBorderAnimator?.drawBorder(canvas)
+ super.draw(canvas)
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ sidePadding =
+ recentsView?.let { it.pagedOrientationHandler?.getClearAllSidePadding(it, isLayoutRtl) }
+ ?: 0
+ }
+
+ private val recentsView: RecentsView<*, *>?
+ get() = parent as? RecentsView<*, *>?
+
+ override fun hasOverlappingRendering() = false
+
+ fun onRecentsViewScroll(scroll: Int, gridEnabled: Boolean) {
+ val recentsView = recentsView ?: return
+
+ val orientationSize =
+ recentsView.pagedOrientationHandler.getPrimaryValue(width, height).toFloat()
+ if (orientationSize == 0f) {
+ return
+ }
+
+ val clearAllScroll = recentsView.clearAllScroll
+ val adjustedScrollFromEdge = abs((scroll - clearAllScroll)).toFloat()
+ val shift = min(adjustedScrollFromEdge, orientationSize)
+ normalTranslationPrimary = if (isLayoutRtl) -shift else shift
+ if (!gridEnabled) {
+ normalTranslationPrimary += sidePadding.toFloat()
+ }
+ applyPrimaryTranslation()
+ applySecondaryTranslation()
+ var clearAllSpacing = recentsView.pageSpacing + recentsView.clearAllExtraPageSpacing
+ clearAllSpacing = if (isLayoutRtl) -clearAllSpacing else clearAllSpacing
+ scrollAlpha =
+ ((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing.toFloat()).coerceAtLeast(
+ 0f
+ )
+ }
+
+ fun getScrollAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean): Float {
+ var scrollAdjustment = 0f
+ if (fullscreenEnabled) {
+ scrollAdjustment += fullscreenTranslationPrimary
+ }
+ if (gridEnabled) {
+ scrollAdjustment += gridTranslationPrimary + gridScrollOffset
+ }
+ scrollAdjustment += scrollOffsetPrimary
+ return scrollAdjustment
+ }
+
+ fun getOffsetAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean) =
+ getScrollAdjustment(fullscreenEnabled, gridEnabled)
+
+ private fun applyPrimaryTranslation() {
+ val recentsView = recentsView ?: return
+ val orientationHandler = recentsView.pagedOrientationHandler
+ orientationHandler.primaryViewTranslate.set(
+ this,
+ (orientationHandler.getPrimaryValue(0f, taskAlignmentTranslationY) +
+ normalTranslationPrimary +
+ getFullscreenTrans(fullscreenTranslationPrimary) +
+ getGridTrans(gridTranslationPrimary)),
+ )
+ }
+
+ private fun applySecondaryTranslation() {
+ val recentsView = recentsView ?: return
+ val orientationHandler = recentsView.pagedOrientationHandler
+ orientationHandler.secondaryViewTranslate.set(
+ this,
+ orientationHandler.getSecondaryValue(0f, taskAlignmentTranslationY),
+ )
+ }
+
+ private fun getFullscreenTrans(endTranslation: Float) =
+ if (fullscreenProgress > 0) endTranslation else 0f
+
+ private fun getGridTrans(endTranslation: Float) = if (gridProgress > 0) endTranslation else 0f
+
+ companion object {
+ private enum class Alpha {
+ SCROLL,
+ CONTENT,
+ VISIBILITY,
+ DISMISS,
+ }
+
+ @JvmField
+ val VISIBILITY_ALPHA: FloatProperty =
+ KFloatProperty(ClearAllButton::visibilityAlpha)
+
+ @JvmField
+ val DISMISS_ALPHA: FloatProperty =
+ KFloatProperty(ClearAllButton::dismissAlpha)
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
new file mode 100644
index 00000000000..ef044f4fda6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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.views
+
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+
+class DesktopTaskContentView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
+ var cornerRadius: Float = 0f
+ set(value) {
+ field = value
+ invalidateOutline()
+ }
+
+ private val bounds = Rect()
+
+ init {
+ clipToOutline = true
+ outlineProvider =
+ object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setRoundRect(bounds, cornerRadius)
+ }
+ }
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ bounds.set(0, 0, w, h)
+ invalidateOutline()
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index c56d7db5c61..bbd06be365f 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -15,271 +15,517 @@
*/
package com.android.quickstep.views
+import android.annotation.SuppressLint
import android.content.Context
-import android.graphics.Point
+import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
-import android.graphics.drawable.LayerDrawable
-import android.graphics.drawable.ShapeDrawable
-import android.graphics.drawable.shapes.RoundRectShape
+import android.graphics.Rect.intersects
+import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
+import android.util.Size
+import android.view.Display.INVALID_DISPLAY
+import android.view.Gravity
import android.view.View
-import android.view.ViewGroup
+import android.view.ViewStub
+import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
-import app.lawnchair.theme.color.tokens.ColorTokens
+import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix
+import com.android.launcher3.Flags.enableDesktopExplodedView
+import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.testing.TestLogging
+import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.Utilities
import com.android.launcher3.util.RunnableList
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.launcher3.util.ViewPool
+import com.android.launcher3.util.rects.lerpRect
import com.android.launcher3.util.rects.set
import com.android.quickstep.BaseContainerInterface
+import com.android.quickstep.DesktopFullscreenDrawParams
+import com.android.quickstep.FullscreenDrawParams
+import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle
import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.ViewUtils
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
+import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
+import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel
+import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.RecentsOrientedState
-import com.android.systemui.shared.recents.model.Task
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops
+import kotlin.math.roundToInt
+
+import app.lawnchair.theme.color.tokens.ColorTokens
/** TaskView that contains all tasks that are part of the desktop. */
class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
- TaskView(context, attrs) {
+ TaskView(
+ context,
+ attrs,
+ type = TaskViewType.DESKTOP,
+ thumbnailFullscreenParams = DesktopFullscreenDrawParams(context),
+ ) {
+ val deskId
+ get() = desktopTask?.deskId ?: DesktopVisibilityController.INACTIVE_DESK_ID
+
+ private var desktopTask: DesktopTask? = null
+
+ private val contentViewFullscreenParams = FullscreenDrawParams(context)
+
+ private val taskThumbnailViewDeprecatedPool =
+ if (!enableRefactorTaskThumbnail()) {
+ ViewPool(
+ context,
+ this,
+ R.layout.task_thumbnail_deprecated,
+ VIEW_POOL_MAX_SIZE,
+ VIEW_POOL_INITIAL_SIZE,
+ )
+ } else null
- private val snapshotDrawParams =
- object : FullscreenDrawParams(context) {
- // DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
- override fun computeTaskCornerRadius(context: Context) =
- computeWindowCornerRadius(context)
- }
private val taskThumbnailViewPool =
- ViewPool(
- context,
- this,
- R.layout.task_thumbnail,
- VIEW_POOL_MAX_SIZE,
- VIEW_POOL_INITIAL_SIZE
- )
+ if (enableRefactorTaskThumbnail()) {
+ ViewPool(
+ context,
+ this,
+ R.layout.task_thumbnail,
+ VIEW_POOL_MAX_SIZE,
+ VIEW_POOL_INITIAL_SIZE,
+ )
+ } else null
+
private val tempPointF = PointF()
- private val tempRect = Rect()
- private lateinit var backgroundView: View
+ private val lastComputedTaskSize = Rect()
private lateinit var iconView: TaskViewIcon
- private var childCountAtInflation = 0
+ private lateinit var contentView: DesktopTaskContentView
+ private lateinit var backgroundView: View
+
+ private var viewModel: DesktopTaskViewModel? = null
+
+ /**
+ * Holds the default (user placed) positions of task windows. This can be moved into the
+ * viewModel once RefactorTaskThumbnail has been launched.
+ */
+ private var fullscreenTaskPositions: List = emptyList()
+
+ /**
+ * When enableDesktopExplodedView is enabled, this controls the gradual transition from the
+ * default positions to the organized non-overlapping positions.
+ */
+ var explodeProgress = 0.0f
+ set(value) {
+ field = value
+ positionTaskWindows()
+ }
+
+ var remoteTargetHandles: Array? = null
+ set(value) {
+ field = value
+ positionTaskWindows()
+ }
+
+ override val displayId: Int
+ get() =
+ if (enableMultipleDesktops(context)) {
+ desktopTask?.displayId ?: INVALID_DISPLAY
+ } else {
+ super.displayId
+ }
+
+ private fun getRemoteTargetHandle(taskId: Int): RemoteTargetHandle? =
+ remoteTargetHandles?.firstOrNull {
+ it.transformParams.targetSet.firstAppTargetTaskId == taskId
+ }
override fun onFinishInflate() {
super.onFinishInflate()
- backgroundView =
- findViewById(R.id.background)!!.apply {
+ iconView =
+ (findViewById(R.id.icon) as TaskViewIcon).apply {
+ setIcon(
+ this,
+ ResourcesCompat.getDrawable(
+ context.resources,
+ R.drawable.ic_desktop_with_bg,
+ context.theme,
+ ),
+ )
+ setText(resources.getText(R.string.recent_task_desktop))
+ }
+ contentView =
+ findViewById(R.id.desktop_content).apply {
updateLayoutParams {
topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
}
- background =
- ShapeDrawable(RoundRectShape(FloatArray(8) { taskCornerRadius }, null, null))
- .apply {
- setTint(
- ColorTokens.Neutral2_300.resolveColor(context)
- )
- }
- }
- iconView =
- getOrInflateIconView(R.id.icon).apply {
- val iconBackground = resources.getDrawable(R.drawable.bg_circle, context.theme)
- val icon = resources.getDrawable(R.drawable.ic_desktop, context.theme)
- setIcon(this, LayerDrawable(arrayOf(iconBackground, icon)))
+ cornerRadius = contentViewFullscreenParams.currentCornerRadius
+ backgroundView = findViewById(R.id.background)
+ backgroundView.setBackgroundColor(
+ resources.getColor(ColorTokens.Neutral2_300.resolveColor(context), context.theme)
+ )
}
- childCountAtInflation = childCount
}
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
- val containerWidth = MeasureSpec.getSize(widthMeasureSpec)
- var containerHeight = MeasureSpec.getSize(heightMeasureSpec)
- setMeasuredDimension(containerWidth, containerHeight)
+ override fun inflateViewStubs() {
+ findViewById(R.id.icon)
+ ?.apply {
+ layoutResource =
+ if (enableOverviewIconMenu()) R.layout.icon_app_chip_view
+ else R.layout.icon_view
+ }
+ ?.inflate()
+ }
+ private fun positionTaskWindows() {
if (taskContainers.isEmpty()) {
return
}
val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
- containerHeight -= thumbnailTopMarginPx
- BaseContainerInterface.getTaskDimension(context, container.deviceProfile, tempPointF)
- val windowWidth = tempPointF.x.toInt()
- val windowHeight = tempPointF.y.toInt()
- val scaleWidth = containerWidth / windowWidth.toFloat()
- val scaleHeight = containerHeight / windowHeight.toFloat()
- if (DEBUG) {
- Log.d(
- TAG,
- "onMeasure: container=[$containerWidth,$containerHeight] " +
- "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]"
- )
- }
+ val taskViewWidth = layoutParams.width
+ val taskViewHeight = layoutParams.height - thumbnailTopMarginPx
+
+ BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+
+ val screenWidth = tempPointF.x.toInt()
+ val screenHeight = tempPointF.y.toInt()
+ val screenRect = Rect(0, 0, screenWidth, screenHeight)
+ val scaleWidth = taskViewWidth / screenWidth.toFloat()
+ val scaleHeight = taskViewHeight / screenHeight.toFloat()
- // Desktop tile is a shrunk down version of launcher and freeform task thumbnails.
taskContainers.forEach {
- // Default to quarter of the desktop if we did not get app bounds.
- val taskSize =
- it.task.appBounds
- ?: tempRect.apply {
- left = 0
- top = 0
- right = windowWidth / 4
- bottom = windowHeight / 4
- }
- val thumbWidth = (taskSize.width() * scaleWidth).toInt()
- val thumbHeight = (taskSize.height() * scaleHeight).toInt()
- it.thumbnailViewDeprecated.measure(
- MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY)
- )
+ val taskId = it.task.key.id
+ val fullscreenTaskPosition =
+ fullscreenTaskPositions.firstOrNull { it.taskId == taskId } ?: return
+ val overviewTaskPosition =
+ if (enableDesktopExplodedView()) {
+ viewModel!!
+ .organizedDesktopTaskPositions
+ .firstOrNull { it.taskId == taskId }
+ ?.let { organizedPosition ->
+ TEMP_OVERVIEW_TASK_POSITION.apply {
+ lerpRect(
+ fullscreenTaskPosition.bounds,
+ organizedPosition.bounds,
+ explodeProgress,
+ )
+ }
+ } ?: fullscreenTaskPosition.bounds
+ } else {
+ fullscreenTaskPosition.bounds
+ }
- // Position the task to the same position as it would be on the desktop
- val positionInParent = it.task.positionInParent ?: ORIGIN
- val taskX = (positionInParent.x * scaleWidth).toInt()
- var taskY = (positionInParent.y * scaleHeight).toInt()
- // move task down by margin size
- taskY += thumbnailTopMarginPx
- it.thumbnailViewDeprecated.x = taskX.toFloat()
- it.thumbnailViewDeprecated.y = taskY.toFloat()
- if (DEBUG) {
- Log.d(
- TAG,
- "onMeasure: task=${it.task.key} thumb=[$thumbWidth,$thumbHeight]" +
- " pos=[$taskX,$taskY]"
- )
+ if (enableDesktopExplodedView()) {
+ getRemoteTargetHandle(taskId)?.let { remoteTargetHandle ->
+ val fromRect =
+ TEMP_FROM_RECTF.apply {
+ set(fullscreenTaskPosition.bounds)
+ scale(scaleWidth)
+ offset(
+ lastComputedTaskSize.left.toFloat(),
+ lastComputedTaskSize.top.toFloat(),
+ )
+ }
+ val toRect =
+ TEMP_TO_RECTF.apply {
+ set(overviewTaskPosition)
+ scale(scaleWidth)
+ offset(
+ lastComputedTaskSize.left.toFloat(),
+ lastComputedTaskSize.top.toFloat(),
+ )
+ }
+ val transform = Matrix()
+ transform.setRectToRect(fromRect, toRect, Matrix.ScaleToFit.FILL)
+ remoteTargetHandle.taskViewSimulator.setTaskRectTransform(transform)
+ remoteTargetHandle.taskViewSimulator.apply(remoteTargetHandle.transformParams)
+ }
}
- }
- }
- override fun onRecycle() {
- super.onRecycle()
- visibility = VISIBLE
+ val taskLeft = overviewTaskPosition.left * scaleWidth
+ val taskTop = overviewTaskPosition.top * scaleHeight
+ val taskWidth = overviewTaskPosition.width() * scaleWidth
+ val taskHeight = overviewTaskPosition.height() * scaleHeight
+ // TODO(b/394660950): Revisit the choice to update the layout when explodeProgress == 1.
+ // To run the explode animation in reverse, it may be simpler to use translation/scale
+ // for all cases where the progress is non-zero.
+ if (explodeProgress == 0.0f || explodeProgress == 1.0f) {
+ // Reset scaling and translation that may have been applied during animation.
+ it.snapshotView.apply {
+ scaleX = 1.0f
+ scaleY = 1.0f
+ translationX = 0.0f
+ translationY = 0.0f
+ }
+
+ // Position the task to the same position as it would be on the desktop
+ it.snapshotView.updateLayoutParams {
+ gravity = Gravity.LEFT or Gravity.TOP
+ width = taskWidth.toInt()
+ height = taskHeight.toInt()
+ leftMargin = taskLeft.toInt()
+ topMargin = taskTop.toInt()
+ }
+
+ if (
+ enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail()
+ ) {
+ it.thumbnailView.outlineBounds =
+ if (intersects(overviewTaskPosition, screenRect))
+ Rect(overviewTaskPosition).apply {
+ intersectUnchecked(screenRect)
+ // Offset to 0,0 to transform into TaskThumbnailView's coordinate
+ // system.
+ offset(-overviewTaskPosition.left, -overviewTaskPosition.top)
+ left = (left * scaleWidth).roundToInt()
+ top = (top * scaleHeight).roundToInt()
+ right = (right * scaleWidth).roundToInt()
+ bottom = (bottom * scaleHeight).roundToInt()
+ }
+ else null
+ }
+ } else {
+ // During the animation, apply translation and scale such that the view is
+ // transformed to where we want, without triggering layout.
+ it.snapshotView.apply {
+ pivotX = 0.0f
+ pivotY = 0.0f
+ translationX = taskLeft - left
+ translationY = taskTop - top
+ scaleX = taskWidth / width.toFloat()
+ scaleY = taskHeight / height.toFloat()
+ }
+ }
+ }
}
/** Updates this desktop task to the gives task list defined in `tasks` */
fun bind(
- tasks: List,
+ desktopTask: DesktopTask,
orientedState: RecentsOrientedState,
- taskOverlayFactory: TaskOverlayFactory
+ taskOverlayFactory: TaskOverlayFactory,
) {
+ this.desktopTask = desktopTask
+ // TODO(b/370495260): Minimized tasks should not be filtered with desktop exploded view
+ // support.
+ // Minimized tasks should not be shown in Overview.
+ val tasks = desktopTask.tasks.filterNot { it.isMinimized }
if (DEBUG) {
val sb = StringBuilder()
sb.append("bind tasks=").append(tasks.size).append("\n")
tasks.forEach { sb.append(" key=${it.key}\n") }
Log.d(TAG, sb.toString())
}
+
cancelPendingLoadTasks()
+ val backgroundViewIndex = contentView.indexOfChild(backgroundView)
+ taskContainers =
+ tasks.map { task ->
+ val snapshotView =
+ if (enableRefactorTaskThumbnail()) {
+ taskThumbnailViewPool!!.view
+ } else {
+ taskThumbnailViewDeprecatedPool!!.view
+ }
+ contentView.addView(snapshotView, backgroundViewIndex + 1)
- if (!isTaskContainersInitialized()) {
- taskContainers = arrayListOf()
- }
- val taskContainers = taskContainers as ArrayList
- taskContainers.ensureCapacity(tasks.size)
- tasks.forEachIndexed { index, task ->
- val thumbnailViewDeprecated: TaskThumbnailViewDeprecated
- if (index >= taskContainers.size) {
- thumbnailViewDeprecated = taskThumbnailViewPool.view
- // Add thumbnailView from to position after the initial child views.
- addView(
- thumbnailViewDeprecated,
- childCountAtInflation,
- LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- )
- } else {
- thumbnailViewDeprecated = taskContainers[index].thumbnailViewDeprecated
- }
- val taskContainer =
TaskContainer(
- task,
- // TODO(b/338360089): Support new TTV for DesktopTaskView
- thumbnailView = null,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
- digitalWellBeingToast = null,
- showWindowsView = null,
- taskOverlayFactory
- )
- .apply { thumbnailViewDeprecated.bind(task, overlay) }
- if (index >= taskContainers.size) {
- taskContainers.add(taskContainer)
- } else {
- taskContainers[index] = taskContainer
+ this,
+ task,
+ snapshotView,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+ digitalWellBeingToast = null,
+ showWindowsView = null,
+ taskOverlayFactory,
+ )
}
+ onBind(orientedState)
+ }
+
+ override fun onBind(orientedState: RecentsOrientedState) {
+ super.onBind(orientedState)
+
+ if (enableRefactorTaskThumbnail()) {
+ viewModel =
+ DesktopTaskViewModel(organizeDesktopTasksUseCase = RecentsDependencies.get(context))
}
- repeat(taskContainers.size - tasks.size) {
- if (Utilities.ATLEAST_T) {
- with(taskContainers.removeLast()) {
- removeView(thumbnailViewDeprecated)
- taskThumbnailViewPool.recycle(thumbnailViewDeprecated)
- }
- } else {
- taskContainers.removeAt(taskContainers.lastIndex)
- }
+ }
+
+ override fun onRecycle() {
+ super.onRecycle()
+ desktopTask = null
+ explodeProgress = 0.0f
+ viewModel = null
+ visibility = VISIBLE
+ taskContainers.forEach { removeAndRecycleThumbnailView(it) }
+ }
+
+ override fun setOrientationState(orientationState: RecentsOrientedState) {
+ super.setOrientationState(orientationState)
+ iconView.setIconOrientation(orientationState, isGridTask)
+ }
+
+ @SuppressLint("RtlHardcoded")
+ override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) {
+ super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize)
+ this.lastComputedTaskSize.set(lastComputedTaskSize)
+
+ updateTaskPositions()
+ }
+
+ override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) {
+ super.onTaskListVisibilityChanged(visible, changes)
+ if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) {
+ contentViewFullscreenParams.updateCornerRadius(context)
}
+ }
- setOrientationState(orientedState)
+ override fun onIconLoaded(taskContainer: TaskContainer) {
+ // Update contentDescription of snapshotView only, individual task icon is unused.
+ taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
}
- override fun needsUpdate(dataChange: Int, flag: Int) =
- if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false
+ override fun setIconState(container: TaskContainer, state: TaskData?) {
+ container.snapshotView.contentDescription = (state as? TaskData.Data)?.titleDescription
+ }
+
+ // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
+ override fun onIconUnloaded(taskContainer: TaskContainer) {}
// thumbnailView is laid out differently and is handled in onMeasure
override fun updateThumbnailSize() {}
override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) {
if (relativeToDragLayer) {
- container.dragLayer.getDescendantRectRelativeToSelf(backgroundView, bounds)
+ container.dragLayer.getDescendantRectRelativeToSelf(contentView, bounds)
} else {
- bounds.set(backgroundView)
+ bounds.set(contentView)
}
}
- override fun launchTaskAnimated(): RunnableList? {
+ private fun launchTaskWithDesktopController(animated: Boolean): RunnableList? {
val recentsView = recentsView ?: return null
+ TestLogging.recordEvent(
+ TestProtocol.SEQUENCE_MAIN,
+ "launchDesktopFromRecents",
+ taskIds.contentToString(),
+ )
val endCallback = RunnableList()
val desktopController = recentsView.desktopRecentsController
checkNotNull(desktopController) { "recentsController is null" }
- desktopController.launchDesktopFromRecents(this) { endCallback.executeAllAndDestroy() }
- Log.d(TAG, "launchTaskAnimated - launchDesktopFromRecents: ${taskIds.contentToString()}")
+ desktopController.launchDesktopFromRecents(this, animated) {
+ endCallback.executeAllAndDestroy()
+ }
+ Log.d(
+ TAG,
+ "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated",
+ )
// Callbacks get run from recentsView for case when recents animation already running
recentsView.addSideTaskLaunchCallback(endCallback)
return endCallback
}
- override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
- launchTasks()
- callback(true)
- }
+ override fun launchAsStaticTile() = launchTaskWithDesktopController(animated = true)
- // Desktop tile can't be in split screen
- override fun confirmSecondSplitSelectApp(): Boolean = false
+ override fun launchWithoutAnimation(
+ isQuickSwitch: Boolean,
+ callback: (launched: Boolean) -> Unit,
+ ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false)
+
+ // Return true when Task cannot be launched as fullscreen (i.e. in split select state) to skip
+ // putting DesktopTaskView to split as it's not supported.
+ override fun confirmSecondSplitSelectApp(): Boolean =
+ recentsView?.canLaunchFullscreenTask() != true
// TODO(b/330685808) support overlay for Screenshot action
override fun setOverlayEnabled(overlayEnabled: Boolean) {}
override fun onFullscreenProgressChanged(fullscreenProgress: Float) {
- // Don't show background while we are transitioning to/from fullscreen
- backgroundView.visibility = if (fullscreenProgress > 0) INVISIBLE else VISIBLE
+ backgroundView.alpha = 1 - fullscreenProgress
+ }
+
+ override fun updateFullscreenParams() {
+ super.updateFullscreenParams()
+ updateFullscreenParams(contentViewFullscreenParams)
+ contentView.cornerRadius = contentViewFullscreenParams.currentCornerRadius
+ }
+
+ override fun addChildrenForAccessibility(outChildren: ArrayList) {
+ super.addChildrenForAccessibility(outChildren)
+ ViewUtils.addAccessibleChildToList(backgroundView, outChildren)
+ }
+
+ fun removeTaskFromExplodedView(taskId: Int, animate: Boolean) {
+ if (!enableDesktopExplodedView()) {
+ Log.e(
+ TAG,
+ "removeTaskFromExplodedView called when enableDesktopExplodedView flag is false",
+ )
+ return
+ }
+
+ // Remove the task's [taskContainer] and its associated Views.
+ val taskContainer = getTaskContainerById(taskId) ?: return
+ removeAndRecycleThumbnailView(taskContainer)
+ taskContainer.destroy()
+ taskContainers = taskContainers.filterNot { it == taskContainer }
+
+ // Dismiss the current DesktopTaskView if all its windows are closed.
+ if (taskContainers.isEmpty()) {
+ recentsView?.dismissTaskView(this, animate, /* removeTask= */ true)
+ } else {
+ // Otherwise, re-position the remaining task windows.
+ // TODO(b/353949276): Implement the re-layout animations.
+ updateTaskPositions()
+ }
}
- override fun updateCurrentFullscreenParams() {
- super.updateCurrentFullscreenParams()
- updateFullscreenParams(snapshotDrawParams)
+ private fun removeAndRecycleThumbnailView(taskContainer: TaskContainer) {
+ contentView.removeView(taskContainer.snapshotView)
+ if (enableRefactorTaskThumbnail()) {
+ taskThumbnailViewPool!!.recycle(taskContainer.thumbnailView)
+ } else {
+ taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated)
+ }
}
- override fun getThumbnailFullscreenParams() = snapshotDrawParams
+ private fun updateTaskPositions() {
+ BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+ val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt())
+ DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4)
+
+ fullscreenTaskPositions =
+ taskContainers.map {
+ DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS)
+ }
+
+ if (enableDesktopExplodedView()) {
+ viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions)
+ }
+ positionTaskWindows()
+ }
companion object {
private const val TAG = "DesktopTaskView"
private const val DEBUG = false
- private const val VIEW_POOL_MAX_SIZE = 10
+ private const val VIEW_POOL_MAX_SIZE = 5
+
// As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool.
private const val VIEW_POOL_INITIAL_SIZE = 0
- private val ORIGIN = Point(0, 0)
+ private val DEFAULT_BOUNDS = Rect()
+ // Temporaries used for various purposes to avoid allocations.
+ private val TEMP_OVERVIEW_TASK_POSITION = Rect()
+ private val TEMP_FROM_RECTF = RectF()
+ private val TEMP_TO_RECTF = RectF()
}
}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
deleted file mode 100644
index a9236197dfc..00000000000
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ /dev/null
@@ -1,443 +0,0 @@
-/*
- * Copyright (C) 2018 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.views;
-
-import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
-
-import static com.android.launcher3.Utilities.prefixTextWithIcon;
-import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR;
-
-import android.app.ActivityOptions;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.AppUsageLimit;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.icu.text.MeasureFormat;
-import android.icu.text.MeasureFormat.FormatWidth;
-import android.icu.util.Measure;
-import android.icu.util.MeasureUnit;
-import android.os.UserHandle;
-import android.util.Log;
-import android.util.Pair;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.time.Duration;
-import java.util.Locale;
-
-public final class DigitalWellBeingToast {
-
- private static final float THRESHOLD_LEFT_ICON_ONLY = 0.4f;
- private static final float THRESHOLD_RIGHT_ICON_ONLY = 0.6f;
-
- /** Will span entire width of taskView with full text */
- private static final int SPLIT_BANNER_FULLSCREEN = 0;
- /** Used for grid task view, only showing icon and time */
- private static final int SPLIT_GRID_BANNER_LARGE = 1;
- /** Used for grid task view, only showing icon */
- private static final int SPLIT_GRID_BANNER_SMALL = 2;
-
- @IntDef(value = {
- SPLIT_BANNER_FULLSCREEN,
- SPLIT_GRID_BANNER_LARGE,
- SPLIT_GRID_BANNER_SMALL,
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface SplitBannerConfig {
- }
-
- static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
- static final int MINUTE_MS = 60000;
-
- private static final String TAG = "DigitalWellBeingToast";
-
- private final RecentsViewContainer mContainer;
- private final TaskView mTaskView;
- private final LauncherApps mLauncherApps;
-
- private final int mBannerHeight;
-
- private Task mTask;
- private boolean mHasLimit;
-
- private long mAppRemainingTimeMs;
- @Nullable
- private View mBanner;
- private ViewOutlineProvider mOldBannerOutlineProvider;
- private float mBannerOffsetPercentage;
- @Nullable
- private SplitBounds mSplitBounds;
- private float mSplitOffsetTranslationY;
- private float mSplitOffsetTranslationX;
-
- private boolean mIsDestroyed = false;
-
- public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) {
- mContainer = container;
- mTaskView = taskView;
- mLauncherApps = container.asContext().getSystemService(LauncherApps.class);
- mBannerHeight = container.asContext().getResources().getDimensionPixelSize(
- R.dimen.digital_wellbeing_toast_height);
- }
-
- private void setNoLimit() {
- mHasLimit = false;
- mTaskView.setContentDescription(mTask.titleDescription);
- replaceBanner(null);
- mAppRemainingTimeMs = -1;
- }
-
- private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
- mAppRemainingTimeMs = appRemainingTimeMs;
- mHasLimit = true;
- TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast,
- mContainer.asContext(), mTaskView);
- toast.setText(prefixTextWithIcon(mContainer.asContext(), R.drawable.ic_hourglass_top,
- getText()));
- toast.setOnClickListener(this::openAppUsageSettings);
- replaceBanner(toast);
-
- mTaskView.setContentDescription(
- getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
- }
-
- public String getText() {
- return getText(mAppRemainingTimeMs, false /* forContentDesc */);
- }
-
- public boolean hasLimit() {
- return mHasLimit;
- }
-
- public void initialize(Task task) {
- if (mIsDestroyed) {
- throw new IllegalStateException("Cannot re-initialize a destroyed toast");
- }
- mTask = task;
- ORDERED_BG_EXECUTOR.execute(() -> {
- AppUsageLimit usageLimit = null;
- try {
- usageLimit = mLauncherApps.getAppUsageLimit(
- mTask.getTopComponent().getPackageName(),
- UserHandle.of(mTask.key.userId));
- } catch (Exception e) {
- Log.e(TAG, "Error initializing digital well being toast", e);
- }
- final long appUsageLimitTimeMs = usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
- final long appRemainingTimeMs = usageLimit != null ? usageLimit.getUsageRemaining() : -1;
-
- mTaskView.post(() -> {
- if (mIsDestroyed) {
- return;
- }
- if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
- setNoLimit();
- } else {
- setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
- }
- });
- });
- }
-
- /**
- * Mark the DWB toast as destroyed and remove banner from TaskView.
- */
- public void destroy() {
- mIsDestroyed = true;
- mTaskView.post(() -> replaceBanner(null));
- }
-
- public void setSplitBounds(@Nullable SplitBounds splitBounds) {
- mSplitBounds = splitBounds;
- }
-
- private @SplitBannerConfig int getSplitBannerConfig() {
- if (mSplitBounds == null
- || !mContainer.getDeviceProfile().isTablet
- || mTaskView.isFocusedTask()) {
- return SPLIT_BANNER_FULLSCREEN;
- }
-
- // For portrait grid only height of task changes, not width. So we keep the text
- // the same
- if (!mContainer.getDeviceProfile().isLeftRightSplit) {
- return SPLIT_GRID_BANNER_LARGE;
- }
-
- // For landscape grid, for 30% width we only show icon, otherwise show icon and
- // time
- if (mTask.key.id == mSplitBounds.leftTopTaskId) {
- return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY
- ? SPLIT_GRID_BANNER_SMALL
- : SPLIT_GRID_BANNER_LARGE;
- } else {
- return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY
- ? SPLIT_GRID_BANNER_SMALL
- : SPLIT_GRID_BANNER_LARGE;
- }
- }
-
- private String getReadableDuration(
- Duration duration,
- @StringRes int durationLessThanOneMinuteStringId) {
- int hours = Math.toIntExact(duration.toHours());
- int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
-
- // Apply FormatWidth.WIDE if both the hour part and the minute part are
- // non-zero.
- if (hours > 0 && minutes > 0) {
- return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW)
- .formatMeasures(
- new Measure(hours, MeasureUnit.HOUR),
- new Measure(minutes, MeasureUnit.MINUTE));
- }
-
- // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
- if (hours > 0) {
- return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
- new Measure(hours, MeasureUnit.HOUR));
- }
-
- // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
- if (minutes > 0) {
- return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
- new Measure(minutes, MeasureUnit.MINUTE));
- }
-
- // Use a specific string for usage less than one minute but non-zero.
- if (duration.compareTo(Duration.ZERO) > 0) {
- return mContainer.asContext().getString(durationLessThanOneMinuteStringId);
- }
-
- // Otherwise, return 0-minute string.
- return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
- new Measure(0, MeasureUnit.MINUTE));
- }
-
- /**
- * Returns text to show for the banner depending on
- * {@link #getSplitBannerConfig()}
- * If {@param forContentDesc} is {@code true}, this will always return the full
- * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN}
- */
- private String getText(long remainingTime, boolean forContentDesc) {
- final Duration duration = Duration.ofMillis(
- remainingTime > MINUTE_MS ? (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS : remainingTime);
- String readableDuration = getReadableDuration(duration,
- R.string.shorter_duration_less_than_one_minute
- /* forceFormatWidth */);
- @SplitBannerConfig
- int splitBannerConfig = getSplitBannerConfig();
- if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
- return mContainer.asContext().getString(
- R.string.time_left_for_app,
- readableDuration);
- }
-
- if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
- // show no text
- return "";
- } else { // SPLIT_GRID_BANNER_LARGE
- // only show time
- return readableDuration;
- }
- }
-
- public void openAppUsageSettings(View view) {
- final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
- .putExtra(Intent.EXTRA_PACKAGE_NAME,
- mTask.getTopComponent().getPackageName())
- .addFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- try {
- final RecentsViewContainer container = RecentsViewContainer.containerFromContext(view.getContext());
- final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
- view, 0, 0,
- view.getWidth(), view.getHeight());
- container.asContext().startActivity(intent, options.toBundle());
-
- // TODO: add WW logging on the app usage settings click.
- } catch (ActivityNotFoundException e) {
- Log.e(TAG, "Failed to open app usage settings for task "
- + mTask.getTopComponent().getPackageName(), e);
- }
- }
-
- private String getContentDescriptionForTask(
- Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
- return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ? mContainer.asContext().getString(
- R.string.task_contents_description_with_remaining_time,
- task.titleDescription,
- getText(appRemainingTimeMs, true /* forContentDesc */)) : task.titleDescription;
- }
-
- private void replaceBanner(@Nullable View view) {
- resetOldBanner();
- setBanner(view);
- }
-
- private void resetOldBanner() {
- if (mBanner != null) {
- mBanner.setOutlineProvider(mOldBannerOutlineProvider);
- mTaskView.removeView(mBanner);
- mBanner.setOnClickListener(null);
- mContainer.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner);
- }
- }
-
- private void setBanner(@Nullable View view) {
- mBanner = view;
- if (mBanner != null && mTaskView.getRecentsView() != null) {
- setupAndAddBanner();
- setBannerOutline();
- }
- }
-
- private void setupAndAddBanner() {
- FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mBanner.getLayoutParams();
- DeviceProfile deviceProfile = mContainer.getDeviceProfile();
- layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams) mTaskView.getFirstThumbnailViewDeprecated()
- .getLayoutParams()).bottomMargin;
- RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
- Pair translations = orientationHandler
- .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
- mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
- mTaskView.getThumbnailViews(), mTask.key.id, mBanner);
- mSplitOffsetTranslationX = translations.first;
- mSplitOffsetTranslationY = translations.second;
- updateTranslationY();
- updateTranslationX();
- mTaskView.addView(mBanner);
- }
-
- private void setBannerOutline() {
- // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
- mOldBannerOutlineProvider = mBanner.getOutlineProvider() != null
- ? mBanner.getOutlineProvider()
- : ViewOutlineProvider.BACKGROUND;
-
- mBanner.setOutlineProvider(new ViewOutlineProvider() {
- @Override
- public void getOutline(View view, Outline outline) {
- mOldBannerOutlineProvider.getOutline(view, outline);
- float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY;
- outline.offset(0, Math.round(verticalTranslation));
- }
- });
- mBanner.setClipToOutline(true);
- }
-
- void updateBannerOffset(float offsetPercentage) {
- if (mBannerOffsetPercentage != offsetPercentage) {
- mBannerOffsetPercentage = offsetPercentage;
- if (mBanner != null) {
- updateTranslationY();
- mBanner.invalidateOutline();
- }
- }
- }
-
- private void updateTranslationY() {
- if (mBanner == null) {
- return;
- }
-
- mBanner.setTranslationY(
- (mBannerOffsetPercentage * mBannerHeight) + mSplitOffsetTranslationY);
- }
-
- private void updateTranslationX() {
- if (mBanner == null) {
- return;
- }
-
- mBanner.setTranslationX(mSplitOffsetTranslationX);
- }
-
- void setBannerColorTint(int color, float amount) {
- if (mBanner == null) {
- return;
- }
- if (amount == 0) {
- mBanner.setLayerType(View.LAYER_TYPE_NONE, null);
- }
- Paint layerPaint = new Paint();
- layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
- mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint);
- mBanner.setLayerPaint(layerPaint);
- }
-
- void setBannerVisibility(int visibility) {
- if (mBanner == null) {
- return;
- }
-
- mBanner.setVisibility(visibility);
- }
-
- private int getAccessibilityActionId() {
- return (mSplitBounds != null
- && mSplitBounds.rightBottomTaskId == mTask.key.id)
- ? R.id.action_digital_wellbeing_bottom_right
- : R.id.action_digital_wellbeing_top_left;
- }
-
- @Nullable
- public AccessibilityNodeInfo.AccessibilityAction getDWBAccessibilityAction() {
- if (!hasLimit()) {
- return null;
- }
-
- Context context = mContainer.asContext();
- String label = (mTaskView.containsMultipleTasks())
- ? context.getString(
- R.string.split_app_usage_settings,
- TaskUtils.getTitle(context, mTask))
- : context.getString(R.string.accessibility_app_usage_settings);
- return new AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label);
- }
-
- public boolean handleAccessibilityAction(int action) {
- if (getAccessibilityActionId() == action) {
- openAppUsageSettings(mTaskView);
- return true;
- } else {
- return false;
- }
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
new file mode 100644
index 00000000000..5c4a35da633
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2018 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.views
+
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.AppUsageLimit
+import android.graphics.Outline
+import android.graphics.Paint
+import android.icu.text.MeasureFormat
+import android.icu.util.Measure
+import android.icu.util.MeasureUnit
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.AttributeSet
+import android.util.Log
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.component1
+import androidx.core.util.component2
+import androidx.core.view.isVisible
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
+import com.android.quickstep.TaskUtils
+import com.android.systemui.shared.recents.model.Task
+import java.time.Duration
+import java.util.Locale
+
+@SuppressLint("AppCompatCustomView")
+class DigitalWellBeingToast
+@JvmOverloads
+constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+) : TextView(context, attrs, defStyleAttr, defStyleRes) {
+ private val recentsViewContainer: RecentsViewContainer =
+ RecentsViewContainer.containerFromContext(context)
+
+ private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java)
+
+ private val bannerHeight =
+ context.resources.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
+
+ private lateinit var task: Task
+ private lateinit var taskView: TaskView
+ private lateinit var snapshotView: View
+ @StagePosition private var stagePosition = STAGE_POSITION_UNDEFINED
+
+ private var appRemainingTimeMs: Long = 0
+ private var splitOffsetTranslationY = 0f
+ set(value) {
+ if (field != value) {
+ field = value
+ updateTranslationY()
+ }
+ }
+
+ private var isDestroyed = false
+
+ var hasLimit = false
+ var splitBounds: SplitConfigurationOptions.SplitBounds? = null
+ var bannerOffsetPercentage = 0f
+ set(value) {
+ if (field != value) {
+ field = value
+ updateTranslationY()
+ }
+ }
+
+ init {
+ setOnClickListener(::openAppUsageSettings)
+ outlineProvider =
+ object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ BACKGROUND.getOutline(view, outline)
+ val verticalTranslation = splitOffsetTranslationY - translationY
+ outline.offset(0, Math.round(verticalTranslation))
+ }
+ }
+ clipToOutline = true
+ }
+
+ private fun setNoLimit() {
+ isVisible = false
+ hasLimit = false
+ appRemainingTimeMs = -1
+ setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
+ }
+
+ private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
+ isVisible = true
+ hasLimit = true
+ this.appRemainingTimeMs = appRemainingTimeMs
+ setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs)
+ text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
+ }
+
+ private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
+ val contentDescription =
+ getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
+ snapshotView.contentDescription = contentDescription
+ }
+
+ fun initialize() {
+ check(!isDestroyed) { "Cannot re-initialize a destroyed toast" }
+ setupTranslations()
+ Executors.ORDERED_BG_EXECUTOR.execute {
+ var usageLimit: AppUsageLimit? = null
+ try {
+ usageLimit =
+ launcherApps?.getAppUsageLimit(
+ task.topComponent.packageName,
+ UserHandle.of(task.key.userId),
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Error initializing digital well being toast", e)
+ }
+ val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1
+ val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1
+
+ taskView.post {
+ if (isDestroyed) return@post
+ if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+ setNoLimit()
+ } else {
+ setLimit(appUsageLimitTimeMs, appRemainingTimeMs)
+ }
+ }
+ }
+ }
+
+ /** Bind the DWB toast to its dependencies. */
+ fun bind(
+ task: Task,
+ taskView: TaskView,
+ snapshotView: View,
+ @StagePosition stagePosition: Int,
+ ) {
+ this.task = task
+ this.taskView = taskView
+ this.snapshotView = snapshotView
+ this.stagePosition = stagePosition
+ isDestroyed = false
+ }
+
+ /** Mark the DWB toast as destroyed and hide it. */
+ fun destroy() {
+ isVisible = false
+ isDestroyed = true
+ }
+
+ private fun getSplitBannerConfig(): SplitBannerConfig {
+ val splitBounds = splitBounds
+ return when {
+ splitBounds == null ||
+ !recentsViewContainer.deviceProfile.isTablet ||
+ taskView.isLargeTile -> SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
+ // For portrait grid only height of task changes, not width. So we keep the text the
+ // same
+ !recentsViewContainer.deviceProfile.isLeftRightSplit ->
+ SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+ // For landscape grid, for 30% width we only show icon, otherwise show icon and time
+ task.key.id == splitBounds.leftTopTaskId ->
+ if (splitBounds.leftTopTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
+ SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+ else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+ else ->
+ if (splitBounds.leftTopTaskPercent > THRESHOLD_RIGHT_ICON_ONLY)
+ SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+ else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+ }
+ }
+
+ private fun getReadableDuration(
+ duration: Duration,
+ @StringRes durationLessThanOneMinuteStringId: Int,
+ ): String {
+ val hours = Math.toIntExact(duration.toHours())
+ val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes())
+ return when {
+ // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
+ hours > 0 && minutes > 0 ->
+ MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW)
+ .formatMeasures(
+ Measure(hours, MeasureUnit.HOUR),
+ Measure(minutes, MeasureUnit.MINUTE),
+ )
+ // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
+ hours > 0 ->
+ MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+ .formatMeasures(Measure(hours, MeasureUnit.HOUR))
+ // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
+ minutes > 0 ->
+ MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+ .formatMeasures(Measure(minutes, MeasureUnit.MINUTE))
+ // Use a specific string for usage less than one minute but non-zero.
+ duration > Duration.ZERO -> context.getString(durationLessThanOneMinuteStringId)
+ // Otherwise, return 0-minute string.
+ else ->
+ MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+ .formatMeasures(Measure(0, MeasureUnit.MINUTE))
+ }
+ }
+
+ /**
+ * Returns text to show for the banner depending on [.getSplitBannerConfig] If {@param
+ * forContentDesc} is `true`, this will always return the full string corresponding to
+ * [.SPLIT_BANNER_FULLSCREEN]
+ */
+ @JvmOverloads
+ @VisibleForTesting
+ fun getBannerText(
+ remainingTime: Long = appRemainingTimeMs,
+ forContentDesc: Boolean = false,
+ ): String {
+ val duration =
+ Duration.ofMillis(
+ if (remainingTime > MINUTE_MS)
+ (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS
+ else remainingTime
+ )
+ val readableDuration =
+ getReadableDuration(
+ duration,
+ R.string.shorter_duration_less_than_one_minute, /* forceFormatWidth */
+ )
+ val splitBannerConfig = getSplitBannerConfig()
+ return when {
+ forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN ->
+ context.getString(R.string.time_left_for_app, readableDuration)
+ // show no text
+ splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> ""
+ // SPLIT_GRID_BANNER_LARGE only show time
+ else -> readableDuration
+ }
+ }
+
+ private fun openAppUsageSettings(view: View) {
+ val intent =
+ Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
+ .putExtra(Intent.EXTRA_PACKAGE_NAME, task.topComponent.packageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ try {
+ val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
+ context.startActivity(intent, options.toBundle())
+
+ // TODO: add WW logging on the app usage settings click.
+ } catch (e: ActivityNotFoundException) {
+ Log.e(
+ TAG,
+ "Failed to open app usage settings for task " + task.topComponent.packageName,
+ e,
+ )
+ }
+ }
+
+ private fun getContentDescriptionForTask(
+ task: Task,
+ appUsageLimitTimeMs: Long,
+ appRemainingTimeMs: Long,
+ ): String? =
+ if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0)
+ context.getString(
+ R.string.task_contents_description_with_remaining_time,
+ task.titleDescription,
+ getBannerText(appRemainingTimeMs, true /* forContentDesc */),
+ )
+ else task.titleDescription
+
+ fun setupLayout() {
+ val snapshotWidth: Int
+ val snapshotHeight: Int
+ val splitBounds = splitBounds
+ if (splitBounds == null) {
+ snapshotWidth = taskView.layoutParams.width
+ snapshotHeight =
+ taskView.layoutParams.height -
+ recentsViewContainer.deviceProfile.overviewTaskThumbnailTopMarginPx
+ } else {
+ val groupedTaskSize =
+ taskView.pagedOrientationHandler.getGroupedTaskViewSizes(
+ recentsViewContainer.deviceProfile,
+ splitBounds,
+ taskView.layoutParams.width,
+ taskView.layoutParams.height,
+ )
+ if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+ snapshotWidth = groupedTaskSize.first.x
+ snapshotHeight = groupedTaskSize.first.y
+ } else {
+ snapshotWidth = groupedTaskSize.second.x
+ snapshotHeight = groupedTaskSize.second.y
+ }
+ }
+ taskView.pagedOrientationHandler.updateDwbBannerLayout(
+ taskView.layoutParams.width,
+ taskView.layoutParams.height,
+ taskView is GroupedTaskView,
+ recentsViewContainer.deviceProfile,
+ snapshotWidth,
+ snapshotHeight,
+ this,
+ )
+ }
+
+ private fun setupTranslations() {
+ val (translationX, translationY) =
+ taskView.pagedOrientationHandler.getDwbBannerTranslations(
+ taskView.layoutParams.width,
+ taskView.layoutParams.height,
+ splitBounds,
+ recentsViewContainer.deviceProfile,
+ taskView.snapshotViews,
+ task.key.id,
+ this,
+ )
+ this.translationX = translationX
+ this.splitOffsetTranslationY = translationY
+ }
+
+ private fun updateTranslationY() {
+ translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
+ invalidateOutline()
+ }
+
+ fun setColorTint(color: Int, amount: Float) {
+ if (amount == 0f) {
+ setLayerType(View.LAYER_TYPE_NONE, null)
+ }
+ val layerPaint = Paint()
+ layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount))
+ setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
+ setLayerPaint(layerPaint)
+ }
+
+ private fun getAccessibilityActionId(): Int =
+ if (splitBounds?.rightBottomTaskId == task.key.id)
+ R.id.action_digital_wellbeing_bottom_right
+ else R.id.action_digital_wellbeing_top_left
+
+ fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? {
+ if (!hasLimit) return null
+ val label =
+ if (taskView.containsMultipleTasks())
+ context.getString(
+ R.string.split_app_usage_settings,
+ TaskUtils.getTitle(context, task),
+ )
+ else context.getString(R.string.accessibility_app_usage_settings)
+ return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label)
+ }
+
+ fun handleAccessibilityAction(action: Int): Boolean {
+ if (getAccessibilityActionId() != action) return false
+ openAppUsageSettings(taskView)
+ return true
+ }
+
+ companion object {
+ private const val THRESHOLD_LEFT_ICON_ONLY = 0.4f
+ private const val THRESHOLD_RIGHT_ICON_ONLY = 0.6f
+
+ enum class SplitBannerConfig {
+ /** Will span entire width of taskView with full text */
+ SPLIT_BANNER_FULLSCREEN,
+ /** Used for grid task view, only showing icon and time */
+ SPLIT_GRID_BANNER_LARGE,
+ /** Used for grid task view, only showing icon */
+ SPLIT_GRID_BANNER_SMALL,
+ }
+
+ val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS)
+ const val MINUTE_MS: Int = 60000
+
+ private const val TAG = "DigitalWellBeingToast"
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt
new file mode 100644
index 00000000000..c8930165b58
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 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.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.ViewGroup
+import android.widget.ImageView
+
+/**
+ * An [ImageView] that does not requestLayout() unless setLayoutParams is called.
+ *
+ * This is useful, particularly during animations, for [ImageView]s that are not supposed to be
+ * resized.
+ */
+@SuppressLint("AppCompatCustomView")
+class FixedSizeImageView : ImageView {
+ private var shouldRequestLayoutOnChanges = false
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr)
+
+ override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
+ shouldRequestLayoutOnChanges = true
+ super.setLayoutParams(params)
+ shouldRequestLayoutOnChanges = false
+ }
+
+ override fun requestLayout() {
+ if (shouldRequestLayoutOnChanges) {
+ super.requestLayout()
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index e024995aa30..6bbd6b2c153 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -57,6 +57,7 @@ open class FloatingAppPairBackground(
private val container: RecentsViewContainer
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG)
// Animation interpolators
protected val expandXInterpolator: Interpolator
@@ -105,13 +106,15 @@ open class FloatingAppPairBackground(
)
// Find device-specific measurements
- deviceCornerRadius = QuickStepContract.getWindowCornerRadius(container.asContext())
+ val resources = context.resources
+ deviceCornerRadius = QuickStepContract.getWindowCornerRadius(context)
deviceHalfDividerSize =
- container.asContext().resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
+ resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
val dividerCenterPos = dividerPos + deviceHalfDividerSize
desiredSplitRatio =
if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx
else dividerCenterPos / dp.heightPx
+ dividerPaint.color = resources.getColor(R.color.taskbar_background_dark, null /*theme*/)
}
override fun draw(canvas: Canvas) {
@@ -153,8 +156,12 @@ open class FloatingAppPairBackground(
val leftSide = RectF(0f, 0f, dividerCenterPos - changingDividerSize, height)
// The right half of the background image
val rightSide = RectF(dividerCenterPos + changingDividerSize, 0f, width, height)
+ // Middle part is for divider background
+ val middleRect = RectF(leftSide.right - deviceHalfDividerSize, 0f,
+ rightSide.left + deviceHalfDividerSize, height)
// Draw background
+ canvas.drawRect(middleRect, dividerPaint)
drawCustomRoundedRect(
canvas,
leftSide,
@@ -251,8 +258,12 @@ open class FloatingAppPairBackground(
val topSide = RectF(0f, 0f, width, dividerCenterPos - changingDividerSize)
// The bottom half of the background image
val bottomSide = RectF(0f, dividerCenterPos + changingDividerSize, width, height)
+ // Middle part is for divider background
+ val middleRect = RectF(0f, topSide.bottom - deviceHalfDividerSize,
+ width, bottomSide.top + deviceHalfDividerSize)
// Draw background
+ canvas.drawRect(middleRect, dividerPaint)
drawCustomRoundedRect(
canvas,
topSide,
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
index e5f241fdbeb..b060168b1bc 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
@@ -33,7 +33,6 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.launcher3.widget.RoundedCornerEnforcement;
import java.util.stream.IntStream;
@@ -180,8 +179,7 @@ private static boolean isSupportedDrawable(Drawable drawable) {
/** Corner radius from source view's outline, or enforced view. */
private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) {
- if (RoundedCornerEnforcement.isRoundedCornerEnabled()
- && hostView.hasEnforcedCornerRadius()) {
+ if (hostView.hasEnforcedCornerRadius()) {
return hostView.getEnforcedCornerRadius();
} else if (Utilities.ATLEAST_S
&& v.getOutlineProvider() instanceof RemoteViewOutlineProvider
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
index fc52b8ee939..b719ee5b73a 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
@@ -18,6 +18,7 @@
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.annotation.TargetApi;
+import android.app.TaskInfo;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
@@ -122,10 +123,9 @@ protected void onDetachedFromWindow() {
@Override
public void onGlobalLayout() {
- if (isUninitialized())
- return;
- positionViews();
- if (mOnTargetChangeRunnable != null) {
+ if (isUninitialized()) return;
+ boolean positionsChanged = positionViews();
+ if (mOnTargetChangeRunnable != null && positionsChanged) {
mOnTargetChangeRunnable.run();
}
}
@@ -143,8 +143,7 @@ public void setFastFinishRunnable(Runnable runnable) {
/** Callback at the end or early exit of the animation. */
@Override
public void fastFinish() {
- if (isUninitialized())
- return;
+ if (isUninitialized()) return;
Runnable fastFinishRunnable = mFastFinishRunnable;
if (fastFinishRunnable != null) {
fastFinishRunnable.run();
@@ -189,23 +188,18 @@ private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView,
/**
* Updates the position and opacity of the floating widget's components.
*
- * @param backgroundPosition the new position of the widget's background
- * relative to the
+ * @param backgroundPosition the new position of the widget's background relative to the
* {@link FloatingWidgetView}'s parent
- * @param floatingWidgetAlpha the overall opacity of the
- * {@link FloatingWidgetView}
+ * @param floatingWidgetAlpha the overall opacity of the {@link FloatingWidgetView}
* @param foregroundAlpha the opacity of the foreground layer
- * @param fallbackBackgroundAlpha the opacity of the fallback background used
- * when the App
+ * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App
* Widget doesn't have a background
- * @param cornerRadiusProgress progress of the corner radius animation, where
- * 0 is the
+ * @param cornerRadiusProgress progress of the corner radius animation, where 0 is the
* original radius and 1 is the window radius
*/
public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha,
float fallbackBackgroundAlpha, float cornerRadiusProgress) {
- if (isUninitialized() || mAppTargetIsTranslucent)
- return;
+ if (isUninitialized() || mAppTargetIsTranslucent) return;
setAlpha(floatingWidgetAlpha);
mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha);
mAppWidgetView.setAlpha(foregroundAlpha);
@@ -220,34 +214,61 @@ public void setPositionOffsetY(float y) {
}
/**
- * Sets the layout parameters of the floating view and its background view
- * child.
+ * Sets the layout parameters of the floating view and its background view child.
+ * @return true if any of the views positions change due to this call.
*/
- private void positionViews() {
+ private boolean positionViews() {
+ boolean positionsChanged = false;
+
LayoutParams layoutParams = (LayoutParams) getLayoutParams();
- layoutParams.setMargins(0, 0, 0, 0);
- setLayoutParams(layoutParams);
+
+ if (layoutParams.topMargin != 0 || layoutParams.bottomMargin != 0
+ || layoutParams.rightMargin != 0 || layoutParams.leftMargin != 0) {
+ positionsChanged = true;
+ layoutParams.setMargins(0, 0, 0, 0);
+ setLayoutParams(layoutParams);
+ }
// FloatingWidgetView layout is forced LTR
- mBackgroundView.setTranslationX(mBackgroundPosition.left);
- mBackgroundView.setTranslationY(mBackgroundPosition.top + mIconOffsetY);
+ float targetY = mBackgroundPosition.top + mIconOffsetY;
+ if (mBackgroundView.getTranslationX() != mBackgroundPosition.left
+ || mBackgroundView.getTranslationY() != targetY) {
+ positionsChanged = true;
+ mBackgroundView.setTranslationX(mBackgroundPosition.left);
+ mBackgroundView.setTranslationY(targetY);
+ }
+
LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams();
- backgroundParams.leftMargin = 0;
- backgroundParams.topMargin = 0;
- backgroundParams.width = (int) mBackgroundPosition.width();
- backgroundParams.height = (int) mBackgroundPosition.height();
- mBackgroundView.setLayoutParams(backgroundParams);
+ if (backgroundParams.leftMargin != 0 || backgroundParams.topMargin != 0
+ || backgroundParams.width != Math.round(mBackgroundPosition.width())
+ || backgroundParams.height != Math.round(mBackgroundPosition.height())) {
+ positionsChanged = true;
+
+ backgroundParams.leftMargin = 0;
+ backgroundParams.topMargin = 0;
+ backgroundParams.width = Math.round(mBackgroundPosition.width());
+ backgroundParams.height = Math.round(mBackgroundPosition.height());
+ mBackgroundView.setLayoutParams(backgroundParams);
+ }
if (mForegroundOverlayView != null) {
sTmpMatrix.reset();
- float foregroundScale = mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth();
+ float foregroundScale =
+ mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth();
sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(),
-mBackgroundOffset.top - mAppWidgetView.getTop());
sTmpMatrix.postScale(foregroundScale, foregroundScale);
sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top
+ mIconOffsetY);
- mForegroundOverlayView.setMatrix(sTmpMatrix);
+
+ // We use the animation matrix here, because calling setMatrix on the GhostView
+ // actually sets the animation matrix, not the regular one.
+ if (!sTmpMatrix.equals(mForegroundOverlayView.getAnimationMatrix())) {
+ positionsChanged = true;
+ mForegroundOverlayView.setMatrix(sTmpMatrix);
+ }
}
+ return positionsChanged;
}
private void finish(DragLayer dragLayer) {
@@ -284,12 +305,10 @@ private void recycle() {
}
/**
- * Configures and returns a an instance of {@link FloatingWidgetView} matching
- * the appearance of
+ * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of
* {@param originalView}.
*
- * @param widgetBackgroundPosition a {@link RectF} that will be updated with the
- * widget's
+ * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's
* background bounds
* @param windowSize the size of the window when launched
* @param windowCornerRadius the corner radius of the window
@@ -300,8 +319,8 @@ public static FloatingWidgetView getFloatingWidgetView(QuickstepLauncher launche
int fallbackBackgroundColor) {
final DragLayer dragLayer = launcher.getDragLayer();
ViewGroup parent = (ViewGroup) dragLayer.getParent();
- FloatingWidgetView floatingView = launcher.getViewCache().getView(R.layout.floating_widget_view, launcher,
- parent);
+ FloatingWidgetView floatingView =
+ launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent);
floatingView.recycle();
floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowSize,
@@ -311,20 +330,24 @@ public static FloatingWidgetView getFloatingWidgetView(QuickstepLauncher launche
}
/**
- * Extract a background color from a target's task description, or fall back to
- * the given
+ * Extract a background color from a target's task description, or fall back to the given
* context's theme background color.
*/
public static int getDefaultBackgroundColor(
- Context context, RemoteAnimationTarget target) {
- return (target != null && target.taskInfo != null
- && target.taskInfo.taskDescription != null)
- ? target.taskInfo.taskDescription.getBackgroundColor()
- : Themes.getColorBackground(context);
+ Context context, @Nullable RemoteAnimationTarget target) {
+ final int fallbackColor = Themes.getColorBackground(context);
+ if (target == null) {
+ return fallbackColor;
+ }
+ final TaskInfo taskInfo = target.taskInfo;
+ if (taskInfo == null) {
+ return fallbackColor;
+ }
+ return taskInfo.taskDescription.getBackgroundColor();
}
private static void getRelativePosition(View descendant, View ancestor, RectF position) {
- float[] points = new float[] { 0, 0, descendant.getWidth(), descendant.getHeight() };
+ float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()};
Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points,
false /* includeRootScroll */, true /* ignoreTransform */);
position.set(
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index d6a3376c533..faa9e2893b4 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -21,11 +21,12 @@ import android.graphics.PointF
import android.util.AttributeSet
import android.util.Log
import android.view.View
+import android.view.ViewStub
import com.android.internal.jank.Cuj
import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
import com.android.launcher3.Utilities
-import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.util.RunnableList
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
@@ -33,12 +34,11 @@ import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_O
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.util.RecentsOrientedState
-import com.android.quickstep.util.SplitScreenUtils.Companion.convertLauncherSplitBoundsToShell
import com.android.quickstep.util.SplitSelectStateController
import com.android.systemui.shared.recents.model.Task
-import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
import com.android.systemui.shared.system.InteractionJankMonitorWrapper
-import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition
+import com.android.wm.shell.Flags.enableFlexibleTwoAppSplit
+import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition
/**
* TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
@@ -51,7 +51,16 @@ import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosi
* (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included).
*/
class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
- TaskView(context, attrs) {
+ TaskView(context, attrs, type = TaskViewType.GROUPED) {
+
+ private val MINIMUM_RATIO_TO_SHOW_ICON = 0.2f
+
+ val leftTopTaskContainer: TaskContainer
+ get() = taskContainers[0]
+
+ val rightBottomTaskContainer: TaskContainer
+ get() = taskContainers[1]
+
// TODO(b/336612373): Support new TTV for GroupedTaskView
var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null
private set
@@ -67,52 +76,41 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
setMeasuredDimension(widthSize, heightSize)
val splitBoundsConfig = splitBoundsConfig ?: return
- val initSplitTaskId = getThisTaskCurrentlyInSplitSelection()
- if (initSplitTaskId == INVALID_TASK_ID) {
- pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
- taskContainers[0].thumbnailViewDeprecated,
- taskContainers[1].thumbnailViewDeprecated,
- widthSize,
- heightSize,
- splitBoundsConfig,
- container.deviceProfile,
- layoutDirection == 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
- taskContainers[0]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateX(taskContainers[0].thumbnailViewDeprecated.translationX)
- taskContainers[0]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateY(taskContainers[0].thumbnailViewDeprecated.translationY)
- taskContainers[1]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateX(taskContainers[1].thumbnailViewDeprecated.translationX)
- taskContainers[1]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateY(taskContainers[1].thumbnailViewDeprecated.translationY)
- } else {
- // Currently being split with this taskView, let the non-split selected thumbnail
- // take up full thumbnail area
- taskContainers
- .firstOrNull { it.task.key.id != initSplitTaskId }
- ?.thumbnailViewDeprecated
- ?.measure(
- widthMeasureSpec,
- MeasureSpec.makeMeasureSpec(
- heightSize - container.deviceProfile.overviewTaskThumbnailTopMarginPx,
- MeasureSpec.EXACTLY
- )
- )
- }
+ val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID
+ pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
+ leftTopTaskContainer.snapshotView,
+ rightBottomTaskContainer.snapshotView,
+ widthSize,
+ heightSize,
+ splitBoundsConfig,
+ container.deviceProfile,
+ layoutDirection == LAYOUT_DIRECTION_RTL,
+ inSplitSelection,
+ )
+
if (!enableOverviewIconMenu()) {
updateIconPlacement()
}
}
+ override fun inflateViewStubs() {
+ super.inflateViewStubs()
+ findViewById(R.id.bottomright_snapshot)
+ ?.apply {
+ layoutResource =
+ if (enableRefactorTaskThumbnail()) R.layout.task_thumbnail
+ else R.layout.task_thumbnail_deprecated
+ }
+ ?.inflate()
+ findViewById(R.id.bottomRight_icon)
+ ?.apply {
+ layoutResource =
+ if (enableOverviewIconMenu()) R.layout.icon_app_chip_view
+ else R.layout.icon_view
+ }
+ ?.inflate()
+ }
+
override fun onRecycle() {
super.onRecycle()
splitBoundsConfig = null
@@ -133,37 +131,23 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
R.id.snapshot,
R.id.icon,
R.id.show_windows,
+ R.id.digital_wellbeing_toast,
STAGE_POSITION_TOP_OR_LEFT,
- taskOverlayFactory
+ taskOverlayFactory,
),
createTaskContainer(
secondaryTask,
R.id.bottomright_snapshot,
R.id.bottomRight_icon,
R.id.show_windows_right,
+ R.id.bottomRight_digital_wellbeing_toast,
STAGE_POSITION_BOTTOM_OR_RIGHT,
- taskOverlayFactory
- )
+ taskOverlayFactory,
+ ),
)
- this.splitBoundsConfig =
- splitBoundsConfig?.also {
- taskContainers[0]
- .thumbnailViewDeprecated
- .previewPositionHelper
- .setSplitBounds(
- convertLauncherSplitBoundsToShell(it),
- PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT
- )
- taskContainers[1]
- .thumbnailViewDeprecated
- .previewPositionHelper
- .setSplitBounds(
- convertLauncherSplitBoundsToShell(it),
- PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT
- )
- }
- taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) }
- setOrientationState(orientedState)
+ this.splitBoundsConfig = splitBoundsConfig
+ taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig }
+ onBind(orientedState)
}
override fun setOrientationState(orientationState: RecentsOrientedState) {
@@ -174,7 +158,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
container.deviceProfile,
it,
layoutParams.width,
- layoutParams.height
+ layoutParams.height,
)
val iconViewMarginStart =
resources.getDimensionPixelSize(
@@ -187,12 +171,10 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2
// setMaxWidth() needs to be called before mIconView.setIconOrientation which is
// called in the super below.
- (taskContainers[0].iconView as IconAppChipView).setMaxWidth(
+ (leftTopTaskContainer.iconView as IconAppChipView).maxWidth =
groupedTaskViewSizes.first.x - iconMargins
- )
- (taskContainers[1].iconView as IconAppChipView).setMaxWidth(
+ (rightBottomTaskContainer.iconView as IconAppChipView).maxWidth =
groupedTaskViewSizes.second.x - iconMargins
- )
}
}
super.setOrientationState(orientationState)
@@ -201,40 +183,72 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
private fun updateIconPlacement() {
val splitBoundsConfig = splitBoundsConfig ?: return
- val taskIconHeight = container.deviceProfile.overviewTaskIconSizePx
- val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+ val deviceProfile = container.deviceProfile
+ val taskIconHeight = deviceProfile.overviewTaskIconSizePx
+ val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID
+ var oneIconHiddenDueToSmallWidth = false
+
+ if (enableFlexibleTwoAppSplit()) {
+ // Update values for both icons' setFlexSplitAlpha. Mainly, we want to hide an icon if
+ // its app tile is too small. But we also have to set the alphas back if we go to
+ // split selection.
+ val hideLeftTopIcon: Boolean
+ val hideRightBottomIcon: Boolean
+ if (inSplitSelection) {
+ hideLeftTopIcon =
+ getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.leftTopTaskId
+ hideRightBottomIcon =
+ getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.rightBottomTaskId
+ } else {
+ hideLeftTopIcon = splitBoundsConfig.leftTopTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON
+ hideRightBottomIcon =
+ splitBoundsConfig.rightBottomTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON
+ if (hideLeftTopIcon || hideRightBottomIcon) {
+ oneIconHiddenDueToSmallWidth = true
+ }
+ }
+
+ leftTopTaskContainer.iconView.setFlexSplitAlpha(if (hideLeftTopIcon) 0f else 1f)
+ rightBottomTaskContainer.iconView.setFlexSplitAlpha(if (hideRightBottomIcon) 0f else 1f)
+ }
+
if (enableOverviewIconMenu()) {
+ val isDeviceRtl = Utilities.isRtl(resources)
val groupedTaskViewSizes =
pagedOrientationHandler.getGroupedTaskViewSizes(
- container.deviceProfile,
+ deviceProfile,
splitBoundsConfig,
layoutParams.width,
- layoutParams.height
+ layoutParams.height,
)
pagedOrientationHandler.setSplitIconParams(
- taskContainers[0].iconView.asView(),
- taskContainers[1].iconView.asView(),
+ leftTopTaskContainer.iconView.asView(),
+ rightBottomTaskContainer.iconView.asView(),
taskIconHeight,
groupedTaskViewSizes.first.x,
groupedTaskViewSizes.first.y,
layoutParams.height,
layoutParams.width,
- isRtl,
- container.deviceProfile,
- splitBoundsConfig
+ isDeviceRtl,
+ deviceProfile,
+ splitBoundsConfig,
+ inSplitSelection,
+ oneIconHiddenDueToSmallWidth,
)
} else {
pagedOrientationHandler.setSplitIconParams(
- taskContainers[0].iconView.asView(),
- taskContainers[1].iconView.asView(),
+ leftTopTaskContainer.iconView.asView(),
+ rightBottomTaskContainer.iconView.asView(),
taskIconHeight,
- taskContainers[0].thumbnailViewDeprecated.measuredWidth,
- taskContainers[0].thumbnailViewDeprecated.measuredHeight,
+ leftTopTaskContainer.snapshotView.measuredWidth,
+ leftTopTaskContainer.snapshotView.measuredHeight,
measuredHeight,
measuredWidth,
- isRtl,
- container.deviceProfile,
- splitBoundsConfig
+ isLayoutRtl,
+ deviceProfile,
+ splitBoundsConfig,
+ inSplitSelection,
+ oneIconHiddenDueToSmallWidth,
)
}
}
@@ -242,24 +256,20 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) {
splitBoundsConfig = splitBounds
taskContainers.forEach {
- it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig)
- it.digitalWellBeingToast?.initialize(it.task)
+ it.digitalWellBeingToast?.splitBounds = splitBoundsConfig
+ it.digitalWellBeingToast?.initialize()
}
invalidate()
}
- override fun launchTaskAnimated(): RunnableList? {
- if (taskContainers.isEmpty()) {
- Log.d(TAG, "launchTaskAnimated - task is not bound")
- return null
- }
+ override fun launchAsStaticTile(): RunnableList? {
val recentsView = recentsView ?: return null
val endCallback = RunnableList()
// Callbacks run from remote animation when recents animation not currently running
InteractionJankMonitorWrapper.begin(
this,
Cuj.CUJ_SPLIT_SCREEN_ENTER,
- "Enter form GroupedTaskView"
+ "Enter form GroupedTaskView",
)
launchTaskInternal(isQuickSwitch = false, launchingExistingTaskView = true) {
endCallback.executeAllAndDestroy()
@@ -271,8 +281,11 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
return endCallback
}
- override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
- launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/)
+ override fun launchWithoutAnimation(
+ isQuickSwitch: Boolean,
+ callback: (launched: Boolean) -> Unit,
+ ) {
+ launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback)
}
/**
@@ -284,19 +297,22 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
private fun launchTaskInternal(
isQuickSwitch: Boolean,
launchingExistingTaskView: Boolean,
- callback: (launched: Boolean) -> Unit
+ callback: (launched: Boolean) -> Unit,
) {
recentsView?.let {
it.splitSelectController.launchExistingSplitPair(
if (launchingExistingTaskView) this else null,
- taskContainers[0].task.key.id,
- taskContainers[1].task.key.id,
+ leftTopTaskContainer.task.key.id,
+ rightBottomTaskContainer.task.key.id,
STAGE_POSITION_TOP_OR_LEFT,
callback,
isQuickSwitch,
- snapPosition
+ snapPosition,
+ )
+ Log.d(
+ TAG,
+ "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView",
)
- Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}")
}
}
@@ -317,14 +333,14 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
// checks below aren't reliable since both of those views may be gone/transformed
val initSplitTaskId = getThisTaskCurrentlyInSplitSelection()
if (initSplitTaskId != INVALID_TASK_ID) {
- return if (initSplitTaskId == taskContainers[0].task.key.id) 1 else 0
+ return if (initSplitTaskId == leftTopTaskContainer.task.key.id) 1 else 0
}
}
// Check which of the two apps was selected
if (
- taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) ||
- taskContainers[1].thumbnailViewDeprecated.containsPoint(lastTouchDownPosition)
+ rightBottomTaskContainer.iconView.asView().containsPoint(lastTouchDownPosition) ||
+ rightBottomTaskContainer.snapshotView.containsPoint(lastTouchDownPosition)
) {
return 1
}
@@ -337,14 +353,6 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
return Utilities.pointInView(this, localPos[0], localPos[1], 0f /* slop */)
}
- override fun setOverlayEnabled(overlayEnabled: Boolean) {
- if (FeatureFlags.enableAppPairs()) {
- super.setOverlayEnabled(overlayEnabled)
- } else {
- // Intentional no-op to prevent setting smart actions overlay on thumbnails
- }
- }
-
companion object {
private const val TAG = "GroupedTaskView"
}
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
deleted file mode 100644
index ba42594e99c..00000000000
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ /dev/null
@@ -1,464 +0,0 @@
-/*
- * 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.views;
-
-import static com.android.app.animation.Interpolators.EMPHASIZED;
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.RectEvaluator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewAnimationUtils;
-import android.view.ViewOutlineProvider;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiPropertyFactory;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * An icon app menu view which can be used in place of an IconView in overview TaskViews.
- */
-public class IconAppChipView extends FrameLayout implements TaskViewIcon {
-
- private static final int MENU_BACKGROUND_REVEAL_DURATION = 417;
- private static final int MENU_BACKGROUND_HIDE_DURATION = 333;
-
- private static final int NUM_ALPHA_CHANNELS = 3;
- private static final int INDEX_CONTENT_ALPHA = 0;
- private static final int INDEX_COLOR_FILTER_ALPHA = 1;
- private static final int INDEX_MODAL_ALPHA = 2;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- private View mMenuAnchorView;
- private IconView mIconView;
- // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
- private TextView mIconTextCollapsedView;
- private TextView mIconTextExpandedView;
- private ImageView mIconArrowView;
- private final Rect mBackgroundRelativeLtrLocation = new Rect();
- final RectEvaluator mBackgroundAnimationRectEvaluator =
- new RectEvaluator(mBackgroundRelativeLtrLocation);
- private final int mCollapsedMenuDefaultWidth;
- private final int mExpandedMenuDefaultWidth;
- private final int mCollapsedMenuDefaultHeight;
- private final int mExpandedMenuDefaultHeight;
- private final int mIconMenuMarginTopStart;
- private final int mMenuToChipGap;
- private final int mBackgroundMarginTopStart;
- private final int mAppNameHorizontalMargin;
- private final int mIconViewMarginStart;
- private final int mAppIconSize;
- private final int mArrowSize;
- private final int mIconViewDrawableExpandedSize;
- private final int mArrowMarginEnd;
- private AnimatorSet mAnimator;
-
- private int mMaxWidth = Integer.MAX_VALUE;
-
- private static final int INDEX_SPLIT_TRANSLATION = 0;
- private static final int INDEX_MENU_TRANSLATION = 1;
- private static final int INDEX_COUNT_TRANSLATION = 2;
-
- private final MultiPropertyFactory mViewTranslationX;
- private final MultiPropertyFactory mViewTranslationY;
-
- /**
- * Gets the view split x-axis translation
- */
- public MultiPropertyFactory.MultiProperty getSplitTranslationX() {
- return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION);
- }
-
- /**
- * Sets the view split x-axis translation
- * @param translationX x-axis translation
- */
- public void setSplitTranslationX(float translationX) {
- getSplitTranslationX().setValue(translationX);
- }
-
- /**
- * Gets the view split y-axis translation
- */
- public MultiPropertyFactory.MultiProperty getSplitTranslationY() {
- return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION);
- }
-
- /**
- * Sets the view split y-axis translation
- * @param translationY y-axis translation
- */
- public void setSplitTranslationY(float translationY) {
- getSplitTranslationY().setValue(translationY);
- }
-
- /**
- * Gets the menu x-axis translation for split task
- */
- public MultiPropertyFactory.MultiProperty getMenuTranslationX() {
- return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
- }
-
- /**
- * Gets the menu y-axis translation for split task
- */
- public MultiPropertyFactory.MultiProperty getMenuTranslationY() {
- return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
- }
-
- public IconAppChipView(Context context) {
- this(context, null);
- }
-
- public IconAppChipView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- Resources res = getResources();
- mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
- mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
-
- // Menu dimensions
- mCollapsedMenuDefaultWidth =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width);
- mExpandedMenuDefaultWidth =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width);
- mCollapsedMenuDefaultHeight =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height);
- mExpandedMenuDefaultHeight =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height);
- mIconMenuMarginTopStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
- mMenuToChipGap = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_expanded_gap);
-
- // Background dimensions
- mBackgroundMarginTopStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
-
- // Contents dimensions
- mAppNameHorizontalMargin = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed);
- mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin);
- mIconViewMarginStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_view_start_margin);
- mAppIconSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size);
- mArrowSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_arrow_size);
- mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
-
- mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
- INDEX_COUNT_TRANSLATION,
- Float::sum);
- mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
- INDEX_COUNT_TRANSLATION,
- Float::sum);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mIconView = findViewById(R.id.icon_view);
- mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed);
- mIconTextExpandedView = findViewById(R.id.icon_text_expanded);
- mIconArrowView = findViewById(R.id.icon_arrow);
- mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor);
- }
-
- protected IconView getIconView() {
- return mIconView;
- }
-
- @Override
- public void setText(CharSequence text) {
- if (mIconTextCollapsedView != null) {
- mIconTextCollapsedView.setText(text);
- }
- if (mIconTextExpandedView != null) {
- mIconTextExpandedView.setText(text);
- }
- }
-
- @Override
- public Drawable getDrawable() {
- return mIconView == null ? null : mIconView.getDrawable();
- }
-
- @Override
- public void setDrawable(Drawable icon) {
- if (mIconView != null) {
- mIconView.setDrawable(icon);
- }
- }
-
- @Override
- public void setDrawableSize(int iconWidth, int iconHeight) {
- if (mIconView != null) {
- mIconView.setDrawableSize(iconWidth, iconHeight);
- }
- }
-
- /**
- * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
- * split screen.
- */
- public void setMaxWidth(int maxWidth) {
- // Width showing only the app icon and arrow. Max width should not be set to less than this.
- int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd;
- mMaxWidth = Math.max(maxWidth, minimumMaxWidth);
- }
-
- @Override
- public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
- RecentsPagedOrientationHandler orientationHandler =
- orientationState.getOrientationHandler();
- // Layout params for anchor view
- LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams();
- anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap;
- mMenuAnchorView.setLayoutParams(anchorLayoutParams);
-
- // Layout Params for the Menu View (this)
- LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
- iconMenuParams.width = mExpandedMenuDefaultWidth;
- iconMenuParams.height = mExpandedMenuDefaultHeight;
- orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart,
- mIconMenuMarginTopStart);
- setLayoutParams(iconMenuParams);
-
- // Layout params for the background
- Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
- mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds);
- setOutlineProvider(new ViewOutlineProvider() {
- final Rect mRtlAppliedOutlineBounds = new Rect();
- @Override
- public void getOutline(View view, Outline outline) {
- mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation);
- if (isLayoutRtl()) {
- int width = getWidth();
- mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right;
- mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left;
- }
- outline.setRoundRect(
- mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f);
- }
- });
-
- // Layout Params for the Icon View
- LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
- int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart;
- orientationHandler.setIconAppChipChildrenParams(
- iconParams, iconMarginStartRelativeToParent);
-
- mIconView.setLayoutParams(iconParams);
- mIconView.setDrawableSize(mAppIconSize, mAppIconSize);
-
- // Layout Params for the collapsed Icon Text View
- int textMarginStart =
- iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin;
- LayoutParams iconTextCollapsedParams =
- (LayoutParams) mIconTextCollapsedView.getLayoutParams();
- orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart);
- int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart
- - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd;
- iconTextCollapsedParams.width = collapsedTextWidth;
- mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams);
- mIconTextCollapsedView.setAlpha(1f);
-
- // Layout Params for the expanded Icon Text View
- LayoutParams iconTextExpandedParams =
- (LayoutParams) mIconTextExpandedView.getLayoutParams();
- orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart);
- mIconTextExpandedView.setLayoutParams(iconTextExpandedParams);
- mIconTextExpandedView.setAlpha(0f);
- mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth);
-
- // Layout Params for the Icon Arrow View
- LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
- int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
- orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
- mIconArrowView.setPivotY(iconArrowParams.height / 2f);
- mIconArrowView.setLayoutParams(iconArrowParams);
-
- // This method is called twice sometimes (like when rotating split tasks). It is called
- // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
- // a new width. This happens because we update widths on rotation and on measure of
- // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
- // it has just measured, so we explicitly call it here.
- measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY));
- }
-
- @Override
- public void setIconColorTint(int color, float amount) {
- // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
- float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR);
- mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha);
- }
-
- @Override
- public void setContentAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setModalAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
- }
-
- @Override
- public int getDrawableWidth() {
- return mIconView == null ? 0 : mIconView.getDrawableWidth();
- }
-
- @Override
- public int getDrawableHeight() {
- return mIconView == null ? 0 : mIconView.getDrawableHeight();
- }
-
- protected void revealAnim(boolean isRevealing) {
- cancelInProgressAnimations();
- final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
- final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds();
- final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation);
- mAnimator = new AnimatorSet();
-
- if (isRevealing) {
- boolean isRtl = isLayoutRtl();
- bringToFront();
- // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
- Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
- mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
- mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth());
- // Animate background clipping
- ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
- mBackgroundAnimationRectEvaluator,
- initialBackground,
- expandedBackgroundBounds);
- backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
- float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize;
- float arrowTranslationX =
- expandedBackgroundBounds.right - collapsedBackgroundBounds.right;
- float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin;
- float iconCenterToTextExpanded =
- mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin;
- float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed;
-
- float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX;
- float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX;
-
- mAnimator.playTogether(
- expandedTextRevealAnim,
- backgroundAnimator,
- ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling),
- ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X,
- textTranslationXWithRtl),
- ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X,
- textTranslationXWithRtl),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
- ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
- ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
- ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1));
- mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
- } else {
- // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
- Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
- mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
- mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth());
-
- // Animate background clipping
- ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
- mBackgroundAnimationRectEvaluator,
- initialBackground,
- collapsedBackgroundBounds);
- backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
- mAnimator.playTogether(
- expandedTextClipAnim,
- backgroundAnimator,
- ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
- ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
- ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1));
- mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
- }
-
- mAnimator.setInterpolator(EMPHASIZED);
- mAnimator.start();
- }
-
- private Rect getCollapsedBackgroundLtrBounds() {
- Rect bounds = new Rect(
- 0,
- 0,
- Math.min(mMaxWidth, mCollapsedMenuDefaultWidth),
- mCollapsedMenuDefaultHeight);
- bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart);
- return bounds;
- }
-
- private Rect getExpandedBackgroundLtrBounds() {
- return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
- }
-
- private void cancelInProgressAnimations() {
- // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
- // expecting to be mutable and will cause a crash if they are re-used.
- if (mAnimator != null && mAnimator.isStarted()) {
- mAnimator.cancel();
- mAnimator = null;
- }
- }
-
- @Override
- public View asView() {
- return this;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
new file mode 100644
index 00000000000..f4fd12792a1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
@@ -0,0 +1,464 @@
+/*
+ * 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.views
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewAnimationUtils
+import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.app.animation.Interpolators
+import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.quickstep.util.RecentsOrientedState
+import kotlin.math.max
+import kotlin.math.min
+
+/** An icon app menu view which can be used in place of an IconView in overview TaskViews. */
+class IconAppChipView
+@JvmOverloads
+constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon {
+
+ private var iconView: IconView? = null
+ private var iconArrowView: ImageView? = null
+ private var menuAnchorView: View? = null
+ // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
+ private var iconTextCollapsedView: TextView? = null
+ private var iconTextExpandedView: TextView? = null
+
+ private val backgroundRelativeLtrLocation = Rect()
+ private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation)
+
+ // Menu dimensions
+ private val collapsedMenuDefaultWidth: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width)
+ private val expandedMenuDefaultWidth: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width)
+ private val collapsedMenuDefaultHeight =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height)
+ private val expandedMenuDefaultHeight =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height)
+ private val iconMenuMarginTopStart =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin)
+ private val menuToChipGap: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap)
+
+ // Background dimensions
+ private val backgroundMarginTopStart: Int =
+ resources.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_background_margin_top_start
+ )
+
+ // Contents dimensions
+ private val appNameHorizontalMargin =
+ resources.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed
+ )
+ private val arrowMarginEnd =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin)
+ private val iconViewMarginStart =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin)
+ private val appIconSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size)
+ private val arrowSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size)
+ private val iconViewDrawableExpandedSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size)
+
+ private var animator: AnimatorSet? = null
+
+ private val multiValueAlpha: MultiValueAlpha =
+ MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) }
+
+ private val viewTranslationX: MultiPropertyFactory =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+ private val viewTranslationY: MultiPropertyFactory =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+ var maxWidth = Int.MAX_VALUE
+ /**
+ * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
+ * split screen.
+ */
+ set(value) {
+ // Width showing only the app icon and arrow. Max width should not be set to less than
+ // this.
+ val minMaxWidth = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd
+ field = max(value, minMaxWidth)
+ }
+
+ var status: AppChipStatus = AppChipStatus.Collapsed
+ private set
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ iconView = findViewById(R.id.icon_view)
+ iconTextCollapsedView = findViewById(R.id.icon_text_collapsed)
+ iconTextExpandedView = findViewById(R.id.icon_text_expanded)
+ iconArrowView = findViewById(R.id.icon_arrow)
+ menuAnchorView = findViewById(R.id.icon_view_menu_anchor)
+ }
+
+ override fun setText(text: CharSequence?) {
+ iconTextCollapsedView?.text = text
+ iconTextExpandedView?.text = text
+ }
+
+ override fun getDrawable(): Drawable? = iconView?.drawable
+
+ override fun setDrawable(icon: Drawable?) {
+ iconView?.drawable = icon
+ }
+
+ override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+ iconView?.setDrawableSize(iconWidth, iconHeight)
+ }
+
+ override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+ val orientationHandler = orientationState.orientationHandler
+ // Layout params for anchor view
+ val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams
+ anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap
+ menuAnchorView!!.layoutParams = anchorLayoutParams
+
+ // Layout Params for the Menu View (this)
+ val iconMenuParams = layoutParams as LayoutParams
+ iconMenuParams.width = expandedMenuDefaultWidth
+ iconMenuParams.height = expandedMenuDefaultHeight
+ orientationHandler.setIconAppChipMenuParams(
+ this,
+ iconMenuParams,
+ iconMenuMarginTopStart,
+ iconMenuMarginTopStart,
+ )
+ layoutParams = iconMenuParams
+
+ // Layout params for the background
+ val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+ backgroundRelativeLtrLocation.set(collapsedBackgroundBounds)
+ outlineProvider =
+ object : ViewOutlineProvider() {
+ val mRtlAppliedOutlineBounds: Rect = Rect()
+
+ override fun getOutline(view: View, outline: Outline) {
+ mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation)
+ if (isLayoutRtl) {
+ val width = width
+ mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right
+ mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left
+ }
+ outline.setRoundRect(
+ mRtlAppliedOutlineBounds,
+ mRtlAppliedOutlineBounds.height() / 2f,
+ )
+ }
+ }
+
+ // Layout Params for the Icon View
+ val iconParams = iconView!!.layoutParams as LayoutParams
+ val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart
+ orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent)
+
+ iconView!!.layoutParams = iconParams
+ iconView!!.setDrawableSize(appIconSize, appIconSize)
+
+ // Layout Params for the collapsed Icon Text View
+ val textMarginStart =
+ iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMargin
+ val iconTextCollapsedParams = iconTextCollapsedView!!.layoutParams as LayoutParams
+ orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart)
+ val collapsedTextWidth =
+ (collapsedBackgroundBounds.width() -
+ iconViewMarginStart -
+ appIconSize -
+ arrowSize -
+ appNameHorizontalMargin -
+ arrowMarginEnd)
+ iconTextCollapsedParams.width = collapsedTextWidth
+ iconTextCollapsedView!!.layoutParams = iconTextCollapsedParams
+ iconTextCollapsedView!!.alpha = 1f
+
+ // Layout Params for the expanded Icon Text View
+ val iconTextExpandedParams = iconTextExpandedView!!.layoutParams as LayoutParams
+ orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart)
+ iconTextExpandedView!!.layoutParams = iconTextExpandedParams
+ iconTextExpandedView!!.alpha = 0f
+ iconTextExpandedView!!.setRevealClip(
+ true,
+ 0f,
+ appIconSize / 2f,
+ collapsedTextWidth.toFloat(),
+ )
+
+ // Layout Params for the Icon Arrow View
+ val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams
+ val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize
+ orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart)
+ iconArrowView!!.pivotY = iconArrowParams.height / 2f
+ iconArrowView!!.layoutParams = iconArrowParams
+
+ // This method is called twice sometimes (like when rotating split tasks). It is called
+ // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
+ // a new width. This happens because we update widths on rotation and on measure of
+ // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
+ // it has just measured, so we explicitly call it here.
+ measure(
+ MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY),
+ )
+ }
+
+ override fun setIconColorTint(color: Int, amount: Float) {
+ // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
+ val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR)
+ multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha
+ }
+
+ override fun setContentAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha
+ }
+
+ override fun setModalAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha
+ }
+
+ override fun setFlexSplitAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha
+ }
+
+ override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0
+
+ override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0
+
+ /** Gets the view split x-axis translation */
+ fun getSplitTranslationX(): MultiPropertyFactory.MultiProperty =
+ viewTranslationX.get(INDEX_SPLIT_TRANSLATION)
+
+ /**
+ * Sets the view split x-axis translation
+ *
+ * @param value x-axis translation
+ */
+ fun setSplitTranslationX(value: Float) {
+ getSplitTranslationX().value = value
+ }
+
+ /** Gets the view split y-axis translation */
+ fun getSplitTranslationY(): MultiPropertyFactory.MultiProperty =
+ viewTranslationY[INDEX_SPLIT_TRANSLATION]
+
+ /**
+ * Sets the view split y-axis translation
+ *
+ * @param value y-axis translation
+ */
+ fun setSplitTranslationY(value: Float) {
+ getSplitTranslationY().value = value
+ }
+
+ /** Gets the menu x-axis translation for split task */
+ fun getMenuTranslationX(): MultiPropertyFactory.MultiProperty =
+ viewTranslationX[INDEX_MENU_TRANSLATION]
+
+ /** Gets the menu y-axis translation for split task */
+ fun getMenuTranslationY(): MultiPropertyFactory.MultiProperty =
+ viewTranslationY[INDEX_MENU_TRANSLATION]
+
+ internal fun revealAnim(isRevealing: Boolean, animated: Boolean = true) {
+ cancelInProgressAnimations()
+ val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+ val expandedBackgroundBounds = getExpandedBackgroundLtrBounds()
+ val initialBackground = Rect(backgroundRelativeLtrLocation)
+ animator = AnimatorSet()
+
+ if (isRevealing) {
+ val isRtl = isLayoutRtl
+ bringToFront()
+ // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+ val expandedTextRevealAnim =
+ ViewAnimationUtils.createCircularReveal(
+ iconTextExpandedView,
+ 0,
+ iconTextExpandedView!!.height / 2,
+ iconTextCollapsedView!!.width.toFloat(),
+ iconTextExpandedView!!.width.toFloat(),
+ )
+ // Animate background clipping
+ val backgroundAnimator =
+ ValueAnimator.ofObject(
+ backgroundAnimationRectEvaluator,
+ initialBackground,
+ expandedBackgroundBounds,
+ )
+ backgroundAnimator.addUpdateListener { invalidateOutline() }
+
+ val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat()
+ val arrowTranslationX =
+ (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat()
+ val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMargin
+ val iconCenterToTextExpanded =
+ iconViewDrawableExpandedSize / 2f + appNameHorizontalMargin
+ val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed
+
+ val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX
+ val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX
+
+ animator!!.playTogether(
+ expandedTextRevealAnim,
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling),
+ ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling),
+ ObjectAnimator.ofFloat(
+ iconTextCollapsedView,
+ TRANSLATION_X,
+ textTranslationXWithRtl,
+ ),
+ ObjectAnimator.ofFloat(
+ iconTextExpandedView,
+ TRANSLATION_X,
+ textTranslationXWithRtl,
+ ),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 0f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 1f),
+ ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
+ ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f),
+ )
+ animator!!.duration = MENU_BACKGROUND_REVEAL_DURATION.toLong()
+ status = AppChipStatus.Expanded
+ } else {
+ // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+ val expandedTextClipAnim =
+ ViewAnimationUtils.createCircularReveal(
+ iconTextExpandedView,
+ 0,
+ iconTextExpandedView!!.height / 2,
+ iconTextExpandedView!!.width.toFloat(),
+ iconTextCollapsedView!!.width.toFloat(),
+ )
+
+ // Animate background clipping
+ val backgroundAnimator =
+ ValueAnimator.ofObject(
+ backgroundAnimationRectEvaluator,
+ initialBackground,
+ collapsedBackgroundBounds,
+ )
+ backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? ->
+ invalidateOutline()
+ }
+
+ animator!!.playTogether(
+ expandedTextClipAnim,
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 1f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 0f),
+ ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f),
+ )
+ animator!!.duration = MENU_BACKGROUND_HIDE_DURATION.toLong()
+ status = AppChipStatus.Collapsed
+ }
+
+ if (!animated) animator!!.duration = 0
+ animator!!.interpolator = Interpolators.EMPHASIZED
+ animator!!.start()
+ }
+
+ private fun getCollapsedBackgroundLtrBounds(): Rect {
+ val bounds =
+ Rect(0, 0, min(maxWidth, collapsedMenuDefaultWidth), collapsedMenuDefaultHeight)
+ bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart)
+ return bounds
+ }
+
+ private fun getExpandedBackgroundLtrBounds() =
+ Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight)
+
+ private fun cancelInProgressAnimations() {
+ // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
+ // expecting to be mutable and will cause a crash if they are re-used.
+ if (animator != null && animator!!.isStarted) {
+ animator!!.cancel()
+ animator = null
+ }
+ }
+
+ override fun focusSearch(direction: Int): View? {
+ if (mParent == null) return null
+ return when (direction) {
+ FOCUS_RIGHT,
+ FOCUS_DOWN -> mParent.focusSearch(this, View.FOCUS_FORWARD)
+ FOCUS_UP,
+ FOCUS_LEFT -> mParent.focusSearch(this, View.FOCUS_BACKWARD)
+ else -> super.focusSearch(direction)
+ }
+ }
+
+ fun reset() {
+ setText(null)
+ setDrawable(null)
+ }
+
+ override fun asView(): View = this
+
+ enum class AppChipStatus {
+ Expanded,
+ Collapsed,
+ }
+
+ private companion object {
+ private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b }
+
+ private const val MENU_BACKGROUND_REVEAL_DURATION = 417
+ private const val MENU_BACKGROUND_HIDE_DURATION = 333
+
+ private const val NUM_ALPHA_CHANNELS = 4
+ private const val INDEX_CONTENT_ALPHA = 0
+ private const val INDEX_COLOR_FILTER_ALPHA = 1
+ private const val INDEX_MODAL_ALPHA = 2
+ /** Used to hide the app chip for 90:10 flex split. */
+ private const val INDEX_MINIMUM_RATIO_ALPHA = 3
+
+ private const val INDEX_SPLIT_TRANSLATION = 0
+ private const val INDEX_MENU_TRANSLATION = 1
+ private const val INDEX_COUNT_TRANSLATION = 2
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
deleted file mode 100644
index bb4a7ecda68..00000000000
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2018 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.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
- * when the drawable changes.
- */
-public class IconView extends View implements TaskViewIcon {
- private static final int NUM_ALPHA_CHANNELS = 2;
- private static final int INDEX_CONTENT_ALPHA = 0;
- private static final int INDEX_MODAL_ALPHA = 1;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- @Nullable
- private Drawable mDrawable;
- private int mDrawableWidth, mDrawableHeight;
-
- public IconView(Context context) {
- this(context, null);
- }
-
- public IconView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
- mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
- }
-
- /**
- * Sets a {@link Drawable} to be displayed.
- */
- @Override
- public void setDrawable(@Nullable Drawable d) {
- if (mDrawable != null) {
- mDrawable.setCallback(null);
- }
- mDrawable = d;
- if (mDrawable != null) {
- mDrawable.setCallback(this);
- setDrawableSizeInternal(getWidth(), getHeight());
- }
- invalidate();
- }
-
- /**
- * Sets the size of the icon drawable.
- */
- @Override
- public void setDrawableSize(int iconWidth, int iconHeight) {
- mDrawableWidth = iconWidth;
- mDrawableHeight = iconHeight;
- if (mDrawable != null) {
- setDrawableSizeInternal(getWidth(), getHeight());
- }
- }
-
- private void setDrawableSizeInternal(int selfWidth, int selfHeight) {
- Rect selfRect = new Rect(0, 0, selfWidth, selfHeight);
- Rect drawableRect = new Rect();
- Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect);
- mDrawable.setBounds(drawableRect);
- }
-
- @Override
- @Nullable
- public Drawable getDrawable() {
- return mDrawable;
- }
-
- @Override
- public int getDrawableWidth() {
- return mDrawableWidth;
- }
-
- @Override
- public int getDrawableHeight() {
- return mDrawableHeight;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (mDrawable != null) {
- setDrawableSizeInternal(w, h);
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || who == mDrawable;
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- final Drawable drawable = mDrawable;
- if (drawable != null && drawable.isStateful()
- && drawable.setState(getDrawableState())) {
- invalidateDrawable(drawable);
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (mDrawable != null) {
- mDrawable.draw(canvas);
- }
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- @Override
- public void setContentAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setModalAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
- }
-
- /**
- * Set the tint color of the icon, useful for scrimming or dimming.
- *
- * @param color to blend in.
- * @param amount [0,1] 0 no tint, 1 full tint
- */
- @Override
- public void setIconColorTint(int color, float amount) {
- if (mDrawable != null) {
- mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
- }
- }
-
- @Override
- public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
- RecentsPagedOrientationHandler orientationHandler =
- orientationState.getOrientationHandler();
- boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- DeviceProfile deviceProfile =
- ActivityContext.lookupContext(getContext()).getDeviceProfile();
-
- FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
-
- int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
- int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
- int taskMargin = deviceProfile.overviewTaskMarginPx;
-
- orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
- thumbnailTopMargin, isRtl);
- iconParams.width = iconParams.height = taskIconHeight;
- setLayoutParams(iconParams);
-
- setRotation(orientationHandler.getDegreesRotated());
- int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
- : deviceProfile.overviewTaskIconDrawableSizePx;
- setDrawableSize(iconDrawableSize, iconDrawableSize);
- }
-
- @Override
- public View asView() {
- return this;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
new file mode 100644
index 00000000000..cb69b22de0e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2018 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.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Flags
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MSDLPlayerWrapper
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.RecentsOrientedState
+import com.google.android.msdl.data.model.MSDLToken
+
+/**
+ * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
+ * when the drawable changes.
+ */
+class IconView : View, TaskViewIcon {
+ private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+ private var drawable: Drawable? = null
+ private var drawableWidth = 0
+ private var drawableHeight = 0
+ private var msdlPlayerWrapper: MSDLPlayerWrapper? = null
+
+ constructor(context: Context) : super(context) {
+ setUpHaptics()
+ }
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
+ setUpHaptics()
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr) {
+ setUpHaptics()
+ }
+
+ init {
+ multiValueAlpha.setUpdateVisibility(true)
+ }
+
+ private fun setUpHaptics() {
+ msdlPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context)
+ // Haptics are handled by the MSDLPlayerWrapper
+ isHapticFeedbackEnabled = !Flags.msdlFeedback() || msdlPlayerWrapper == null
+ }
+
+ override fun setOnLongClickListener(l: OnLongClickListener?) {
+ super.setOnLongClickListener(l?.withFeedback())
+ }
+
+ /** Sets a [Drawable] to be displayed. */
+ override fun setDrawable(d: Drawable?) {
+ drawable?.callback = null
+
+ // Copy drawable so that mutations below do not affect other users of the drawable
+ drawable = d?.constantState?.newDrawable()?.mutate()
+ drawable?.let {
+ it.callback = this
+ setDrawableSizeInternal(width, height)
+ }
+ invalidate()
+ }
+
+ /** Sets the size of the icon drawable. */
+ override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+ drawableWidth = iconWidth
+ drawableHeight = iconHeight
+ drawable?.let { setDrawableSizeInternal(width, height) }
+ }
+
+ private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) {
+ val selfRect = Rect(0, 0, selfWidth, selfHeight)
+ val drawableRect = Rect()
+ Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect)
+ drawable?.bounds = drawableRect
+ }
+
+ override fun getDrawable(): Drawable? = drawable
+
+ override fun getDrawableWidth(): Int = drawableWidth
+
+ override fun getDrawableHeight(): Int = drawableHeight
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ drawable?.let { setDrawableSizeInternal(w, h) }
+ }
+
+ override fun verifyDrawable(who: Drawable): Boolean =
+ super.verifyDrawable(who) || who === drawable
+
+ override fun drawableStateChanged() {
+ super.drawableStateChanged()
+ drawable?.let {
+ if (it.isStateful && it.setState(drawableState)) {
+ invalidateDrawable(it)
+ }
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ drawable?.draw(canvas)
+ }
+
+ override fun hasOverlappingRendering(): Boolean = false
+
+ override fun setContentAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha)
+ }
+
+ override fun setModalAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha)
+ }
+
+ override fun setFlexSplitAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_FLEX_SPLIT_ALPHA].setValue(alpha)
+ }
+
+ /**
+ * Set the tint color of the icon, useful for scrimming or dimming.
+ *
+ * @param color to blend in.
+ * @param amount [0,1] 0 no tint, 1 full tint
+ */
+ override fun setIconColorTint(color: Int, amount: Float) {
+ drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount)
+ }
+
+ override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+ val orientationHandler = orientationState.orientationHandler
+ val deviceProfile: DeviceProfile =
+ (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile()
+ orientationHandler.setTaskIconParams(
+ iconParams = getLayoutParams() as FrameLayout.LayoutParams,
+ taskIconMargin = deviceProfile.overviewTaskMarginPx,
+ taskIconHeight = deviceProfile.overviewTaskIconSizePx,
+ thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx,
+ isRtl = layoutDirection == LAYOUT_DIRECTION_RTL,
+ )
+ updateLayoutParams {
+ height = deviceProfile.overviewTaskIconSizePx
+ width = height
+ }
+ setRotation(orientationHandler.degreesRotated)
+ val iconDrawableSize =
+ if (isGridTask) deviceProfile.overviewTaskIconDrawableSizeGridPx
+ else deviceProfile.overviewTaskIconDrawableSizePx
+ setDrawableSize(iconDrawableSize, iconDrawableSize)
+ }
+
+ override fun asView(): View = this
+
+ private fun OnLongClickListener.withFeedback(): OnLongClickListener {
+ val delegate = this
+ return OnLongClickListener { v: View ->
+ if (Flags.msdlFeedback()) {
+ msdlPlayerWrapper?.playToken(MSDLToken.LONG_PRESS)
+ }
+ delegate.onLongClick(v)
+ }
+ }
+
+ companion object {
+ private const val NUM_ALPHA_CHANNELS = 3
+ private const val INDEX_CONTENT_ALPHA = 0
+ private const val INDEX_MODAL_ALPHA = 1
+ private const val INDEX_FLEX_SPLIT_ALPHA = 2
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index b4bca1ced86..a6be3f789ae 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -16,17 +16,16 @@
package com.android.quickstep.views;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
-import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
-import static com.android.launcher3.LauncherState.EDIT_MODE;
+import static com.android.launcher3.LauncherState.ADD_DESK_BUTTON;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
-import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
-import static com.android.window.flags2.Flags.enableDesktopWindowingWallpaperActivity;
import android.annotation.TargetApi;
import android.content.Context;
@@ -40,7 +39,6 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.desktop.DesktopRecentsTransitionController;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.statehandlers.DepthController;
@@ -51,12 +49,13 @@
import com.android.launcher3.util.PendingSplitSelectInfo;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
+import com.android.quickstep.BaseContainerInterface;
import com.android.quickstep.GestureState;
import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.RotationTouchHelper;
import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
-import com.android.systemui.shared.recents.model.Task;
+import com.android.wm.shell.shared.GroupedTaskInfo;
import kotlin.Unit;
@@ -76,7 +75,7 @@ public LauncherRecentsView(Context context, @Nullable AttributeSet attrs) {
}
public LauncherRecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr, LauncherActivityInterface.INSTANCE);
+ super(context, attrs, defStyleAttr);
getStateManager().addStateListener(this);
}
@@ -92,10 +91,12 @@ public void init(OverviewActionsView actionsView,
protected void handleStartHome(boolean animated) {
StateManager stateManager = getStateManager();
animated &= stateManager.shouldAnimateStateChange();
- stateManager.goToState(NORMAL, animated);
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.getSplitAnimationController()
- .playPlaceholderDismissAnim(mContainer, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
+ if (mSplitSelectStateController.isSplitSelectActive()) {
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+ mSplitSelectStateController.getSplitAnimationController());
+ } else {
+ stateManager.goToState(NORMAL, animated);
}
AbstractFloatingView.closeAllOpenViews(mContainer, animated);
}
@@ -128,9 +129,11 @@ public void onTaskIconChanged(int taskId) {
// If Launcher needs to return to split select state, do it now, after the icon has updated.
if (mContainer.hasPendingSplitSelectInfo()) {
PendingSplitSelectInfo recoveryData = mContainer.getPendingSplitSelectInfo();
- if (recoveryData.getStagedTaskId() == taskId) {
+ TaskContainer taskContainer;
+ if (recoveryData != null && recoveryData.getStagedTaskId() == taskId && (taskContainer =
+ mUtils.getTaskContainerById(taskId)) != null) {
initiateSplitSelect(
- getTaskViewByTaskId(recoveryData.getStagedTaskId()),
+ taskContainer,
recoveryData.getStagePosition(), recoveryData.getSource()
);
mContainer.finishSplitSelectRecovery();
@@ -150,7 +153,14 @@ public void reset() {
public void onStateTransitionStart(LauncherState toState) {
setOverviewStateEnabled(toState.isRecentsViewVisible);
- setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+ if (enableGridOnlyOverview()) {
+ if (toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
+ setOverviewGridEnabled(true);
+ }
+ } else {
+ setOverviewGridEnabled(
+ toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+ }
setOverviewFullscreenEnabled(toState.getOverviewFullscreenProgress() == 1);
if (toState == OVERVIEW_MODAL_TASK) {
setOverviewSelectEnabled(true);
@@ -164,15 +174,18 @@ public void onStateTransitionStart(LauncherState toState) {
}
setFreezeViewVisibility(true);
- if (mContainer.getDesktopVisibilityController() != null) {
- mContainer.getDesktopVisibilityController().onLauncherStateChanged(toState);
- }
}
@Override
public void onStateTransitionComplete(LauncherState finalState) {
- if (finalState == NORMAL || finalState == SPRING_LOADED || finalState == EDIT_MODE
- || finalState == ALL_APPS) {
+ DesktopVisibilityController.INSTANCE.get(mContainer).onLauncherStateChanged(finalState);
+ if (enableGridOnlyOverview()) {
+ if (!finalState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
+ setOverviewGridEnabled(false);
+ }
+ }
+
+ if (!finalState.isRecentsViewVisible) {
// Clean-up logic that occurs when recents is no longer in use/visible.
reset();
}
@@ -186,10 +199,8 @@ public void onStateTransitionComplete(LauncherState finalState) {
if (finalState.isRecentsViewVisible && finalState != OVERVIEW_MODAL_TASK) {
setTaskBorderEnabled(true);
}
-
if (isOverlayEnabled) {
- runActionOnRemoteHandles(
- remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
+ mBlurUtils.setDrawLiveTileBelowRecents(true);
}
}
@@ -200,7 +211,10 @@ public void setOverviewStateEnabled(boolean enabled) {
LauncherState state = getStateManager().getState();
boolean hasClearAllButton = (state.getVisibleElements(mContainer)
& CLEAR_ALL_BUTTON) != 0;
+ boolean hasAddDeskButton = (state.getVisibleElements(mContainer)
+ & ADD_DESK_BUTTON) != 0;
setDisallowScrollToClearAll(!hasClearAllButton);
+ setDisallowScrollToAddDesk(!hasAddDeskButton);
}
}
@@ -226,6 +240,11 @@ public void setModalStateEnabled(int taskId, boolean animate) {
}
}
+ @Override
+ protected BaseContainerInterface getContainerInterface(int displayId) {
+ return LauncherActivityInterface.INSTANCE;
+ }
+
@Override
protected void onDismissAnimationEnds() {
super.onDismissAnimationEnds();
@@ -238,10 +257,10 @@ protected void onDismissAnimationEnds() {
}
@Override
- public void initiateSplitSelect(TaskView taskView,
+ public void initiateSplitSelect(TaskContainer taskContainer,
@SplitConfigurationOptions.StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent) {
- super.initiateSplitSelect(taskView, stagePosition, splitEvent);
+ super.initiateSplitSelect(taskContainer, stagePosition, splitEvent);
getStateManager().goToState(LauncherState.OVERVIEW_SPLIT_SELECT);
}
@@ -252,44 +271,34 @@ public void initiateSplitSelect(SplitSelectSource splitSelectSource) {
}
@Override
- protected boolean canLaunchFullscreenTask() {
- if (FeatureFlags.enableSplitContextually()) {
- return !mSplitSelectStateController.isSplitSelectActive();
- } else {
- return !mContainer.isInState(OVERVIEW_SPLIT_SELECT);
- }
+ public boolean canLaunchFullscreenTask() {
+ return !mSplitSelectStateController.isSplitSelectActive();
}
@Override
- public void onGestureAnimationStart(Task[] runningTasks,
- RotationTouchHelper rotationTouchHelper) {
- super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
- DesktopVisibilityController desktopVisibilityController =
- mContainer.getDesktopVisibilityController();
- if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+ public void onGestureAnimationStart(GroupedTaskInfo groupedTaskInfo) {
+ super.onGestureAnimationStart(groupedTaskInfo);
+ if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
// TODO: b/333533253 - Remove after flag rollout
- desktopVisibilityController.setRecentsGestureStart();
+ DesktopVisibilityController.INSTANCE.get(mContainer).setRecentsGestureStart();
}
}
@Override
public void onGestureAnimationEnd() {
- DesktopVisibilityController desktopVisibilityController =
- mContainer.getDesktopVisibilityController();
+ final DesktopVisibilityController desktopVisibilityController =
+ DesktopVisibilityController.INSTANCE.get(mContainer);
boolean showDesktopApps = false;
- GestureState.GestureEndTarget endTarget = null;
- if (desktopVisibilityController != null) {
- desktopVisibilityController = mContainer.getDesktopVisibilityController();
- endTarget = mCurrentGestureEndTarget;
- if (endTarget == GestureState.GestureEndTarget.LAST_TASK
- && desktopVisibilityController.areDesktopTasksVisible()) {
- // Recents gesture was cancelled and we are returning to the previous task.
- // After super class has handled clean up, show desktop apps on top again
- showDesktopApps = true;
- }
+ GestureState.GestureEndTarget endTarget = mCurrentGestureEndTarget;
+ if (endTarget == GestureState.GestureEndTarget.LAST_TASK
+ && desktopVisibilityController.isInDesktopModeAndNotInOverview(
+ mContainer.getDisplayId())) {
+ // Recents gesture was cancelled and we are returning to the previous task.
+ // After super class has handled clean up, show desktop apps on top again
+ showDesktopApps = true;
}
super.onGestureAnimationEnd();
- if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+ if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
// TODO: b/333533253 - Remove after flag rollout
desktopVisibilityController.setRecentsGestureEnd(endTarget);
}
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 9c1f0663e3b..ea0c41ce790 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -40,6 +40,8 @@
import com.android.launcher3.util.NavigationMode;
import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
import com.android.quickstep.util.LayoutUtils;
+import com.android.wm.shell.shared.TypefaceUtils;
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -65,8 +67,7 @@ public class OverviewActionsView extends FrameLayo
HIDDEN_DESKTOP
})
@Retention(RetentionPolicy.SOURCE)
- public @interface ActionsHiddenFlags {
- }
+ public @interface ActionsHiddenFlags { }
public static final int HIDDEN_NON_ZERO_ROTATION = 1 << 0;
public static final int HIDDEN_NO_TASKS = 1 << 1;
@@ -79,10 +80,9 @@ public class OverviewActionsView extends FrameLayo
@IntDef(flag = true, value = {
DISABLED_SCROLLING,
DISABLED_ROTATED,
- DISABLED_NO_THUMBNAIL })
+ DISABLED_NO_THUMBNAIL})
@Retention(RetentionPolicy.SOURCE)
- public @interface ActionsDisabledFlags {
- }
+ public @interface ActionsDisabledFlags { }
public static final int DISABLED_SCROLLING = 1 << 0;
public static final int DISABLED_ROTATED = 1 << 1;
@@ -98,14 +98,11 @@ public class OverviewActionsView extends FrameLayo
private static final int INDEX_3P_LAUNCHER = 7;
private static final int NUM_ALPHAS = 8;
- public @interface SplitButtonHiddenFlags {
- }
-
+ public @interface SplitButtonHiddenFlags { }
public static final int FLAG_SMALL_SCREEN_HIDE_SPLIT = 1 << 0;
/**
- * Holds an AnimatedFloat for each alpha property, used to set or animate alpha
- * values in
+ * Holds an AnimatedFloat for each alpha property, used to set or animate alpha values in
* {@link #mMultiValueAlphas}.
*/
private final AnimatedFloat[] mAlphaProperties = new AnimatedFloat[NUM_ALPHAS];
@@ -117,14 +114,11 @@ public class OverviewActionsView extends FrameLayo
/** Index used for grouped-task actions in the mMultiValueAlphas array */
private static final int GROUP_ACTIONS_ALPHAS = 1;
- /**
- * Container for the action buttons below a focused, non-split Overview tile.
- */
+ /** Container for the action buttons below a focused, non-split Overview tile. */
protected LinearLayout mActionButtons;
private Button mSplitButton;
/**
- * The "save app pair" button. Currently this is the only button that is not
- * contained in
+ * The "save app pair" button. Currently this is the only button that is not contained in
* mActionButtons, since it is the sole button that appears for a grouped task.
*/
private Button mSaveAppPairButton;
@@ -163,18 +157,17 @@ public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int de
protected void onFinishInflate() {
super.onFinishInflate();
// Initialize 2 view containers: one for single tasks, one for grouped tasks.
- // These will take up the same space on the screen and alternate visibility as
- // needed.
+ // These will take up the same space on the screen and alternate visibility as needed.
// Currently, the only grouped task action is "save app pairs".
mActionButtons = findViewById(R.id.action_buttons);
mSaveAppPairButton = findViewById(R.id.action_save_app_pair);
- // Initialize a list to hold alphas for mActionButtons and any group action
- // buttons.
+ TypefaceUtils.setTypeface(mSaveAppPairButton, FontFamily.GSF_LABEL_LARGE);
+ // Initialize a list to hold alphas for mActionButtons and any group action buttons.
mMultiValueAlphas[ACTIONS_ALPHAS] = new MultiValueAlpha(mActionButtons, NUM_ALPHAS);
- mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] = new MultiValueAlpha(mSaveAppPairButton, NUM_ALPHAS);
+ mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] =
+ new MultiValueAlpha(mSaveAppPairButton, NUM_ALPHAS);
Arrays.stream(mMultiValueAlphas).forEach(a -> a.setUpdateVisibility(true));
- // To control alpha simultaneously on mActionButtons and any group action
- // buttons, we set up
+ // To control alpha simultaneously on mActionButtons and any group action buttons, we set up
// an AnimatedFloat for each alpha property.
for (int i = 0; i < NUM_ALPHAS; i++) {
final int index = i;
@@ -185,10 +178,8 @@ protected void onFinishInflate() {
}, 1f /* initialValue */);
}
- // The screenshot button is implemented as a Button in launcher3 and
- // NexusLauncher, but is
- // an ImageButton in go launcher (does not share a common class with Button).
- // Take care when
+ // The screenshot button is implemented as a Button in launcher3 and NexusLauncher, but is
+ // an ImageButton in go launcher (does not share a common class with Button). Take care when
// casting this.
View screenshotButton = findViewById(R.id.action_screenshot);
screenshotButton.setOnClickListener(this);
@@ -245,15 +236,12 @@ public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean e
}
/**
- * Updates the proper disabled flag to indicate whether OverviewActionsView
- * should be enabled.
- * Ignores DISABLED_ROTATED flag for determining enabled. Flag is used to
- * enable/disable
+ * Updates the proper disabled flag to indicate whether OverviewActionsView should be enabled.
+ * Ignores DISABLED_ROTATED flag for determining enabled. Flag is used to enable/disable
* buttons individually, currently done for select button in subclass.
*
* @param disabledFlags The flag to update.
- * @param enable Whether to enable the disable flag: True will cause view
- * to be disabled.
+ * @param enable Whether to enable the disable flag: True will cause view to be disabled.
*/
public void updateDisabledFlags(@ActionsDisabledFlags int disabledFlags, boolean enable) {
if (enable) {
@@ -266,14 +254,11 @@ public void updateDisabledFlags(@ActionsDisabledFlags int disabledFlags, boolean
}
/**
- * Updates a batch of flags to hide and show actions buttons when a grouped task
- * (split screen)
+ * Updates a batch of flags to hide and show actions buttons when a grouped task (split screen)
* is focused.
- *
- * @param isGroupedTask True if the focused task is a grouped task.
- * @param canSaveAppPair True if the focused task is a grouped task and can be
- * saved as an app
- * pair.
+ * @param isGroupedTask True if the focused task is a grouped task.
+ * @param canSaveAppPair True if the focused task is a grouped task and can be saved as an app
+ * pair.
*/
public void updateForGroupedTask(boolean isGroupedTask, boolean canSaveAppPair) {
Log.d(TAG, "updateForGroupedTask() called with: isGroupedTask = [" + isGroupedTask
@@ -284,8 +269,7 @@ public void updateForGroupedTask(boolean isGroupedTask, boolean canSaveAppPair)
}
/**
- * Updates a batch of flags to hide and show actions buttons for tablet/non
- * tablet case.
+ * Updates a batch of flags to hide and show actions buttons for tablet/non tablet case.
*/
private void updateForIsTablet() {
assert mDp != null;
@@ -295,7 +279,9 @@ private void updateForIsTablet() {
}
private void updateActionButtonsVisibility() {
- assert mDp != null;
+ if (mDp == null) {
+ return;
+ }
boolean showSingleTaskActions = !mIsGroupedTask;
boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair;
Log.d(TAG, "updateActionButtonsVisibility() called: showSingleTaskActions = ["
@@ -320,17 +306,14 @@ private MultiValueAlpha getGroupActionsAlphas() {
}
/**
- * Updates the proper flags to indicate whether the "Split screen" button should
- * be hidden.
+ * Updates the proper flags to indicate whether the "Split screen" button should be hidden.
*
* @param flag The flag to update.
- * @param enable Whether to enable the hidden flag: True will cause view to be
- * hidden.
+ * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
*/
void updateSplitButtonHiddenFlags(@SplitButtonHiddenFlags int flag,
boolean enable) {
- if (mSplitButton == null)
- return;
+ if (mSplitButton == null) return;
if (enable) {
mSplitButtonHiddenFlags |= flag;
} else {
@@ -372,19 +355,14 @@ public boolean areActionsButtonsVisible() {
}
/**
- * Offsets OverviewActionsView horizontal position based on 3 button nav
- * container in taskbar.
+ * Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar.
*/
private void updatePadding() {
- // If taskbar is in overview, overview action has dedicated space above nav
- // buttons
+ // If taskbar is in overview, overview action has dedicated space above nav buttons
setPadding(mInsets.left, 0, mInsets.right, 0);
}
- /**
- * Updates vertical margins for different navigation mode or configuration
- * changes.
- */
+ /** Updates vertical margins for different navigation mode or configuration changes. */
public void updateVerticalMargin(NavigationMode mode) {
updateActionBarPosition(mActionButtons);
updateActionBarPosition(mSaveAppPairButton);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
new file mode 100644
index 00000000000..6acaeae8db5
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2025 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.views
+
+import android.os.VibrationAttributes
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.FloatValueHolder
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.launcher3.Flags.enableGridOnlyOverview
+import com.android.launcher3.R
+import com.android.launcher3.Utilities.boundToRange
+import com.android.launcher3.util.DynamicResource
+import com.android.launcher3.util.MSDLPlayerWrapper
+import com.android.quickstep.util.TaskGridNavHelper
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.InteractionProperties
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
+ * RecentsView related to TaskView dismissal.
+ */
+class RecentsDismissUtils(private val recentsView: RecentsView<*, *>) {
+
+ /**
+ * Creates the spring animations which run when a dragged task view in overview is released.
+ *
+ * When a task dismiss is cancelled, the task will return to its original position via a
+ * spring animation. As it passes the threshold of its settling state, its neighbors will spring
+ * in response to the perceived impact of the settling task.
+ */
+ fun createTaskDismissSettlingSpringAnimation(
+ draggedTaskView: TaskView?,
+ velocity: Float,
+ isDismissing: Boolean,
+ dismissLength: Int,
+ onEndRunnable: () -> Unit,
+ ): SpringAnimation? {
+ draggedTaskView ?: return null
+ val taskDismissFloatProperty =
+ FloatPropertyCompat.createFloatPropertyCompat(
+ draggedTaskView.secondaryDismissTranslationProperty
+ )
+ val minVelocity =
+ recentsView.pagedOrientationHandler.getSecondaryDimension(draggedTaskView).toFloat()
+ val startVelocity = abs(velocity).coerceAtLeast(minVelocity) * velocity.sign
+ // Animate dragged task towards dismissal or rest state.
+ val draggedTaskViewSpringAnimation =
+ SpringAnimation(draggedTaskView, taskDismissFloatProperty)
+ .setSpring(createExpressiveDismissSpringForce())
+ .setStartVelocity(startVelocity)
+ .addUpdateListener { animation, value, _ ->
+ if (isDismissing && abs(value) >= abs(dismissLength)) {
+ animation.cancel()
+ } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+ taskDismissFloatProperty.getValue(draggedTaskView)
+ }
+ recentsView.redrawLiveTile()
+ }
+ }
+ .addEndListener { _, _, _, _ ->
+ if (isDismissing) {
+ if (!recentsView.showAsGrid() || enableGridOnlyOverview()) {
+ runTaskGridReflowSpringAnimation(
+ draggedTaskView,
+ getDismissedTaskGapForReflow(draggedTaskView),
+ onEndRunnable,
+ )
+ } else {
+ recentsView.dismissTaskView(
+ draggedTaskView,
+ /* animateTaskView = */ false,
+ /* removeTask = */ true,
+ )
+ onEndRunnable()
+ }
+ } else {
+ recentsView.onDismissAnimationEnds()
+ onEndRunnable()
+ }
+ }
+ if (!isDismissing) {
+ addNeighborSettlingSpringAnimations(
+ draggedTaskView,
+ draggedTaskViewSpringAnimation,
+ driverProgressThreshold = 0f,
+ isSpringDirectionVertical = true,
+ minVelocity = startVelocity,
+ )
+ }
+ return draggedTaskViewSpringAnimation
+ }
+
+ private fun addNeighborSettlingSpringAnimations(
+ draggedTaskView: TaskView,
+ springAnimationDriver: SpringAnimation,
+ tasksToExclude: List = emptyList(),
+ driverProgressThreshold: Float,
+ isSpringDirectionVertical: Boolean,
+ minVelocity: Float,
+ ) {
+ // Empty spring animation exists for conditional start, and to drive neighboring springs.
+ val neighborsToSettle =
+ SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
+
+ // Add tasks before dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ var previousNeighbor = neighborsToSettle
+ getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = true)
+ .filter { (taskView, _) -> !tasksToExclude.contains(taskView) }
+ .forEach { (taskView, offset) ->
+ previousNeighbor =
+ createNeighboringTaskViewSpringAnimation(
+ taskView,
+ offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
+ previousNeighbor,
+ isSpringDirectionVertical,
+ )
+ }
+ // Add tasks after dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ previousNeighbor = neighborsToSettle
+ getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = false)
+ .filter { (taskView, _) -> !tasksToExclude.contains(taskView) }
+ .forEach { (taskView, offset) ->
+ previousNeighbor =
+ createNeighboringTaskViewSpringAnimation(
+ taskView,
+ offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
+ previousNeighbor,
+ isSpringDirectionVertical,
+ )
+ }
+
+ val isCurrentDisplacementAboveOrigin =
+ recentsView.pagedOrientationHandler.isGoingUp(
+ draggedTaskView.secondaryDismissTranslationProperty.get(draggedTaskView),
+ recentsView.isRtl,
+ )
+ addThresholdSpringAnimationTrigger(
+ springAnimationDriver,
+ progressThreshold = driverProgressThreshold,
+ neighborsToSettle,
+ isCurrentDisplacementAboveOrigin,
+ minVelocity,
+ )
+ }
+
+ /** As spring passes threshold for the first time, run conditional spring with velocity. */
+ private fun addThresholdSpringAnimationTrigger(
+ springAnimationDriver: SpringAnimation,
+ progressThreshold: Float,
+ conditionalSpring: SpringAnimation,
+ isCurrentDisplacementAboveOrigin: Boolean,
+ minVelocity: Float,
+ ) {
+ val runSettlingAtVelocity = { velocity: Float ->
+ conditionalSpring.setStartVelocity(velocity).animateToFinalPosition(0f)
+ playDismissSettlingHaptic(velocity)
+ }
+ if (isCurrentDisplacementAboveOrigin) {
+ var lastPosition = 0f
+ var startSettling = false
+ springAnimationDriver.addUpdateListener { _, value, velocity ->
+ // We do not compare to the threshold directly, as the update listener
+ // does not necessarily hit every value. Do not check again once it has started
+ // settling, as a spring can bounce past the end value multiple times.
+ if (startSettling) return@addUpdateListener
+ if (
+ lastPosition < progressThreshold && value >= progressThreshold ||
+ lastPosition > progressThreshold && value <= progressThreshold
+ ) {
+ startSettling = true
+ }
+ lastPosition = value
+ if (startSettling) {
+ runSettlingAtVelocity(velocity)
+ }
+ }
+ } else {
+ // Run settling animations immediately when displacement is already below settled state.
+ runSettlingAtVelocity(minVelocity)
+ }
+ }
+
+ /**
+ * Gets pairs of (TaskView, offset) adjacent the dragged task in visual order.
+ *
+ * Gets tasks either before or after the dragged task along with their offset from it. The
+ * offset is the distance between indices for carousels, or distance between columns for grids.
+ */
+ private fun getTasksOffsetPairAdjacentToDraggedTask(
+ draggedTaskView: TaskView,
+ towardsStart: Boolean,
+ ): Sequence> {
+ if (recentsView.showAsGrid()) {
+ val taskGridNavHelper =
+ TaskGridNavHelper(
+ recentsView.mUtils.getTopRowIdArray(),
+ recentsView.mUtils.getBottomRowIdArray(),
+ recentsView.mUtils.getLargeTaskViewIds(),
+ hasAddDesktopButton = false,
+ )
+ return taskGridNavHelper
+ .gridTaskViewIdOffsetPairInTabOrderSequence(
+ draggedTaskView.taskViewId,
+ towardsStart,
+ )
+ .mapNotNull { (taskViewId, columnOffset) ->
+ recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
+ Pair(taskView, columnOffset)
+ }
+ }
+ } else {
+ val taskViewList = recentsView.mUtils.taskViews.toList()
+ val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
+
+ return if (towardsStart) {
+ taskViewList
+ .take(draggedTaskViewIndex)
+ .reversed()
+ .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
+ .asSequence()
+ } else {
+ taskViewList
+ .takeLast(taskViewList.size - draggedTaskViewIndex - 1)
+ .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
+ .asSequence()
+ }
+ }
+ }
+
+ /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
+ private fun createNeighboringTaskViewSpringAnimation(
+ taskView: TaskView,
+ dampingOffsetRatio: Float,
+ previousNeighborSpringAnimation: SpringAnimation,
+ springingDirectionVertical: Boolean,
+ ): SpringAnimation {
+ val springProperty =
+ if (springingDirectionVertical) taskView.secondaryDismissTranslationProperty
+ else taskView.primaryDismissTranslationProperty
+ val neighboringTaskViewSpringAnimation =
+ SpringAnimation(taskView, FloatPropertyCompat.createFloatPropertyCompat(springProperty))
+ .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio))
+ // Update live tile on spring animation.
+ if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ val taskTranslation =
+ if (springingDirectionVertical) {
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation
+ } else {
+ remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation
+ }
+ taskTranslation.value = springProperty.get(taskView)
+ }
+ recentsView.redrawLiveTile()
+ }
+ }
+ // Drive current neighbor's spring with the previous neighbor's.
+ previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
+ neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
+ }
+ return neighboringTaskViewSpringAnimation
+ }
+
+ private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ return SpringForce()
+ .setDampingRatio(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) +
+ dampingRatioOffset
+ )
+ .setStiffness(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
+ )
+ }
+
+ private fun createExpressiveGridReflowSpringForce(
+ finalPosition: Float = Float.MAX_VALUE
+ ): SpringForce {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ return SpringForce(finalPosition)
+ .setDampingRatio(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_damping_ratio)
+ )
+ .setStiffness(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_stiffness)
+ )
+ }
+
+ /**
+ * Plays a haptic as the dragged task view settles back into its rest state.
+ *
+ * Haptic intensity is proportional to velocity.
+ */
+ private fun playDismissSettlingHaptic(velocity: Float) {
+ val maxDismissSettlingVelocity =
+ recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
+ MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
+ ?.playToken(
+ MSDLToken.CANCEL,
+ InteractionProperties.DynamicVibrationScale(
+ boundToRange(abs(velocity) / maxDismissSettlingVelocity, 0f, 1f),
+ VibrationAttributes.Builder()
+ .setUsage(VibrationAttributes.USAGE_TOUCH)
+ .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
+ .build(),
+ ),
+ )
+ }
+
+ /** Animates RecentsView's scale to the provided value, using spring animations. */
+ fun animateRecentsScale(scale: Float): SpringAnimation {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ val dampingRatio = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio)
+ val stiffness = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_stiffness)
+
+ // Spring which sets the Recents scale on update. This is needed, as the SpringAnimation
+ // struggles to animate small values like changing recents scale from 0.9 to 1. So
+ // we animate over a larger range (e.g. 900 to 1000) and convert back to the required value.
+ // (This is instead of converting RECENTS_SCALE_PROPERTY to a FloatPropertyCompat and
+ // animating it directly via springs.)
+ val initialRecentsScaleSpringValue =
+ RECENTS_SCALE_SPRING_MULTIPLIER * RECENTS_SCALE_PROPERTY.get(recentsView)
+ return SpringAnimation(FloatValueHolder(initialRecentsScaleSpringValue))
+ .setSpring(
+ SpringForce(initialRecentsScaleSpringValue)
+ .setDampingRatio(dampingRatio)
+ .setStiffness(stiffness)
+ )
+ .addUpdateListener { _, value, _ ->
+ RECENTS_SCALE_PROPERTY.setValue(
+ recentsView,
+ value / RECENTS_SCALE_SPRING_MULTIPLIER,
+ )
+ }
+ .apply { animateToFinalPosition(RECENTS_SCALE_SPRING_MULTIPLIER * scale) }
+ }
+
+ /** Animates with springs the TaskViews beyond the dismissed task to fill the gap it left. */
+ private fun runTaskGridReflowSpringAnimation(
+ dismissedTaskView: TaskView,
+ dismissedTaskGap: Float,
+ onEndRunnable: () -> Unit,
+ ) {
+ // Empty spring animation exists for conditional start, and to drive neighboring springs.
+ val springAnimationDriver =
+ SpringAnimation(FloatValueHolder())
+ .setSpring(createExpressiveGridReflowSpringForce(finalPosition = dismissedTaskGap))
+ val towardsStart = if (recentsView.isRtl) dismissedTaskGap < 0 else dismissedTaskGap > 0
+
+ var tasksToReflow: List
+ // Build the chains of Spring Animations
+ when {
+ !recentsView.showAsGrid() -> {
+ tasksToReflow =
+ getTasksToReflow(
+ recentsView.mUtils.taskViews.toList(),
+ dismissedTaskView,
+ towardsStart,
+ )
+ buildDismissReflowSpringAnimationChain(
+ tasksToReflow,
+ dismissedTaskGap,
+ previousSpring = springAnimationDriver,
+ )
+ }
+ dismissedTaskView.isLargeTile -> {
+ tasksToReflow =
+ getTasksToReflow(
+ recentsView.mUtils.getLargeTaskViews(),
+ dismissedTaskView,
+ towardsStart,
+ )
+ val lastSpringAnimation =
+ buildDismissReflowSpringAnimationChain(
+ tasksToReflow,
+ dismissedTaskGap,
+ previousSpring = springAnimationDriver,
+ )
+ // Add all top and bottom grid tasks when animating towards the end of the grid.
+ if (!towardsStart) {
+ tasksToReflow += recentsView.mUtils.getTopRowTaskViews()
+ tasksToReflow += recentsView.mUtils.getBottomRowTaskViews()
+ buildDismissReflowSpringAnimationChain(
+ recentsView.mUtils.getTopRowTaskViews(),
+ dismissedTaskGap,
+ previousSpring = lastSpringAnimation,
+ )
+ buildDismissReflowSpringAnimationChain(
+ recentsView.mUtils.getBottomRowTaskViews(),
+ dismissedTaskGap,
+ previousSpring = lastSpringAnimation,
+ )
+ }
+ }
+ recentsView.isOnGridBottomRow(dismissedTaskView) -> {
+ tasksToReflow =
+ getTasksToReflow(
+ recentsView.mUtils.getBottomRowTaskViews(),
+ dismissedTaskView,
+ towardsStart,
+ )
+ buildDismissReflowSpringAnimationChain(
+ tasksToReflow,
+ dismissedTaskGap,
+ previousSpring = springAnimationDriver,
+ )
+ }
+ else -> {
+ tasksToReflow =
+ getTasksToReflow(
+ recentsView.mUtils.getTopRowTaskViews(),
+ dismissedTaskView,
+ towardsStart,
+ )
+ buildDismissReflowSpringAnimationChain(
+ tasksToReflow,
+ dismissedTaskGap,
+ previousSpring = springAnimationDriver,
+ )
+ }
+ }
+
+ if (tasksToReflow.isNotEmpty()) {
+ addNeighborSettlingSpringAnimations(
+ dismissedTaskView,
+ springAnimationDriver,
+ tasksToExclude = tasksToReflow,
+ driverProgressThreshold = dismissedTaskGap,
+ isSpringDirectionVertical = false,
+ minVelocity = 0f,
+ )
+ } else {
+ springAnimationDriver.addEndListener { _, _, _, _ ->
+ // Play the same haptic as when neighbors spring into place.
+ MSDLPlayerWrapper.INSTANCE.get(recentsView.context)?.playToken(MSDLToken.CANCEL)
+ }
+ }
+
+ // Start animations and remove the dismissed task at the end, dismiss immediately if no
+ // neighboring tasks exist.
+ val runGridEndAnimationAndRelayout = {
+ recentsView.expressiveDismissTaskView(dismissedTaskView, onEndRunnable)
+ }
+ springAnimationDriver?.apply {
+ addEndListener { _, _, _, _ -> runGridEndAnimationAndRelayout() }
+ animateToFinalPosition(dismissedTaskGap)
+ } ?: runGridEndAnimationAndRelayout()
+ }
+
+ private fun getDismissedTaskGapForReflow(dismissedTaskView: TaskView): Float {
+ val screenStart = recentsView.pagedOrientationHandler.getPrimaryScroll(recentsView)
+ val screenEnd =
+ screenStart + recentsView.pagedOrientationHandler.getMeasuredSize(recentsView)
+ val taskStart =
+ recentsView.pagedOrientationHandler.getChildStart(dismissedTaskView) +
+ dismissedTaskView.getOffsetAdjustment(recentsView.showAsGrid())
+ val taskSize =
+ recentsView.pagedOrientationHandler.getMeasuredSize(dismissedTaskView) *
+ dismissedTaskView.getSizeAdjustment(recentsView.showAsFullscreen())
+ val taskEnd = taskStart + taskSize
+
+ val isDismissedTaskBeyondEndOfScreen =
+ if (recentsView.isRtl) taskEnd > screenEnd else taskStart < screenStart
+ if (
+ dismissedTaskView.isLargeTile &&
+ isDismissedTaskBeyondEndOfScreen &&
+ recentsView.mUtils.getLargeTileCount() == 1
+ ) {
+ return with(recentsView) {
+ pagedOrientationHandler.getPrimaryScroll(this) -
+ getScrollForPage(indexOfChild(mUtils.getFirstNonDesktopTaskView()))
+ }
+ .toFloat()
+ }
+
+ // If current page is beyond last TaskView's index, use last TaskView to calculate offset.
+ val lastTaskViewIndex = recentsView.indexOfChild(recentsView.mUtils.getLastTaskView())
+ val currentPage = recentsView.currentPage.coerceAtMost(lastTaskViewIndex)
+ val dismissHorizontalFactor =
+ when {
+ dismissedTaskView.isGridTask -> 1f
+ currentPage == lastTaskViewIndex -> -1f
+ recentsView.indexOfChild(dismissedTaskView) < currentPage -> -1f
+ else -> 1f
+ } * (if (recentsView.isRtl) 1f else -1f)
+
+ return (recentsView.pagedOrientationHandler.getPrimarySize(dismissedTaskView) +
+ recentsView.pageSpacing) * dismissHorizontalFactor
+ }
+
+ private fun getTasksToReflow(
+ taskViews: List,
+ dismissedTaskView: TaskView,
+ towardsStart: Boolean,
+ ): List {
+ val dismissedTaskViewIndex = taskViews.indexOf(dismissedTaskView)
+ if (dismissedTaskViewIndex == -1) {
+ return emptyList()
+ }
+ return if (towardsStart) {
+ taskViews.take(dismissedTaskViewIndex).reversed()
+ } else {
+ taskViews.takeLast(taskViews.size - dismissedTaskViewIndex - 1)
+ }
+ }
+
+ private fun willTaskBeVisibleAfterDismiss(taskView: TaskView, taskTranslation: Int): Boolean {
+ val screenStart = recentsView.pagedOrientationHandler.getPrimaryScroll(recentsView)
+ val screenEnd =
+ screenStart + recentsView.pagedOrientationHandler.getMeasuredSize(recentsView)
+ return recentsView.isTaskViewWithinBounds(
+ taskView,
+ screenStart,
+ screenEnd,
+ /* taskViewTranslation = */ taskTranslation,
+ )
+ }
+
+ /** Builds a chain of spring animations for task reflow after dismissal */
+ private fun buildDismissReflowSpringAnimationChain(
+ taskViews: Iterable,
+ dismissedTaskGap: Float,
+ previousSpring: SpringAnimation,
+ ): SpringAnimation {
+ var lastTaskViewSpring = previousSpring
+ taskViews
+ .filter { taskView ->
+ willTaskBeVisibleAfterDismiss(taskView, dismissedTaskGap.roundToInt())
+ }
+ .forEach { taskView ->
+ val taskViewSpringAnimation =
+ SpringAnimation(
+ taskView,
+ FloatPropertyCompat.createFloatPropertyCompat(
+ taskView.primaryDismissTranslationProperty
+ ),
+ )
+ .setSpring(createExpressiveGridReflowSpringForce(dismissedTaskGap))
+ // Update live tile on spring animation.
+ if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ taskViewSpringAnimation.addUpdateListener { _, _, _ ->
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation.value =
+ taskView.primaryDismissTranslationProperty.get(taskView)
+ }
+ recentsView.redrawLiveTile()
+ }
+ }
+ lastTaskViewSpring.addUpdateListener { _, value, _ ->
+ taskViewSpringAnimation.animateToFinalPosition(value)
+ }
+ lastTaskViewSpring = taskViewSpringAnimation
+ }
+ return lastTaskViewSpring
+ }
+
+ private companion object {
+ // The additional damping to apply to tasks further from the dismissed task.
+ private const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
+ private const val RECENTS_SCALE_SPRING_MULTIPLIER = 1000f
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 163732ff07c..63bb799c97d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -17,6 +17,8 @@
package com.android.quickstep.views;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.Trace.traceBegin;
+import static android.os.Trace.traceEnd;
import static android.view.Surface.ROTATION_0;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
@@ -25,21 +27,25 @@
import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.app.animation.Interpolators.DECELERATE_2;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.app.animation.Interpolators.OVERSHOOT_0_75;
import static com.android.app.animation.Interpolators.clampToProgress;
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
-import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
-import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
+import static com.android.launcher3.Flags.enableDesktopExplodedView;
+import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
+import static com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur;
import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
+import static com.android.launcher3.Flags.enableSeparateExternalDisplayTasks;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
@@ -51,27 +57,26 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
+import static com.android.launcher3.statehandlers.DesktopVisibilityController.INACTIVE_DESK_ID;
import static com.android.launcher3.testing.shared.TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE;
import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
+import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.util.DesksUtils.areMultiDesksFlagsEnabled;
import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP;
import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
-import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SCREEN;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE;
+import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS;
+import static com.android.quickstep.views.TaskView.SPLIT_ALPHA;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -90,6 +95,7 @@
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
@@ -100,6 +106,7 @@
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
+import android.os.Trace;
import android.os.UserHandle;
import android.os.VibrationEffect;
import android.text.Layout;
@@ -125,12 +132,15 @@
import android.widget.ListView;
import android.widget.OverScroller;
import android.widget.Toast;
+import android.window.DesktopModeFlags;
import android.window.PictureInPictureSurfaceTransaction;
+import android.window.TransitionInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.graphics.ColorUtils;
+import androidx.dynamicanimation.animation.SpringAnimation;
import com.android.internal.jank.Cuj;
import com.android.launcher3.AbstractFloatingView;
@@ -138,7 +148,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.Insettable;
-import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
@@ -154,6 +163,7 @@
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StatefulContainer;
@@ -174,8 +184,11 @@
import com.android.launcher3.util.TranslateEdgeEffect;
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.util.ViewPool;
+import com.android.launcher3.util.coroutines.DispatcherProvider;
+import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener;
import com.android.quickstep.BaseContainerInterface;
import com.android.quickstep.GestureState;
+import com.android.quickstep.HighResLoadingState;
import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.RecentsAnimationController;
import com.android.quickstep.RecentsAnimationTargets;
@@ -188,24 +201,31 @@
import com.android.quickstep.SplitSelectionListener;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskThumbnailCache;
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.ViewUtils;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.recents.data.TasksRepository;
+import com.android.quickstep.recents.data.RecentTasksRepository;
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository;
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl;
+import com.android.quickstep.recents.data.RecentsRotationStateRepository;
+import com.android.quickstep.recents.data.RecentsRotationStateRepositoryImpl;
+import com.android.quickstep.recents.di.RecentsDependencies;
import com.android.quickstep.recents.viewmodel.RecentsViewData;
-import com.android.quickstep.util.ActiveGestureErrorDetector;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.recents.viewmodel.RecentsViewModel;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.util.RecentsAtomicAnimationFactory;
import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SingleTask;
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.SplitTask;
import com.android.quickstep.util.SurfaceTransaction;
import com.android.quickstep.util.SurfaceTransactionApplier;
import com.android.quickstep.util.TaskGridNavHelper;
@@ -213,47 +233,55 @@
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.quickstep.util.TransformParams;
import com.android.quickstep.util.VibrationConstants;
-import com.android.quickstep.views.TaskView.TaskContainer;
import com.android.systemui.plugins.ResourceProvider;
import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.PackageManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
-import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.common.pip.IPipAnimationListener;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.GroupedTaskInfo;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
import kotlin.Unit;
+import kotlin.collections.CollectionsKt;
+import kotlin.jvm.functions.Function0;
+
+import kotlinx.coroutines.CoroutineScope;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
-
/**
* A list of recent tasks.
+ *
* @param : the container that should host recents view
- * @param : the type of base state that will be used
+ * @param : the type of base state that will be used
*/
-
-public abstract class RecentsView,
STATE_TYPE extends BaseState> extends PagedView implements Insettable,
- TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
- TaskVisualsChangeListener {
+ HighResLoadingState.HighResLoadingStateChangedCallback,
+ TaskVisualsChangeListener, DesktopVisibilityListener {
- private static final String TAG = "RecentsView";
+ protected static final String TAG = "RecentsView";
private static final boolean DEBUG = false;
- public static final FloatProperty CONTENT_ALPHA =
- new FloatProperty("contentAlpha") {
+ public static final FloatProperty> CONTENT_ALPHA =
+ new FloatProperty<>("contentAlpha") {
@Override
public void setValue(RecentsView view, float v) {
view.setContentAlpha(v);
@@ -265,8 +293,8 @@ public Float get(RecentsView view) {
}
};
- public static final FloatProperty FULLSCREEN_PROGRESS =
- new FloatProperty("fullscreenProgress") {
+ public static final FloatProperty> FULLSCREEN_PROGRESS =
+ new FloatProperty<>("fullscreenProgress") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setFullscreenProgress(v);
@@ -278,8 +306,8 @@ public Float get(RecentsView recentsView) {
}
};
- public static final FloatProperty TASK_MODALNESS =
- new FloatProperty("taskModalness") {
+ public static final FloatProperty> TASK_MODALNESS =
+ new FloatProperty<>("taskModalness") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskModalness(v);
@@ -291,8 +319,8 @@ public Float get(RecentsView recentsView) {
}
};
- public static final FloatProperty ADJACENT_PAGE_HORIZONTAL_OFFSET =
- new FloatProperty("adjacentPageHorizontalOffset") {
+ public static final FloatProperty> ADJACENT_PAGE_HORIZONTAL_OFFSET =
+ new FloatProperty<>("adjacentPageHorizontalOffset") {
@Override
public void setValue(RecentsView recentsView, float v) {
if (recentsView.mAdjacentPageHorizontalOffset != v) {
@@ -307,6 +335,20 @@ public Float get(RecentsView recentsView) {
}
};
+ public static final FloatProperty> RUNNING_TASK_ATTACH_ALPHA =
+ new FloatProperty<>("runningTaskAttachAlpha") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.mRunningTaskAttachAlpha = v;
+ recentsView.applyAttachAlpha();
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mRunningTaskAttachAlpha;
+ }
+ };
+
public static final int SCROLL_VIBRATION_PRIMITIVE =
Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1;
public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f;
@@ -317,10 +359,9 @@ public Float get(RecentsView recentsView) {
/**
* Can be used to tint the color of the RecentsView to simulate a scrim that can views
* excluded from. Really should be a proper scrim.
- * TODO(b/187528071): Remove this and replace with a real scrim.
*/
- private static final FloatProperty COLOR_TINT =
- new FloatProperty("colorTint") {
+ private static final FloatProperty> COLOR_TINT =
+ new FloatProperty<>("colorTint") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setColorTint(v);
@@ -338,8 +379,8 @@ public Float get(RecentsView recentsView) {
* more specific, we'd want to create a similar FloatProperty just for a TaskView's
* offsetX/Y property
*/
- public static final FloatProperty TASK_SECONDARY_TRANSLATION =
- new FloatProperty("taskSecondaryTranslation") {
+ public static final FloatProperty> TASK_SECONDARY_TRANSLATION =
+ new FloatProperty<>("taskSecondaryTranslation") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskViewsResistanceTranslation(v);
@@ -357,8 +398,8 @@ public Float get(RecentsView recentsView) {
* more specific, we'd want to create a similar FloatProperty just for a TaskView's
* offsetX/Y property
*/
- public static final FloatProperty TASK_PRIMARY_SPLIT_TRANSLATION =
- new FloatProperty("taskPrimarySplitTranslation") {
+ public static final FloatProperty> TASK_PRIMARY_SPLIT_TRANSLATION =
+ new FloatProperty<>("taskPrimarySplitTranslation") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskViewsPrimarySplitTranslation(v);
@@ -370,8 +411,8 @@ public Float get(RecentsView recentsView) {
}
};
- public static final FloatProperty TASK_SECONDARY_SPLIT_TRANSLATION =
- new FloatProperty("taskSecondarySplitTranslation") {
+ public static final FloatProperty> TASK_SECONDARY_SPLIT_TRANSLATION =
+ new FloatProperty<>("taskSecondarySplitTranslation") {
@Override
public void setValue(RecentsView recentsView, float v) {
recentsView.setTaskViewsSecondarySplitTranslation(v);
@@ -384,15 +425,12 @@ public Float get(RecentsView recentsView) {
};
/** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
- public static final FloatProperty RECENTS_SCALE_PROPERTY =
- new FloatProperty("recentsScale") {
+ public static final FloatProperty> RECENTS_SCALE_PROPERTY =
+ new FloatProperty<>("recentsScale") {
@Override
public void setValue(RecentsView view, float scale) {
view.setScaleX(scale);
view.setScaleY(scale);
- if (enableRefactorTaskThumbnail()) {
- view.mRecentsViewData.getScale().setValue(scale);
- }
view.mLastComputedTaskStartPushOutDistance = null;
view.mLastComputedTaskEndPushOutDistance = null;
view.runActionOnRemoteHandles(new Consumer() {
@@ -417,8 +455,8 @@ public Float get(RecentsView view) {
* Progress of Recents view from carousel layout to grid layout. If Recents is not shown as a
* grid, then the value remains 0.
*/
- public static final FloatProperty RECENTS_GRID_PROGRESS =
- new FloatProperty("recentsGrid") {
+ public static final FloatProperty> RECENTS_GRID_PROGRESS =
+ new FloatProperty<>("recentsGrid") {
@Override
public void setValue(RecentsView view, float gridProgress) {
view.setGridProgress(gridProgress);
@@ -430,12 +468,27 @@ public Float get(RecentsView view) {
}
};
+ public static final FloatProperty> DESKTOP_CAROUSEL_DETACH_PROGRESS =
+ new FloatProperty<>("desktopCarouselDetachProgress") {
+ @Override
+ public void setValue(RecentsView view, float offset) {
+ view.mDesktopCarouselDetachProgress = offset;
+ view.applyAttachAlpha();
+ view.updatePageOffsets();
+ }
+
+ @Override
+ public Float get(RecentsView view) {
+ return view.mDesktopCarouselDetachProgress;
+ }
+ };
+
/**
* Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and
* being in any other state has a value of 0.
*/
- public static final FloatProperty TASK_THUMBNAIL_SPLASH_ALPHA =
- new FloatProperty("taskThumbnailSplashAlpha") {
+ public static final FloatProperty> TASK_THUMBNAIL_SPLASH_ALPHA =
+ new FloatProperty<>("taskThumbnailSplashAlpha") {
@Override
public void setValue(RecentsView view, float taskThumbnailSplashAlpha) {
view.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
@@ -463,11 +516,8 @@ public Float get(RecentsView view) {
private static final float FOREGROUND_SCRIM_TINT = 0.32f;
- public final RecentsViewData mRecentsViewData = new RecentsViewData();
- public final TasksRepository mTasksRepository;
-
protected final RecentsOrientedState mOrientationState;
- protected final BaseContainerInterface mSizeStrategy;
+ protected final BaseContainerInterface mSizeStrategy;
@Nullable
protected RecentsAnimationController mRecentsAnimationController;
@Nullable
@@ -483,11 +533,9 @@ public Float get(RecentsView view) {
@Nullable
protected RemoteTargetHandle[] mRemoteTargetHandles;
- protected final Rect mLastComputedCarouselTaskSize = new Rect();
protected final Rect mLastComputedTaskSize = new Rect();
protected final Rect mLastComputedGridSize = new Rect();
protected final Rect mLastComputedGridTaskSize = new Rect();
- private TaskView mSelectedTask = null;
// How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
@Nullable
protected Float mLastComputedTaskStartPushOutDistance = null;
@@ -511,19 +559,25 @@ public Float get(RecentsView view) {
private final int mSplitPlaceholderSize;
private final int mSplitPlaceholderInset;
private final ClearAllButton mClearAllButton;
+ @Nullable
+ private AddDesktopButton mAddDesktopButton = null;
private final Rect mClearAllButtonDeadZoneRect = new Rect();
private final Rect mTaskViewDeadZoneRect = new Rect();
+ private final Rect mTopRowDeadZoneRect = new Rect();
+ private final Rect mBottomRowDeadZoneRect = new Rect();
+
+ @Nullable
+ private DesktopVisibilityController mDesktopVisibilityController = null;
+
/**
- * Reflects if Recents is currently in the middle of a gesture, and if so, which tasks are
- * running. If a gesture is not in progress, this will be null.
+ * Reflects if Recents is currently in the middle of a gesture, and if so, which related
+ * [GroupedTaskInfo] is running. If a gesture is not in progress, this will be null.
*/
- private @Nullable Task[] mActiveGestureRunningTasks;
+ private @Nullable GroupedTaskInfo mActiveGestureGroupedTaskInfo;
// Keeps track of the previously known visible tasks for purposes of loading/unloading task data
private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
- private final InvariantDeviceProfile mIdp;
-
/**
* Getting views should be done via {@link #getTaskViewFromPool(int)}
*/
@@ -531,9 +585,11 @@ public Float get(RecentsView view) {
private final ViewPool mGroupedTaskViewPool;
private final ViewPool mDesktopTaskViewPool;
- private final TaskOverlayFactory mTaskOverlayFactory;
+ protected final TaskOverlayFactory mTaskOverlayFactory;
protected boolean mDisallowScrollToClearAll;
+ // True if it is not allowed to scroll to [AddDesktopButton].
+ protected boolean mDisallowScrollToAddDesk;
private boolean mOverlayEnabled;
protected boolean mFreezeViewVisibility;
private boolean mOverviewGridEnabled;
@@ -544,21 +600,22 @@ public Float get(RecentsView view) {
private int mClampedScrollOffsetBound;
private float mAdjacentPageHorizontalOffset = 0;
+ private float mDesktopCarouselDetachProgress = 0;
protected float mTaskViewsSecondaryTranslation = 0;
protected float mTaskViewsPrimarySplitTranslation = 0;
protected float mTaskViewsSecondarySplitTranslation = 0;
// Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
private float mGridProgress = 0;
private float mTaskThumbnailSplashAlpha = 0;
+ private boolean mBorderEnabled = false;
private boolean mShowAsGridLastOnLayout = false;
- private final IntSet mTopRowIdSet = new IntSet();
+ protected final IntSet mTopRowIdSet = new IntSet();
private int mClearAllShortTotalWidthTranslation = 0;
// The GestureEndTarget that is still in progress.
@Nullable
protected GestureState.GestureEndTarget mCurrentGestureEndTarget;
- // TODO(b/187528071): Remove these and replace with a real scrim.
private float mColorTint;
private final int mTintingColor;
@Nullable
@@ -570,6 +627,9 @@ public Float get(RecentsView view) {
private int mKeyboardTaskFocusSnapAnimationDuration;
private int mKeyboardTaskFocusIndex = INVALID_PAGE;
+ private Map mTaskViewsDismissPrimaryTranslations =
+ new HashMap();
+
/**
* TODO: Call reloadIdNeeded in onTaskStackChanged.
*/
@@ -604,25 +664,28 @@ public void onActivityUnpinned() {
@Override
public void onTaskRemoved(int taskId) {
if (!mHandleTaskStackChanges) {
+ Log.d(TAG, "onTaskRemoved: " + taskId + ", not handling task stack changes");
return;
}
- TaskView taskView = getTaskViewByTaskId(taskId);
- if (taskView == null) {
+ TaskContainer taskContainer = mUtils.getTaskContainerById(taskId);
+ if (taskContainer == null) {
+ Log.d(TAG, "onTaskRemoved: " + taskId + ", no associated Task");
return;
}
- Task.TaskKey taskKey = taskView.getFirstTask().key;
+ Log.d(TAG, "onTaskRemoved: " + taskId);
+ TaskKey taskKey = taskContainer.getTask().key;
UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
() -> PackageManagerWrapper.getInstance()
.getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
MAIN_EXECUTOR,
apkRemoved -> {
if (apkRemoved) {
- dismissTask(taskId);
+ dismissTask(taskId, /*animate=*/true, /*removeTask=*/false);
} else {
mModel.isTaskRemoved(taskKey.id, taskRemoved -> {
if (taskRemoved) {
- dismissTask(taskId);
+ dismissTask(taskId, /*animate=*/true, /*removeTask=*/false);
}
}, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter()));
}
@@ -647,12 +710,11 @@ public void onTaskRemoved(int taskId) {
protected int mRunningTaskViewId = -1;
private int mTaskViewIdCount;
protected boolean mRunningTaskTileHidden;
- @Nullable
- private Task[] mTmpRunningTasks;
- protected int mFocusedTaskViewId = -1;
+ protected int mFocusedTaskViewId = INVALID_TASK_ID;
- private boolean mTaskIconScaledDown = false;
+ private boolean mTaskIconVisible = true;
private boolean mRunningTaskShowScreenshot = false;
+ private float mRunningTaskAttachAlpha;
private boolean mOverviewStateEnabled;
private boolean mHandleTaskStackChanges;
@@ -721,10 +783,12 @@ public void onTaskRemoved(int taskId) {
private final SplitSelectionListener mSplitSelectionListener = new SplitSelectionListener() {
@Override
- public void onSplitSelectionConfirmed() { }
+ public void onSplitSelectionConfirmed() {
+ }
@Override
- public void onSplitSelectionActive() { }
+ public void onSplitSelectionActive() {
+ }
@Override
public void onSplitSelectionExit(boolean launchedSplit) {
@@ -762,12 +826,6 @@ public void onSplitSelectionExit(boolean launchedSplit) {
@Nullable
private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
- /**
- * Keeps track of the desktop task. Optional and only present when the feature flag is enabled.
- */
- @Nullable
- private DesktopTaskView mDesktopTaskView;
-
private MultiWindowModeChangedListener mMultiWindowModeChangedListener =
new MultiWindowModeChangedListener() {
@Override
@@ -775,7 +833,7 @@ public void onMultiWindowModeChanged(boolean inMultiWindowMode) {
mOrientationState.setMultiWindowMode(inMultiWindowMode);
setLayoutRotation(mOrientationState.getTouchRotation(),
mOrientationState.getDisplayRotation());
- updateChildTaskOrientations();
+ mUtils.updateChildTaskOrientations();
if (!inMultiWindowMode && mOverviewStateEnabled) {
// TODO: Re-enable layout transitions for addition of the unpinned task
reloadIfNeeded();
@@ -796,40 +854,93 @@ public void onMultiWindowModeChanged(boolean inMultiWindowMode) {
private int mOffsetMidpointIndexOverride = INVALID_PAGE;
- public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- BaseContainerInterface sizeStrategy) {
+ /**
+ * Whether or not any task has been dismissed i.e. swiped away by the user, in the lifetime of
+ * RecentsView being open and displayed to the user. It is reset in the {@link #reset()} method
+ * i.e. when RecentsView closes.
+ */
+ private boolean mAnyTaskHasBeenDismissed;
+
+ protected final RecentsViewModel mRecentsViewModel;
+ private final RecentsViewModelHelper mHelper;
+ protected final RecentsViewUtils mUtils = new RecentsViewUtils(this);
+ protected final RecentsDismissUtils mDismissUtils = new RecentsDismissUtils(this);
+
+ private final Matrix mTmpMatrix = new Matrix();
+
+ private int mTaskViewCount = 0;
+
+ protected final BlurUtils mBlurUtils = new BlurUtils(this);
+
+ @Nullable
+ public TaskView getFirstTaskView() {
+ return mUtils.getFirstTaskView();
+ }
+
+ public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setEnableFreeScroll(true);
- mSizeStrategy = sizeStrategy;
+
mContainer = RecentsViewContainer.containerFromContext(context);
+ mSizeStrategy = getContainerInterface(mContainer.getDisplayId());
+
mOrientationState = new RecentsOrientedState(
context, mSizeStrategy, this::animateRecentsRotationInPlace);
final int rotation;
rotation = Utilities.ATLEAST_R ? mContainer.getDisplay().getRotation() : WindowConfiguration.ROTATION_UNDEFINED;
mOrientationState.setRecentsRotation(rotation);
+ // Start Recents Dependency graph
+ if (enableRefactorTaskThumbnail()) {
+ RecentsDependencies recentsDependencies = RecentsDependencies.Companion.maybeInitialize(
+ context);
+ String scopeId = recentsDependencies.createRecentsViewScope(context);
+ mRecentsViewModel = new RecentsViewModel(
+ recentsDependencies.inject(RecentTasksRepository.class, scopeId),
+ recentsDependencies.inject(RecentsViewData.class, scopeId)
+ );
+ mHelper = new RecentsViewModelHelper(
+ mRecentsViewModel,
+ recentsDependencies.inject(CoroutineScope.class, scopeId),
+ recentsDependencies.inject(DispatcherProvider.class, scopeId)
+ );
+
+ recentsDependencies.provide(RecentsRotationStateRepository.class, scopeId,
+ () -> new RecentsRotationStateRepositoryImpl(mOrientationState));
+
+ recentsDependencies.provide(RecentsDeviceProfileRepository.class, scopeId,
+ () -> new RecentsDeviceProfileRepositoryImpl(mContainer));
+ } else {
+ mRecentsViewModel = null;
+ mHelper = null;
+ }
+
mScrollHapticMinGapMillis = getResources()
.getInteger(R.integer.recentsScrollHapticMinGapMillis);
mFastFlingVelocity = getResources()
.getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
mModel = RecentsModel.INSTANCE.get(context);
- mIdp = InvariantDeviceProfile.INSTANCE.get(context);
- if (enableRefactorTaskThumbnail()) {
- mTasksRepository = new TasksRepository(
- mModel, mModel.getThumbnailCache(), mModel.getIconCache());
- } else {
- mTasksRepository = null;
- }
mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
.inflate(R.layout.overview_clear_all_button, this, false);
mClearAllButton.setOnClickListener(this::dismissAllTasks);
+
+ if (DesktopModeStatus.enableMultipleDesktops(context)) {
+ mAddDesktopButton = (AddDesktopButton) LayoutInflater.from(context).inflate(
+ R.layout.overview_add_desktop_button, this, false);
+ mAddDesktopButton.setOnClickListener(this::createDesk);
+
+ mDesktopVisibilityController = DesktopVisibilityController.INSTANCE.get(context);
+ }
+
mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
10 /* initial size */);
+ int groupedViewPoolInitialSize = enableRefactorTaskThumbnail() ? 2 : 10;
mGroupedTaskViewPool = new ViewPool<>(context, this,
- R.layout.task_grouped, 20 /* max size */, 10 /* initial size */);
+ R.layout.task_grouped, 20 /* max size */, groupedViewPoolInitialSize);
+ int desktopViewPoolInitialSize = DesktopModeStatus.canEnterDesktopMode(context) ? 1 : 0;
mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop,
- 5 /* max size */, 1 /* initial size */);
+ 5 /* max size */, desktopViewPoolInitialSize);
setOrientationHandler(mOrientationState.getOrientationHandler());
mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources());
@@ -849,8 +960,11 @@ public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAt
mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
mEmptyMessagePaint.setTextSize(getResources()
.getDimension(R.dimen.recents_empty_message_text_size));
- mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
- Typeface.NORMAL));
+ Typeface typeface = Typeface.create(
+ Typeface.create(Themes.getDefaultBodyFont(context), Typeface.NORMAL),
+ getFontWeight(),
+ false);
+ mEmptyMessagePaint.setTypeface(typeface);
mEmptyMessagePaint.setAntiAlias(true);
mEmptyMessagePadding = getResources()
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
@@ -1019,15 +1133,20 @@ public int getOverScrollShift() {
@Override
@Nullable
public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
+ if (enableRefactorTaskThumbnail()) {
+ return null;
+ }
if (mHandleTaskStackChanges) {
- TaskView taskView = getTaskViewByTaskId(taskId);
- if (taskView != null) {
- for (TaskContainer container : taskView.getTaskContainers()) {
- if (container == null || taskId != container.getTask().key.id) {
- continue;
+ if (!enableRefactorTaskThumbnail()) {
+ TaskView taskView = getTaskViewByTaskId(taskId);
+ if (taskView != null) {
+ for (TaskContainer container : taskView.getTaskContainers()) {
+ if (taskId != container.getTask().key.id) {
+ continue;
+ }
+ container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
+ thumbnailData);
}
- container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
- thumbnailData);
}
}
}
@@ -1035,15 +1154,15 @@ public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
}
@Override
- public void onTaskIconChanged(String pkg, UserHandle user) {
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView tv = requireTaskViewAt(i);
- Task task = tv.getFirstTask();
- if (pkg.equals(task.key.getPackageName()) && task.key.userId == user.getIdentifier()) {
- task.icon = null;
- if (tv.getTaskContainers().stream().anyMatch(
+ public void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {
+ for (TaskView taskView : getTaskViews()) {
+ Task firstTask = taskView.getFirstTask();
+ if (firstTask != null && pkg.equals(firstTask.key.getPackageName())
+ && firstTask.key.userId == user.getIdentifier()) {
+ firstTask.icon = null;
+ if (taskView.getTaskContainers().stream().anyMatch(
container -> container.getIconView().getDrawable() != null)) {
- tv.onTaskListVisibilityChanged(true /* visible */);
+ taskView.onTaskListVisibilityChanged(true /* visible */);
}
}
}
@@ -1051,42 +1170,36 @@ public void onTaskIconChanged(String pkg, UserHandle user) {
@Override
public void onTaskIconChanged(int taskId) {
+ if (enableRefactorTaskThumbnail()) {
+ return;
+ }
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView != null) {
taskView.refreshTaskThumbnailSplash();
}
}
- /**
- * Update the thumbnail(s) of the relevant TaskView.
- * @param refreshNow Refresh immediately if it's true.
- */
- @Nullable
- public TaskView updateThumbnail(
- HashMap thumbnailData, boolean refreshNow) {
- TaskView updatedTaskView = null;
- for (Map.Entry entry : thumbnailData.entrySet()) {
- Integer id = entry.getKey();
- ThumbnailData thumbnail = entry.getValue();
- TaskView taskView = getTaskViewByTaskId(id);
- if (taskView == null) {
- continue;
- }
- // taskView could be a GroupedTaskView, so select the relevant task by ID
- TaskContainer taskAttributes = taskView.getTaskContainerById(id);
- if (taskAttributes == null) {
- continue;
+ /** Updates the thumbnail(s) of the relevant TaskView. */
+ public void updateThumbnail(Map thumbnailData) {
+ if (!enableRefactorTaskThumbnail()) {
+ for (Map.Entry entry : thumbnailData.entrySet()) {
+ Integer id = entry.getKey();
+ ThumbnailData thumbnail = entry.getValue();
+ TaskView taskView = getTaskViewByTaskId(id);
+ if (taskView == null) {
+ continue;
+ }
+ // taskView could be a GroupedTaskView, so select the relevant task by ID
+ TaskContainer taskContainer = taskView.getTaskContainerById(id);
+ if (taskContainer == null) {
+ continue;
+ }
+ Task task = taskContainer.getTask();
+ TaskThumbnailViewDeprecated taskThumbnailViewDeprecated =
+ taskContainer.getThumbnailViewDeprecated();
+ taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, /*refreshNow=*/false);
}
- Task task = taskAttributes.getTask();
- TaskThumbnailViewDeprecated taskThumbnailViewDeprecated =
- taskAttributes.getThumbnailViewDeprecated();
- taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, refreshNow);
- // thumbnailData can contain 1-2 ids, but they should correspond to the same
- // TaskView, so overwriting is ok
- updatedTaskView = taskView;
}
-
- return updatedTaskView;
}
@Override
@@ -1098,7 +1211,7 @@ protected void onWindowVisibilityChanged(int visibility) {
public void init(OverviewActionsView actionsView, SplitSelectStateController splitController,
@Nullable DesktopRecentsTransitionController desktopRecentsTransitionController) {
mActionsView = actionsView;
- mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+ mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, !hasTaskViews());
// Update flags for 1p/3p launchers
mActionsView.updateFor3pLauncher(!supportsAppPairs());
mSplitSelectStateController = splitController;
@@ -1116,9 +1229,10 @@ public boolean isSplitSelectionActive() {
/**
* See overridden implementations
+ *
* @return {@code true} if child TaskViews can be launched when user taps on them
*/
- protected boolean canLaunchFullscreenTask() {
+ public boolean canLaunchFullscreenTask() {
return true;
}
@@ -1132,20 +1246,22 @@ protected void onAttachedToWindow() {
mSyncTransactionApplier = new SurfaceTransactionApplier(this);
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
.setSyncTransactionApplier(mSyncTransactionApplier));
- RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
+ RecentsModel.INSTANCE.get(mContext).addThumbnailChangeListener(this);
mIPipAnimationListener.setActivityAndRecentsView(mContainer, this);
- SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(
+ SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(
mIPipAnimationListener);
mOrientationState.initListeners();
mTaskOverlayFactory.initListeners();
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
+ mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
+ if (mDesktopVisibilityController != null) {
+ mDesktopVisibilityController.registerDesktopVisibilityListener(this);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
+
updateTaskStackListenerState();
mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
@@ -1154,51 +1270,81 @@ protected void onDetachedFromWindow() {
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
.setSyncTransactionApplier(null));
executeSideTaskLaunchCallback();
- RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
- SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null);
+ RecentsModel.INSTANCE.get(mContext).removeThumbnailChangeListener(this);
+ SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(null);
mIPipAnimationListener.setActivityAndRecentsView(null, null);
mOrientationState.destroyListeners();
mTaskOverlayFactory.removeListeners();
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
+ mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
+ if (mDesktopVisibilityController != null) {
+ mDesktopVisibilityController.unregisterDesktopVisibilityListener(this);
}
reset();
}
+ /**
+ * Execute clean-up logic needed when the view is destroyed.
+ */
+ public void destroy() {
+ Log.d(TAG, "destroy");
+ if (enableRefactorTaskThumbnail()) {
+ try {
+ mTaskViewPool.killOngoingInitializations();
+ mGroupedTaskViewPool.killOngoingInitializations();
+ mDesktopTaskViewPool.killOngoingInitializations();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Ongoing initializations could not be killed", e);
+ }
+ mHelper.onDestroy();
+ RecentsDependencies.destroy(getContext());
+ }
+ }
+
@Override
public void onViewRemoved(View child) {
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.onViewRemoved");
super.onViewRemoved(child);
-
// Clear the task data for the removed child if it was visible unless:
// - It's the initial taskview for entering split screen, we only pretend to dismiss the
// task
// - It's the focused task to be moved to the front, we immediately re-add the task
- if (child instanceof TaskView && child != mSplitHiddenTaskView
- && child != mMovingTaskView) {
- TaskView taskView = (TaskView) child;
- for (int i : taskView.getTaskIds()) {
- mHasVisibleTaskData.delete(i);
- }
- if (child instanceof GroupedTaskView) {
- mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
- } else if (child instanceof DesktopTaskView) {
- mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
- } else {
- mTaskViewPool.recycle(taskView);
+ if (child instanceof TaskView) {
+ mTaskViewCount = Math.max(0, --mTaskViewCount);
+ if (child != mSplitHiddenTaskView && child != mMovingTaskView) {
+ clearAndRecycleTaskView((TaskView) child);
}
- mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
}
+ traceEnd(Trace.TRACE_TAG_APP);
+ }
+
+ private void clearAndRecycleTaskView(TaskView taskView) {
+ for (int i : taskView.getTaskIds()) {
+ mHasVisibleTaskData.delete(i);
+ }
+ if (taskView instanceof GroupedTaskView) {
+ mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
+ } else if (taskView instanceof DesktopTaskView) {
+ mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
+ } else {
+ mTaskViewPool.recycle(taskView);
+ }
+ mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, !hasTaskViews());
}
@Override
public void onViewAdded(View child) {
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.onViewAdded");
super.onViewAdded(child);
+ if (child instanceof TaskView) {
+ mTaskViewCount++;
+ }
child.setAlpha(mContentAlpha);
// RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
// child direction back to match system settings.
child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL);
mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false);
updateEmptyMessage();
+ traceEnd(Trace.TRACE_TAG_APP);
}
@Override
@@ -1277,12 +1423,13 @@ public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
RemoteAnimationTargets targets = params.getTargetSet();
if (targets != null && targets.findTask(taskId) != null) {
launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers,
- targets.nonApps);
+ targets.nonApps, /* transitionInfo= */ null);
}
}
public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps) {
+ RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps,
+ @Nullable TransitionInfo transitionInfo) {
AnimatorSet anim = new AnimatorSet();
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView == null || !isTaskViewVisible(taskView)) {
@@ -1328,22 +1475,35 @@ public void onAnimationStart(Animator animation) {
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- finishRecentsAnimation(false /* toRecents */, null);
+ finishRecentsAnimation(false /* toRecents */, true /*shouldPip*/,
+ allAppsAreTranslucent(apps), null);
}
});
} else {
TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps,
true /* launcherClosing */, getStateManager(), this,
- getDepthController());
+ getDepthController(), transitionInfo);
}
anim.start();
}
+ private boolean allAppsAreTranslucent(RemoteAnimationTarget[] apps) {
+ if (apps == null) {
+ return false;
+ }
+ for (int i = apps.length - 1; i >= 0; --i) {
+ if (!apps[i].isTranslucent) {
+ return false;
+ }
+ }
+ return true;
+ }
+
public boolean isTaskViewVisible(TaskView tv) {
if (showAsGrid()) {
int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
- return isTaskViewWithinBounds(tv, screenStart, screenEnd);
+ return isTaskViewWithinBounds(tv, screenStart, screenEnd, /*taskViewTranslation=*/ 0);
} else {
// For now, just check if it's the active task or an adjacent task
return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
@@ -1363,7 +1523,7 @@ public boolean isTaskViewFullyVisible(TaskView tv) {
@Nullable
private TaskView getLastGridTaskView() {
- return getLastGridTaskView(getTopRowIdArray(), getBottomRowIdArray());
+ return getLastGridTaskView(mUtils.getTopRowIdArray(), mUtils.getBottomRowIdArray());
}
@Nullable
@@ -1390,14 +1550,41 @@ private int getLastTaskScroll(int clearAllScroll, int clearAllWidth) {
return clearAllScroll + (mIsRtl ? distance : -distance);
}
- private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
- int taskStart = getPagedOrientationHandler().getChildStart(tv)
- + (int) tv.getOffsetAdjustment(showAsGrid());
- int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(tv)
- * tv.getSizeAdjustment(showAsFullscreen()));
+ /**
+ * Launch running task view if it is instance of DesktopTaskView.
+ * @return provides runnable list to attach runnable at end of Desktop Mode launch
+ */
+ @Nullable
+ public RunnableList launchRunningDesktopTaskView() {
+ TaskView taskView = getRunningTaskView();
+ if (taskView instanceof DesktopTaskView) {
+ return taskView.launchWithAnimation();
+ }
+ return null;
+ }
+
+ /*
+ * Returns if TaskView is within screen bounds defined in [screenStart, screenEnd].
+ *
+ * @param taskViewTranslation taskView is considered within bounds if either translated or
+ * original position of taskView is within screen bounds.
+ */
+ protected boolean isTaskViewWithinBounds(TaskView taskView, int screenStart, int screenEnd,
+ int taskViewTranslation) {
+ int taskStart = getPagedOrientationHandler().getChildStart(taskView)
+ + (int) taskView.getOffsetAdjustment(showAsGrid());
+ int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(taskView)
+ * taskView.getSizeAdjustment(showAsFullscreen()));
int taskEnd = taskStart + taskSize;
- return (taskStart >= start && taskStart <= end) || (taskEnd >= start
- && taskEnd <= end);
+
+ int translatedTaskStart = taskStart + taskViewTranslation;
+ int translatedTaskEnd = taskEnd + taskViewTranslation;
+
+ taskStart = Math.min(taskStart, translatedTaskStart);
+ taskEnd = Math.max(taskEnd, translatedTaskEnd);
+
+ return (taskStart >= screenStart && taskStart <= screenEnd) || (taskEnd >= screenStart
+ && taskEnd <= screenEnd);
}
private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
@@ -1410,17 +1597,19 @@ private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
}
/**
- * Returns true if the task is in expected scroll position.
- *
- * @param taskIndex the index of the task
+ * Returns true if the given TaskView is in expected scroll position.
*/
- public boolean isTaskInExpectedScrollPosition(int taskIndex) {
- return getScrollForPage(taskIndex) == getPagedOrientationHandler().getPrimaryScroll(this);
+ public boolean isTaskInExpectedScrollPosition(@NonNull TaskView taskView) {
+ return getScrollForPage(indexOfChild(taskView))
+ == getPagedOrientationHandler().getPrimaryScroll(this);
}
- private boolean isFocusedTaskInExpectedScrollPosition() {
+ /**
+ * Returns true if the focused TaskView is in expected scroll position.
+ */
+ public boolean isFocusedTaskInExpectedScrollPosition() {
TaskView focusedTask = getFocusedTaskView();
- return focusedTask != null && isTaskInExpectedScrollPosition(indexOfChild(focusedTask));
+ return focusedTask != null && isTaskInExpectedScrollPosition(focusedTask);
}
/**
@@ -1432,8 +1621,7 @@ public TaskView getTaskViewByTaskId(int taskId) {
return null;
}
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView taskView = requireTaskViewAt(i);
+ for (TaskView taskView : getTaskViews()) {
if (taskView.containsTaskId(taskId)) {
return taskView;
}
@@ -1454,8 +1642,7 @@ public TaskView getTaskViewByTaskIds(int[] taskIds) {
int[] taskIdsCopy = Arrays.copyOf(taskIds, taskIds.length);
Arrays.sort(taskIdsCopy);
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView taskView = requireTaskViewAt(i);
+ for (TaskView taskView : getTaskViews()) {
int[] taskViewIdsCopy = taskView.getTaskIds();
Arrays.sort(taskViewIdsCopy);
if (Arrays.equals(taskIdsCopy, taskViewIdsCopy)) {
@@ -1477,9 +1664,6 @@ public void setOverviewStateEnabled(boolean enabled) {
updateTaskStackListenerState();
mOrientationState.setRotationWatcherEnabled(enabled);
if (!enabled) {
- // Reset the running task when leaving overview since it can still have a reference to
- // its thumbnail
- mTmpRunningTasks = null;
mSplitBoundsConfig = null;
mTaskOverlayFactory.clearAllActiveState();
}
@@ -1490,12 +1674,14 @@ public void setOverviewStateEnabled(boolean enabled) {
* Enable or disable showing border on hover and focus change on task views
*/
public void setTaskBorderEnabled(boolean enabled) {
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
+ mBorderEnabled = enabled;
+ for (TaskView taskView : getTaskViews()) {
taskView.setBorderEnabled(enabled);
}
mClearAllButton.setBorderEnabled(enabled);
+ if (mAddDesktopButton != null) {
+ mAddDesktopButton.setBorderEnabled(enabled);
+ }
}
/**
@@ -1523,8 +1709,7 @@ protected void onPageBeginTransition() {
@Override
protected void onPageEndTransition() {
super.onPageEndTransition();
- ActiveGestureLog.INSTANCE.addLog(
- "onPageEndTransition: current page index updated", getNextPage());
+ ActiveGestureProtoLogProxy.logOnPageEndTransition(getNextPage());
if (isClearAllHidden() && !mContainer.getDeviceProfile().isTablet) {
mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
}
@@ -1559,9 +1744,7 @@ public boolean onTouchEvent(MotionEvent ev) {
super.onTouchEvent(ev);
if (showAsGrid()) {
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
+ for (TaskView taskView : getTaskViews()) {
if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
// Keep consuming events to pass to delegate
return true;
@@ -1607,8 +1790,11 @@ public boolean onTouchEvent(MotionEvent ev) {
mClearAllButton.getAlpha() == 1
&& mClearAllButtonDeadZoneRect.contains(x, y);
final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+ int adjustedX = x + getScrollX();
if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
- && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
+ && !mTaskViewDeadZoneRect.contains(adjustedX, y)
+ && !mTopRowDeadZoneRect.contains(adjustedX, y)
+ && !mBottomRowDeadZoneRect.contains(adjustedX, y)) {
mTouchDownToStartHome = true;
}
}
@@ -1643,11 +1829,11 @@ protected void onNotSnappingToPageInFreeScroll() {
return;
}
TaskView taskView = getTaskViewAt(mNextPage);
- // Snap to fully visible focused task and clear all button.
- boolean shouldSnapToFocusedTask = taskView != null && taskView.isFocusedTask()
- && isTaskViewFullyVisible(taskView);
+ boolean shouldSnapToLargeTask = taskView != null && taskView.isLargeTile()
+ && !mUtils.isAnySmallTaskFullyVisible();
boolean shouldSnapToClearAll = mNextPage == indexOfChild(mClearAllButton);
- if (!shouldSnapToFocusedTask && !shouldSnapToClearAll) {
+ // Snap to large tile when grid tasks aren't fully visible or the clear all button.
+ if (!shouldSnapToLargeTask && !shouldSnapToClearAll) {
return;
}
}
@@ -1690,24 +1876,17 @@ protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
}
/**
- * Moves the running task to the front of the carousel in tablets, to minimize animation
- * required to move the running task in grid.
+ * Moves the running task to the expected position in the carousel. In tablets, this minimize
+ * animation required to move the running task into focused task position.
*/
- public void moveRunningTaskToFront() {
- if (!mContainer.getDeviceProfile().isTablet) {
- return;
- }
-
+ public void moveRunningTaskToExpectedPosition() {
TaskView runningTaskView = getRunningTaskView();
- if (runningTaskView == null) {
- return;
- }
-
- if (indexOfChild(runningTaskView) != mCurrentPage) {
+ if (runningTaskView == null || mCurrentPage != indexOfChild(runningTaskView)) {
return;
}
- if (mCurrentPage == 0) {
+ int runningTaskExpectedIndex = mUtils.getRunningTaskExpectedIndex(runningTaskView);
+ if (mCurrentPage == runningTaskExpectedIndex) {
return;
}
@@ -1719,16 +1898,16 @@ public void moveRunningTaskToFront() {
removeView(runningTaskView);
mMovingTaskView = null;
runningTaskView.resetPersistentViewTransforms();
- addView(runningTaskView, 0);
- setCurrentPage(0);
+
+ addView(runningTaskView, runningTaskExpectedIndex);
+ setCurrentPage(runningTaskExpectedIndex);
updateTaskSize();
}
@Override
protected void onScrollerAnimationAborted() {
- ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
- ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+ ActiveGestureProtoLogProxy.logOnScrollerAnimationAborted();
}
@Override
@@ -1738,7 +1917,8 @@ protected boolean isPageScrollsInitialized() {
protected void applyLoadPlan(List taskGroups) {
if (mPendingAnimation != null) {
- mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups));
+ final List finalTaskGroups = taskGroups;
+ mPendingAnimation.addEndListener(success -> applyLoadPlan(finalTaskGroups));
return;
}
@@ -1746,11 +1926,11 @@ protected void applyLoadPlan(List taskGroups) {
Log.d(TAG, "applyLoadPlan - taskGroups is null");
} else {
Log.d(TAG, "applyLoadPlan - taskGroups: " + taskGroups.stream().map(
- GroupTask::toString).collect(Collectors.toList()));
+ GroupTask::toString).toList());
}
mLoadPlanEverApplied = true;
if (taskGroups == null || taskGroups.isEmpty()) {
- removeTasksViewsAndClearAllButton();
+ removeAllTaskViews();
onTaskStackUpdated();
// With all tasks removed, touch handling in PagedView is disabled and we need to reset
// touch state or otherwise values will be obsolete.
@@ -1761,12 +1941,19 @@ protected void applyLoadPlan(List taskGroups) {
return;
}
- int[] currentTaskIds;
+ // Start here to avoid early returns and empty cases which have special logic
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan");
+
TaskView currentTaskView = getTaskViewAt(mCurrentPage);
- if (currentTaskView != null) {
+ int[] currentTaskIds = null;
+ // Track the current DesktopTaskView through [deskId] as a desk can be empty without any
+ // tasks.
+ int currentTaskViewDeskId = INACTIVE_DESK_ID;
+ if (areMultiDesksFlagsEnabled()
+ && currentTaskView instanceof DesktopTaskView desktopTaskView) {
+ currentTaskViewDeskId = desktopTaskView.getDeskId();
+ } else if (currentTaskView != null) {
currentTaskIds = currentTaskView.getTaskIds();
- } else {
- currentTaskIds = new int[0];
}
// Unload existing visible task data
@@ -1778,19 +1965,33 @@ protected void applyLoadPlan(List taskGroups) {
// Save running task ID if it exists before rebinding all taskViews, otherwise the task from
// the runningTaskView currently bound could get assigned to another TaskView
- int[] runningTaskIds = getTaskIdsForTaskViewId(mRunningTaskViewId);
- int[] focusedTaskIds = getTaskIdsForTaskViewId(mFocusedTaskViewId);
+ TaskView runningTaskView = getRunningTaskView();
+ int[] runningTaskIds = null;
+
+ // Track the running TaskView through [deskId] as a desk can be empty without any tasks.
+ int runningTaskViewDeskId = INACTIVE_DESK_ID;
+ if (areMultiDesksFlagsEnabled()
+ && runningTaskView instanceof DesktopTaskView desktopTaskView) {
+ runningTaskViewDeskId = desktopTaskView.getDeskId();
+ } else if (runningTaskView != null) {
+ runningTaskIds = runningTaskView.getTaskIds();
+ }
+ int[] focusedTaskIds = getTaskIdsForTaskViewId(mFocusedTaskViewId);
// Reset the focused task to avoiding initializing TaskViews layout as focused task during
// binding. The focused task view will be updated after all the TaskViews are bound.
- mFocusedTaskViewId = INVALID_TASK_ID;
+ setFocusedTaskViewId(INVALID_TASK_ID);
// Removing views sets the currentPage to 0, so we save this and restore it after
// the new set of views are added
int previousCurrentPage = mCurrentPage;
int previousFocusedPage = indexOfChild(getFocusedChild());
+ // TaskIds will no longer be valid after remove and re-add, clearing mTopRowIdSet.
+ mAnyTaskHasBeenDismissed = false;
+ mTopRowIdSet.clear();
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.removeAllViews");
removeAllViews();
-
+ traceEnd(Trace.TRACE_TAG_APP);
// If we are entering Overview as a result of initiating a split from somewhere else
// (e.g. split from Home), we need to make sure the staged app is not drawn as a thumbnail.
int stagedTaskIdToBeRemoved;
@@ -1804,81 +2005,115 @@ protected void applyLoadPlan(List taskGroups) {
mFilterState.updateInstanceCountMap(taskGroups);
// Clear out desktop view if it is set
- mDesktopTaskView = null;
+
+ // Move Desktop Tasks to the end of the list
+ if (enableLargeDesktopWindowingTile()) {
+ taskGroups = mUtils.sortDesktopTasksToFront(taskGroups);
+ }
+ if (enableSeparateExternalDisplayTasks()) {
+ taskGroups = mUtils.sortExternalDisplayTasksToFront(taskGroups);
+ }
+
+ if (mAddDesktopButton != null) {
+ // Add `mAddDesktopButton` as the first child.
+ addView(mAddDesktopButton);
+ }
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop");
// Add views as children based on whether it's grouped or single task. Looping through
// taskGroups backwards populates the thumbnail grid from least recent to most recent.
for (int i = taskGroups.size() - 1; i >= 0; i--) {
GroupTask groupTask = taskGroups.get(i);
- boolean isRemovalNeeded = stagedTaskIdToBeRemoved != INVALID_TASK_ID
+ boolean containsStagedTask = stagedTaskIdToBeRemoved != INVALID_TASK_ID
&& groupTask.containsTask(stagedTaskIdToBeRemoved);
+ boolean shouldSkipGroupTask = containsStagedTask && groupTask instanceof SingleTask;
- if (isRemovalNeeded && !groupTask.hasMultipleTasks()) {
- // If the task we need to remove is not part of a pair, avoiding creating the
- // TaskView.
+ if ((isSplitSelectionActive() && groupTask.taskViewType == TaskViewType.DESKTOP)
+ || shouldSkipGroupTask) {
+ // To avoid these tasks from being chosen as the app pair, the creation of a
+ // TaskView is bypassed. The staged task is already selected for the app pair,
+ // and the Desktop task should be hidden when selecting a pair.
continue;
}
// If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE
// to be a temporary container for the remaining task.
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.createTaskView");
TaskView taskView = getTaskViewFromPool(
- isRemovalNeeded ? TaskView.Type.SINGLE : groupTask.taskViewType);
- if (taskView instanceof GroupedTaskView) {
- boolean firstTaskIsLeftTopTask =
- groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
- Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
- Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
- ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
- mTaskOverlayFactory, groupTask.mSplitBounds);
- } else if (taskView instanceof DesktopTaskView) {
- ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks,
- mOrientationState, mTaskOverlayFactory);
- mDesktopTaskView = (DesktopTaskView) taskView;
- } else {
- Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
- : groupTask.task1;
+ containsStagedTask ? TaskViewType.SINGLE : groupTask.taskViewType);
+ traceEnd(Trace.TRACE_TAG_APP);
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.bind");
+ if (taskView instanceof GroupedTaskView groupedTaskView) {
+ var splitTask = (SplitTask) groupTask;
+ groupedTaskView.bind(splitTask.getTopLeftTask(),
+ splitTask.getBottomRightTask(), mOrientationState,
+ mTaskOverlayFactory, splitTask.getSplitBounds());
+ } else if (taskView instanceof DesktopTaskView desktopTaskView) {
+ desktopTaskView.bind((DesktopTask) groupTask, mOrientationState,
+ mTaskOverlayFactory);
+ } else if (groupTask instanceof SplitTask splitTask) {
+ Task task = splitTask.getTopLeftTask().key.id == stagedTaskIdToBeRemoved
+ ? splitTask.getBottomRightTask()
+ : splitTask.getTopLeftTask();
taskView.bind(task, mOrientationState, mTaskOverlayFactory);
+ } else {
+ taskView.bind(((SingleTask) groupTask).getTask(), mOrientationState,
+ mTaskOverlayFactory);
}
+ traceEnd(Trace.TRACE_TAG_APP);
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.addTaskView");
addView(taskView);
+ traceEnd(Trace.TRACE_TAG_APP);
// enables instance filtering if the feature flag for it is on
if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) {
taskView.setUpShowAllInstancesListener();
}
}
+ // For loop end trace
+ traceEnd(Trace.TRACE_TAG_APP);
- if (!taskGroups.isEmpty()) {
- addView(mClearAllButton);
- }
+ addView(mClearAllButton);
// Keep same previous focused task
- TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
- // If the list changed, maybe the focused task doesn't exist anymore
- if (newFocusedTaskView == null && getTaskViewCount() > 0) {
- newFocusedTaskView = getTaskViewAt(0);
+ TaskView newFocusedTaskView = null;
+ if (!enableGridOnlyOverview()) {
+ newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
+ if (enableLargeDesktopWindowingTile()
+ && newFocusedTaskView instanceof DesktopTaskView) {
+ newFocusedTaskView = null;
+ }
+ // If the list changed, maybe the focused task doesn't exist anymore.
+ if (newFocusedTaskView == null) {
+ newFocusedTaskView = mUtils.getFirstNonDesktopTaskView();
+ }
}
- mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview()
- ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID;
+ setFocusedTaskViewId(
+ newFocusedTaskView != null ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID);
+
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.layouts");
updateTaskSize();
- updateChildTaskOrientations();
+ mUtils.updateChildTaskOrientations();
+ traceEnd(Trace.TRACE_TAG_APP);
- TaskView newRunningTaskView = null;
- if (hasAllValidTaskIds(runningTaskIds)) {
+ TaskView newRunningTaskView = mUtils.getDesktopTaskViewForDeskId(runningTaskViewDeskId);
+ if (newRunningTaskView == null) {
// Update mRunningTaskViewId to be the new TaskView that was assigned by binding
// the full list of tasks to taskViews
newRunningTaskView = getTaskViewByTaskIds(runningTaskIds);
- if (newRunningTaskView != null) {
- setRunningTaskViewId(newRunningTaskView.getTaskViewId());
+ }
+ if (newRunningTaskView != null) {
+ setRunningTaskViewId(newRunningTaskView.getTaskViewId());
+ } else {
+ if (mActiveGestureGroupedTaskInfo != null) {
+ // This will update mRunningTaskViewId and create a stub view if necessary.
+ // We try to avoid this because it can cause a scroll jump, but it is needed
+ // for cases where the running task isn't included in this load plan (e.g. if
+ // the current running task is excludedFromRecents.)
+ showCurrentTask(mActiveGestureGroupedTaskInfo, "applyLoadPlan");
+ newRunningTaskView = getRunningTaskView();
} else {
- if (mActiveGestureRunningTasks != null) {
- // This will update mRunningTaskViewId and create a stub view if necessary.
- // We try to avoid this because it can cause a scroll jump, but it is needed
- // for cases where the running task isn't included in this load plan (e.g. if
- // the current running task is excludedFromRecents.)
- showCurrentTask(mActiveGestureRunningTasks);
- } else {
- setRunningTaskViewId(INVALID_TASK_ID);
- }
+ setRunningTaskViewId(INVALID_TASK_ID);
}
}
@@ -1886,21 +2121,18 @@ protected void applyLoadPlan(List taskGroups) {
if (mNextPage != INVALID_PAGE) {
// Restore mCurrentPage but don't call setCurrentPage() as that clobbers the scroll.
mCurrentPage = previousCurrentPage;
- if (hasAllValidTaskIds(currentTaskIds)) {
+ currentTaskView = mUtils.getDesktopTaskViewForDeskId(currentTaskViewDeskId);
+ if (currentTaskView == null) {
currentTaskView = getTaskViewByTaskIds(currentTaskIds);
- if (currentTaskView != null) {
- targetPage = indexOfChild(currentTaskView);
- }
+ }
+ if (currentTaskView != null) {
+ targetPage = indexOfChild(currentTaskView);
}
} else if (previousFocusedPage != INVALID_PAGE) {
targetPage = previousFocusedPage;
} else {
- // Set the current page to the running task, but not if settling on new task.
- if (hasAllValidTaskIds(runningTaskIds)) {
- targetPage = indexOfChild(newRunningTaskView);
- } else if (getTaskViewCount() > 0) {
- targetPage = indexOfChild(requireTaskViewAt(0));
- }
+ targetPage = indexOfChild(
+ mUtils.getExpectedCurrentTask(newRunningTaskView, newFocusedTaskView));
}
if (targetPage != -1 && mCurrentPage != targetPage) {
int finalTargetPage = targetPage;
@@ -1917,6 +2149,7 @@ protected void applyLoadPlan(List taskGroups) {
});
}
+ traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.cleanupStates");
if (mIgnoreResetTaskId != INVALID_TASK_ID &&
getTaskViewByTaskId(mIgnoreResetTaskId) != ignoreResetTaskView) {
// If the taskView mapping is changing, do not preserve the visuals. Since we are
@@ -1924,12 +2157,17 @@ protected void applyLoadPlan(List taskGroups) {
// generally map to the same task.
mIgnoreResetTaskId = INVALID_TASK_ID;
}
+
resetTaskVisuals();
onTaskStackUpdated();
updateEnabledOverlays();
if (isPageScrollsInitialized()) {
onPageScrollsInitialized();
}
+ traceEnd(Trace.TRACE_TAG_APP);
+
+ // applyLoadPlan end trace
+ traceEnd(Trace.TRACE_TAG_APP);
}
private boolean isModal() {
@@ -1940,38 +2178,30 @@ public boolean isLoadingTasks() {
return mModel.isLoadingTasksInBackground();
}
- private void removeTasksViewsAndClearAllButton() {
- // This handles an edge case where applyLoadPlan happens during a gesture when the
- // only Task is one with excludeFromRecents, in which case we should not remove it.
- final int stubRunningTaskIndex = isGestureActive() ? getRunningTaskIndex() : -1;
-
- for (int i = getTaskViewCount() - 1; i >= 0; i--) {
- if (i == stubRunningTaskIndex) {
- continue;
- }
- removeView(requireTaskViewAt(i));
- }
- if (getTaskViewCount() == 0 && indexOfChild(mClearAllButton) != -1) {
+ private void removeAllTaskViews() {
+ // This handles an edge case where applyLoadPlan happens during a gesture when the only
+ // Task is one with excludeFromRecents, in which case we should not remove it.
+ CollectionsKt
+ .filter(getTaskViews(), taskView -> !isGestureActive() || !taskView.isRunningTask())
+ .forEach(this::removeView);
+ if (!hasTaskViews()) {
+ removeView(mAddDesktopButton);
removeView(mClearAllButton);
}
}
+ /** Returns true if there are at least one TaskView has been added to the RecentsView. */
+ public boolean hasTaskViews() {
+ return mUtils.hasTaskViews();
+ }
+
public int getTaskViewCount() {
- int taskViewCount = getChildCount();
- if (indexOfChild(mClearAllButton) != -1) {
- taskViewCount--;
- }
- return taskViewCount;
+ return mTaskViewCount;
}
- public int getGroupedTaskViewCount() {
- int groupViewCount = 0;
- for (int i = 0; i < getChildCount(); i++) {
- if (getChildAt(i) instanceof GroupedTaskView) {
- groupViewCount++;
- }
- }
- return groupViewCount;
+ /** Counts {@link TaskView}s that are not {@link DesktopTaskView} instances. */
+ public int getNonDesktopTaskViewCount() {
+ return mUtils.getNonDesktopTaskViewCount();
}
/**
@@ -1994,16 +2224,16 @@ protected void onTaskStackUpdated() {
}
public void resetTaskVisuals() {
- for (int i = getTaskViewCount() - 1; i >= 0; i--) {
- TaskView taskView = requireTaskViewAt(i);
+ for (TaskView taskView : getTaskViews()) {
if (Arrays.stream(taskView.getTaskIds()).noneMatch(
taskId -> taskId == mIgnoreResetTaskId)) {
taskView.resetViewTransforms();
- taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+ taskView.setIconVisibleForGesture(mTaskIconVisible);
taskView.setStableAlpha(mContentAlpha);
taskView.setFullscreenProgress(mFullscreenProgress);
taskView.setModalness(mTaskModalness);
taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha);
+ taskView.setBorderEnabled(mBorderEnabled);
}
}
// resetTaskVisuals is called at the end of dismiss animation which could update
@@ -2016,14 +2246,10 @@ public void resetTaskVisuals() {
simulator.fullScreenProgress.value = 0;
simulator.recentsViewScale.value = 1;
});
- // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
- // null.
- if (!mRunningTaskShowScreenshot) {
- setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
- }
- if (mRunningTaskTileHidden) {
- setRunningTaskHidden(mRunningTaskTileHidden);
- }
+ // Reapply runningTask related attributes as they might have been reset by
+ // resetViewTransforms().
+ setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
+ applyAttachAlpha();
updateCurveProperties();
// Update the set of visible task's data
@@ -2034,12 +2260,8 @@ public void resetTaskVisuals() {
public void setFullscreenProgress(float fullscreenProgress) {
mFullscreenProgress = fullscreenProgress;
- if (enableRefactorTaskThumbnail()) {
- mRecentsViewData.getFullscreenProgress().setValue(mFullscreenProgress);
- }
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setFullscreenProgress(mFullscreenProgress);
}
mClearAllButton.setFullscreenProgress(fullscreenProgress);
@@ -2052,6 +2274,7 @@ private void updateTaskStackListenerState() {
boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
&& getWindowVisibility() == VISIBLE;
if (handleTaskStackChanges != mHandleTaskStackChanges) {
+ Log.d(TAG, "updateTaskStackListenerState: " + handleTaskStackChanges);
mHandleTaskStackChanges = handleTaskStackChanges;
if (handleTaskStackChanges) {
reloadIfNeeded();
@@ -2122,7 +2345,7 @@ private void updateOrientationHandler(boolean forceRecreateDragLayerControllers)
updateSizeAndPadding();
// Update TaskView's DeviceProfile dependent layout.
- updateChildTaskOrientations();
+ mUtils.updateChildTaskOrientations();
requestLayout();
// Reapply the current page to update page scrolls.
@@ -2153,11 +2376,6 @@ private void updateSizeAndPadding() {
mSizeStrategy.calculateGridTaskSize(mContainer, dp, mLastComputedGridTaskSize,
getPagedOrientationHandler());
- if (enableGridOnlyOverview()) {
- mSizeStrategy.calculateCarouselTaskSize(mContainer, dp, mLastComputedCarouselTaskSize,
- getPagedOrientationHandler());
- }
-
mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
mTopBottomRowHeightDiff =
mLastComputedGridTaskSize.height() + dp.overviewTaskThumbnailTopMarginPx
@@ -2172,33 +2390,14 @@ private void updateSizeAndPadding() {
* Updates TaskView scaling and translation required to support variable width.
*/
private void updateTaskSize() {
- updateTaskSize(false);
- }
-
- /**
- * Updates TaskView scaling and translation required to support variable width.
- *
- * @param isTaskDismissal indicates if update was called due to task dismissal
- */
- private void updateTaskSize(boolean isTaskDismissal) {
- final int taskCount = getTaskViewCount();
- if (taskCount == 0) {
+ if (!hasTaskViews()) {
return;
}
float accumulatedTranslationX = 0;
- float translateXToMiddle = 0;
- if (enableGridOnlyOverview() && mContainer.getDeviceProfile().isTablet) {
- translateXToMiddle = mIsRtl
- ? mLastComputedCarouselTaskSize.right - mLastComputedTaskSize.right
- : mLastComputedCarouselTaskSize.left - mLastComputedTaskSize.left;
- }
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
- taskView.updateTaskSize(mLastComputedTaskSize, mLastComputedGridTaskSize,
- mLastComputedCarouselTaskSize);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.updateTaskSize(mLastComputedTaskSize, mLastComputedGridTaskSize);
taskView.setNonGridTranslationX(accumulatedTranslationX);
- taskView.setNonGridPivotTranslationX(translateXToMiddle);
// Compensate space caused by TaskView scaling.
float widthDiff =
taskView.getLayoutParams().width * (1 - taskView.getNonGridScale());
@@ -2207,7 +2406,13 @@ private void updateTaskSize(boolean isTaskDismissal) {
mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
- updateGridProperties(isTaskDismissal);
+ float taskAlignmentTranslationY = getTaskAlignmentTranslationY();
+ mClearAllButton.setTaskAlignmentTranslationY(taskAlignmentTranslationY);
+ if (mAddDesktopButton != null) {
+ mAddDesktopButton.setTranslationY(taskAlignmentTranslationY);
+ }
+
+ updateGridProperties();
}
public void getTaskSize(Rect outRect) {
@@ -2216,28 +2421,50 @@ public void getTaskSize(Rect outRect) {
}
/**
- * Sets the last TaskView selected.
+ * Returns the currently selected TaskView in Select mode.
+ */
+ @Nullable
+ public TaskView getSelectedTaskView() {
+ return mUtils.getSelectedTaskView();
+ }
+
+ /**
+ * Sets the selected TaskView in Select mode.
*/
public void setSelectedTask(int lastSelectedTaskId) {
- mSelectedTask = getTaskViewByTaskId(lastSelectedTaskId);
+ mUtils.setSelectedTaskView(getTaskViewByTaskId(lastSelectedTaskId));
}
/**
* Returns the bounds of the task selected to enter modal state.
*/
public Rect getSelectedTaskBounds() {
- if (mSelectedTask == null) {
+ if (getSelectedTaskView() == null) {
return mLastComputedTaskSize;
}
- return getTaskBounds(mSelectedTask);
+ return getTaskBounds(getSelectedTaskView());
+ }
+
+ /**
+ * Get the Y translation that should be applied to the non-TaskView item inside the RecentsView
+ * (ClearAllButton and AddDesktopButton) in the original layout position, before scrolling. This
+ * is done to make sure the button is aligned to the middle of Task thumbnail in y coordinate.
+ */
+ private float getTaskAlignmentTranslationY() {
+ DeviceProfile deviceProfile = mContainer.getDeviceProfile();
+ if (deviceProfile.isTablet) {
+ return deviceProfile.overviewRowSpacing;
+ }
+ return deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f;
}
- private Rect getTaskBounds(TaskView taskView) {
+ protected Rect getTaskBounds(TaskView taskView) {
int selectedPage = indexOfChild(taskView);
int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
int selectedPageScroll = getScrollForPage(selectedPage);
- boolean isTopRow = taskView != null && mTopRowIdSet.contains(taskView.getTaskViewId());
- Rect outRect = new Rect(mLastComputedTaskSize);
+ boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
+ Rect outRect = new Rect(
+ taskView.isGridTask() ? mLastComputedGridTaskSize : mLastComputedTaskSize);
outRect.offset(
-(primaryScroll - (selectedPageScroll + getOffsetFromScrollPosition(selectedPage))),
(int) (showAsGrid() && enableGridOnlyOverview() && !isTopRow
@@ -2254,10 +2481,6 @@ public Rect getLastComputedGridTaskSize() {
return mLastComputedGridTaskSize;
}
- public Rect getLastComputedCarouselTaskSize() {
- return mLastComputedCarouselTaskSize;
- }
-
/** Gets the task size for modal state. */
public void getModalTaskSize(Rect outRect) {
mSizeStrategy.calculateModalTaskSize(mContainer, mContainer.getDeviceProfile(), outRect,
@@ -2341,6 +2564,11 @@ protected int getDestinationPage(int scaledScroll) {
int minDistanceFromScreenStart = Integer.MAX_VALUE;
int minDistanceFromScreenStartIndex = INVALID_PAGE;
for (int i = 0; i < getChildCount(); ++i) {
+ // Do not set the destination page to the AddDesktopButton, which has the same page
+ // scrolls as the first [TaskView] and shouldn't be scrolled to.
+ if (getChildAt(i) instanceof AddDesktopButton) {
+ continue;
+ }
int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
if (distanceFromScreenStart < minDistanceFromScreenStart) {
minDistanceFromScreenStart = distanceFromScreenStart;
@@ -2362,41 +2590,39 @@ public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
return;
}
- int lower = 0;
- int upper = 0;
- int visibleStart = 0;
- int visibleEnd = 0;
+ int lowerIndex, upperIndex, visibleStart, visibleEnd;
if (showAsGrid()) {
int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
// For GRID_ONLY_OVERVIEW, use +/- 1 task column as visible area for preloading
// adjacent thumbnails, otherwise use +/-50% screen width
- int extraWidth = enableGridOnlyOverview() ? getLastComputedTaskSize().width()
- + getPageSpacing() : pageOrientedSize / 2;
+ int extraWidth =
+ enableGridOnlyOverview() ? getLastComputedTaskSize().width() + getPageSpacing()
+ : pageOrientedSize / 2;
+ lowerIndex = upperIndex = 0;
visibleStart = screenStart - extraWidth;
visibleEnd = screenStart + pageOrientedSize + extraWidth;
} else {
int centerPageIndex = getPageNearestToCenterOfScreen();
int numChildren = getChildCount();
- lower = Math.max(0, centerPageIndex - 2);
- upper = Math.min(centerPageIndex + 2, numChildren - 1);
+ lowerIndex = Math.max(0, centerPageIndex - 2);
+ upperIndex = Math.min(centerPageIndex + 2, numChildren - 1);
+ visibleStart = visibleEnd = 0;
}
List visibleTaskIds = new ArrayList<>();
-
// Update the task data for the in/visible children
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView taskView = requireTaskViewAt(i);
+ getTaskViews().forEachWithIndexInParent((index, taskView) -> {
List containers = taskView.getTaskContainers();
if (containers.isEmpty()) {
- continue;
+ return;
}
- int index = indexOfChild(taskView);
boolean visible;
if (showAsGrid()) {
- visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
+ visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd,
+ mTaskViewsDismissPrimaryTranslations.getOrDefault(taskView, 0));
} else {
- visible = lower <= index && index <= upper;
+ visible = index >= lowerIndex && index <= upperIndex;
}
if (visible) {
// Default update all non-null tasks, then remove running ones
@@ -2405,18 +2631,12 @@ public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
.collect(Collectors.toCollection(ArrayList::new));
if (enableRefactorTaskThumbnail()) {
visibleTaskIds.addAll(
- tasksToUpdate.stream().map((task) -> task.key.id).collect(Collectors.toList()));
- }
- if (mTmpRunningTasks != null) {
- for (Task t : mTmpRunningTasks) {
- // Skip loading if this is the task that we are animating into
- // TODO(b/280812109) change this equality check to use A.equals(B)
- tasksToUpdate.removeIf(task -> task == t);
- }
+ tasksToUpdate.stream().map((task) -> task.key.id).toList());
}
if (tasksToUpdate.isEmpty()) {
- continue;
+ return;
}
+ int visibilityChanges = 0;
for (Task task : tasksToUpdate) {
if (!mHasVisibleTaskData.get(task.key.id)) {
// Ignore thumbnail update if it's current running task during the gesture
@@ -2425,25 +2645,32 @@ public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
if (taskView == getRunningTaskView() && isGestureActive()) {
changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL;
}
- taskView.onTaskListVisibilityChanged(true /* visible */, changes);
+ visibilityChanges |= changes;
}
- mHasVisibleTaskData.put(task.key.id, visible);
+ mHasVisibleTaskData.put(task.key.id, true);
+ }
+ if (visibilityChanges != 0) {
+ taskView.onTaskListVisibilityChanged(true /* visible */, visibilityChanges);
}
} else {
+ int visibilityChanges = 0;
for (TaskContainer container : containers) {
if (container == null) {
continue;
}
if (mHasVisibleTaskData.get(container.getTask().key.id)) {
- taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
+ visibilityChanges = dataChanges;
}
mHasVisibleTaskData.delete(container.getTask().key.id);
}
+ if (visibilityChanges != 0) {
+ taskView.onTaskListVisibilityChanged(false /* visible */, visibilityChanges);
+ }
}
- }
+ });
if (enableRefactorTaskThumbnail()) {
- mTasksRepository.setVisibleTasks(visibleTaskIds);
+ mRecentsViewModel.updateVisibleTasks(visibleTaskIds);
}
}
@@ -2470,6 +2697,10 @@ public void onHighResLoadingStateChanged(boolean enabled) {
mModel.preloadCacheIfNeeded();
}
+ if (enableRefactorTaskThumbnail()) {
+ return;
+ }
+
// Whenever the high res loading state changes, poke each of the visible tasks to see if
// they want to updated their thumbnail state
for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
@@ -2507,7 +2738,14 @@ public void reset() {
mCurrentPageScrollDiff = 0;
mIgnoreResetTaskId = -1;
mTaskListChangeId = -1;
- mFocusedTaskViewId = -1;
+ setFocusedTaskViewId(INVALID_TASK_ID);
+ mAnyTaskHasBeenDismissed = false;
+
+ if (enableRefactorTaskThumbnail()) {
+ // TODO(b/353917593): RecentsView is never destroyed, so its dependencies need to
+ // be cleaned up during the reset, but re-created when RecentsView is "resumed".
+ // RecentsDependencies.Companion.destroy();
+ }
Log.d(TAG, "reset - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile
+ ", mRecentsAnimationController: " + mRecentsAnimationController);
@@ -2522,22 +2760,22 @@ public void reset() {
}
setEnableDrawingLiveTile(false);
}
- runActionOnRemoteHandles(remoteTargetHandle ->
- remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false));
- if (!FeatureFlags.enableSplitContextually()) {
- resetFromSplitSelectionState();
- }
-
+ mBlurUtils.setDrawLiveTileBelowRecents(false);
// These are relatively expensive and don't need to be done this frame (RecentsView isn't
// visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
- post(() -> {
- unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
- setCurrentPage(0);
- LayoutUtils.setViewEnabled(mActionsView, true);
- if (mOrientationState.setGestureActive(false)) {
- updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
- }
- });
+ post(this::onReset);
+ }
+
+ private void onReset() {
+ unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+ setCurrentPage(0);
+ LayoutUtils.setViewEnabled(mActionsView, true);
+ if (mOrientationState.setGestureActive(false)) {
+ updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
+ }
+ if (enableRefactorTaskThumbnail()) {
+ mRecentsViewModel.onReset();
+ }
}
public int getRunningTaskViewId() {
@@ -2567,13 +2805,12 @@ private int[] getTaskIdsForTaskViewId(int taskViewId) {
}
@Nullable
- private TaskView getTaskViewFromTaskViewId(int taskViewId) {
+ TaskView getTaskViewFromTaskViewId(int taskViewId) {
if (taskViewId == -1) {
return null;
}
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView taskView = requireTaskViewAt(i);
+ for (TaskView taskView : getTaskViews()) {
if (taskView.getTaskViewId() == taskViewId) {
return taskView;
}
@@ -2594,16 +2831,16 @@ public int getRunningTaskIndex() {
* Handle the edge case where Recents could increment task count very high over long
* period of device usage. Probably will never happen, but meh.
*/
- private TaskView getTaskViewFromPool(@TaskView.Type int type) {
+ protected TaskView getTaskViewFromPool(TaskViewType type) {
TaskView taskView;
switch (type) {
- case TaskView.Type.GROUPED:
+ case GROUPED:
taskView = mGroupedTaskViewPool.getView();
break;
- case TaskView.Type.DESKTOP:
+ case DESKTOP:
taskView = mDesktopTaskViewPool.getView();
break;
- case TaskView.Type.SINGLE:
+ case SINGLE:
default:
taskView = mTaskViewPool.getView();
}
@@ -2619,6 +2856,7 @@ private TaskView getTaskViewFromPool(@TaskView.Type int type) {
/**
* Get the index of the task view whose id matches {@param taskId}.
+ *
* @return -1 if there is no task view for the task id, else the index of the task view.
*/
public int getTaskIndexForId(int taskId) {
@@ -2633,37 +2871,48 @@ public void reloadIfNeeded() {
if (!mModel.isTaskListValid(mTaskListChangeId)) {
mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
.getFilter(mFilterState.getPackageNameToFilter()));
+ Log.d(TAG, "reloadIfNeeded - getTasks: " + mTaskListChangeId);
if (enableRefactorTaskThumbnail()) {
- mTasksRepository.getAllTaskData(/* forceRefresh = */ true);
+ mRecentsViewModel.refreshAllTaskData();
}
+ } else {
+ Log.d(TAG, "reloadIfNeeded - task list still valid: " + mTaskListChangeId);
}
}
/**
* Called when a gesture from an app is starting.
*/
- public void onGestureAnimationStart(
- Task[] runningTasks, RotationTouchHelper rotationTouchHelper) {
- Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
- mActiveGestureRunningTasks = runningTasks;
+ // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity` from being
+ // considered in Overview.
+ public void onGestureAnimationStart(GroupedTaskInfo groupedTaskInfo) {
+ Log.d(TAG, "onGestureAnimationStart - groupedTaskInfo: " + groupedTaskInfo);
+ mActiveGestureGroupedTaskInfo = groupedTaskInfo;
+
// This needs to be called before the other states are set since it can create the task view
if (mOrientationState.setGestureActive(true)) {
- setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
- rotationTouchHelper.getDisplayRotation());
+ reapplyActiveRotation();
// Force update to ensure the initial task size is computed even if the orientation has
// not changed.
updateSizeAndPadding();
}
- showCurrentTask(mActiveGestureRunningTasks);
+ showCurrentTask(groupedTaskInfo, "onGestureAnimationStart");
setEnableFreeScroll(false);
setEnableDrawingLiveTile(false);
setRunningTaskHidden(true);
- setTaskIconScaledDown(true);
+ setTaskIconVisible(false);
+ }
+
+ /**
+ * Returns whether the running task's attach alpha should be updated during the attach animation
+ */
+ public boolean shouldUpdateRunningTaskAlpha() {
+ return enableDesktopTaskAlphaAnimation() && getRunningTaskView() instanceof DesktopTaskView;
}
private boolean isGestureActive() {
- return mActiveGestureRunningTasks != null;
+ return mActiveGestureGroupedTaskInfo != null;
}
/**
@@ -2671,7 +2920,7 @@ private boolean isGestureActive() {
* {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
*/
public void onSwipeUpAnimationSuccess() {
- animateUpTaskIconScale();
+ startIconFadeInOnGestureComplete();
setSwipeDownShouldLaunchApp(true);
}
@@ -2710,28 +2959,12 @@ public AnimatorSet setRecentsChangedOrientation(boolean fadeOut) {
return as;
}
- private void updateChildTaskOrientations() {
- for (int i = 0; i < getTaskViewCount(); i++) {
- requireTaskViewAt(i).setOrientationState(mOrientationState);
- }
- boolean shouldRotateMenuForFakeRotation =
- !mOrientationState.isRecentsActivityRotationAllowed();
- if (!shouldRotateMenuForFakeRotation) {
- return;
- }
- TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mContainer, TYPE_TASK_MENU);
- if (tv != null) {
- // Rotation is supported on phone (details at b/254198019#comment4)
- tv.onRotationChanged();
- }
- }
-
/**
* Called when a gesture from an app has finished, and an end target has been determined.
*/
public void onPrepareGestureEndAnimation(
@Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
- TaskViewSimulator[] taskViewSimulators) {
+ RemoteTargetHandle[] remoteTargetHandles) {
Log.d(TAG, "onPrepareGestureEndAnimation - endTarget: " + endTarget);
mCurrentGestureEndTarget = endTarget;
boolean isOverviewEndTarget = endTarget == GestureState.GestureEndTarget.RECENTS;
@@ -2740,31 +2973,59 @@ public void onPrepareGestureEndAnimation(
}
BaseState> endState = mSizeStrategy.stateFromGestureEndTarget(endTarget);
+ // Starting the desk exploded animation when the gesture from an app is released.
+ if (enableDesktopExplodedView()) {
+ if (animatorSet == null) {
+ mUtils.setDeskExplodeProgress(endState.showExplodedDesktopView() ? 1f : 0f);
+ } else {
+ animatorSet.play(
+ ObjectAnimator.ofFloat(this, DESK_EXPLODE_PROGRESS,
+ endState.showExplodedDesktopView() ? 1f : 0f));
+ }
+
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView instanceof DesktopTaskView desktopTaskView) {
+ desktopTaskView.setRemoteTargetHandles(remoteTargetHandles);
+ }
+ }
+ }
+
if (endState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
TaskView runningTaskView = getRunningTaskView();
- float runningTaskPrimaryGridTranslation = 0;
- float runningTaskSecondaryGridTranslation = 0;
+ float runningTaskGridTranslationX = 0;
+ float runningTaskGridTranslationY = 0;
if (runningTaskView != null) {
// Apply the grid translation to running task unless it's being snapped to
// and removes the current translation applied to the running task.
- runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX()
+ runningTaskGridTranslationX = runningTaskView.getGridTranslationX()
- runningTaskView.getNonGridTranslationX();
- runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY();
+ runningTaskGridTranslationY = runningTaskView.getGridTranslationY();
}
- for (TaskViewSimulator tvs : taskViewSimulators) {
+ for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) {
+ TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator();
if (animatorSet == null) {
setGridProgress(1);
- tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
- tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
+ if (enableGridOnlyOverview()) {
+ tvs.taskGridTranslationX.value = runningTaskGridTranslationX;
+ tvs.taskGridTranslationY.value = runningTaskGridTranslationY;
+ } else {
+ tvs.taskPrimaryTranslation.value = runningTaskGridTranslationX;
+ tvs.taskSecondaryTranslation.value = runningTaskGridTranslationY;
+ }
} else {
animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
- animatorSet.play(tvs.carouselScale.animateToValue(1));
- animatorSet.play(tvs.carouselPrimaryTranslation.animateToValue(0));
- animatorSet.play(tvs.carouselSecondaryTranslation.animateToValue(0));
- animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
- runningTaskPrimaryGridTranslation));
- animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
- runningTaskSecondaryGridTranslation));
+ if (enableGridOnlyOverview()) {
+ animatorSet.play(tvs.carouselScale.animateToValue(1));
+ animatorSet.play(tvs.taskGridTranslationX.animateToValue(
+ runningTaskGridTranslationX));
+ animatorSet.play(tvs.taskGridTranslationY.animateToValue(
+ runningTaskGridTranslationY));
+ } else {
+ animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
+ runningTaskGridTranslationX));
+ animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
+ runningTaskGridTranslationY));
+ }
}
}
}
@@ -2775,13 +3036,21 @@ public void onPrepareGestureEndAnimation(
animatorSet.play(
ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, splashAlpha));
}
+ if (enableLargeDesktopWindowingTile()) {
+ if (animatorSet != null) {
+ animatorSet.play(
+ ObjectAnimator.ofFloat(this, DESKTOP_CAROUSEL_DETACH_PROGRESS, 0f));
+ } else {
+ DESKTOP_CAROUSEL_DETACH_PROGRESS.set(this, 0f);
+ }
+ }
}
/**
* Called when a gesture from an app has finished, and the animation to the target has ended.
*/
public void onGestureAnimationEnd() {
- mActiveGestureRunningTasks = null;
+ mActiveGestureGroupedTaskInfo = null;
if (mOrientationState.setGestureActive(false)) {
updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
}
@@ -2790,20 +3059,48 @@ public void onGestureAnimationEnd() {
setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
Log.d(TAG, "onGestureAnimationEnd - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile);
setRunningTaskHidden(false);
- animateUpTaskIconScale();
+ startIconFadeInOnGestureComplete();
animateActionsViewIn();
+ if (mEnableDrawingLiveTile) {
+ if (enableDesktopExplodedView()) {
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView instanceof DesktopTaskView desktopTaskView) {
+ desktopTaskView.setRemoteTargetHandles(mRemoteTargetHandles);
+ }
+ }
+ }
+ TaskView runningTaskView = getRunningTaskView();
+ if (showAsGrid() && enableGridOnlyOverview() && runningTaskView != null) {
+ runActionOnRemoteHandles(remoteTargetHandle -> {
+ TaskViewSimulator taskViewSimulator = remoteTargetHandle.getTaskViewSimulator();
+ // After settling in Overview, recentsScroll will be used to adjust horizontally
+ // location and taskGridTranslationX doesn't needs to be applied.
+ taskViewSimulator.taskGridTranslationX.value = 0;
+ taskViewSimulator.taskGridTranslationY.value =
+ runningTaskView.getGridTranslationY();
+ });
+ }
+ }
+
mCurrentGestureEndTarget = null;
}
/**
* Returns true if we should add a stub taskView for the running task id
*/
- protected boolean shouldAddStubTaskView(Task[] runningTasks) {
- int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray();
+ protected boolean shouldAddStubTaskView(GroupedTaskInfo groupedTaskInfo) {
+ int[] runningTaskIds;
+ if (groupedTaskInfo != null) {
+ runningTaskIds = groupedTaskInfo.getTaskInfoList().stream().mapToInt(
+ taskInfo -> taskInfo.taskId).toArray();
+ } else {
+ runningTaskIds = new int[0];
+ }
TaskView matchingTaskView = null;
- if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) {
- // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single
+ if (groupedTaskInfo != null && groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK)
+ && runningTaskIds.length == 1) {
+ // TODO(b/342635213): Unsure if it's expected, desktop runningTasks only have a single
// taskId, therefore we match any DesktopTaskView that contains the runningTaskId.
TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]);
if (taskview instanceof DesktopTaskView) {
@@ -2816,44 +3113,41 @@ protected boolean shouldAddStubTaskView(Task[] runningTasks) {
}
/**
- * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
+ * Creates a task view (if necessary) to represent the tasks with the {@param groupedTaskInfo}.
*
* All subsequent calls to reload will keep the task as the first item until {@link #reset()}
* is called. Also scrolls the view to this task.
*/
- private void showCurrentTask(Task[] runningTasks) {
- Log.d(TAG, "showCurrentTask - runningTasks: " + Arrays.toString(runningTasks));
- if (runningTasks.length == 0) {
+ private void showCurrentTask(GroupedTaskInfo groupedTaskInfo, String caller) {
+ Log.d(TAG, "showCurrentTask(" + caller + ") - groupedTaskInfo: " + groupedTaskInfo);
+ if (groupedTaskInfo == null) {
return;
}
+
int runningTaskViewId = -1;
- boolean needGroupTaskView = runningTasks.length > 1;
- boolean needDesktopTask = hasDesktopTask(runningTasks);
- if (shouldAddStubTaskView(runningTasks)) {
+ if (shouldAddStubTaskView(groupedTaskInfo)) {
boolean wasEmpty = getChildCount() == 0;
// Add an empty view for now until the task plan is loaded and applied
final TaskView taskView;
- if (needDesktopTask) {
- taskView = getTaskViewFromPool(TaskView.Type.DESKTOP);
- mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length);
- ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks),
- mOrientationState, mTaskOverlayFactory);
- } else if (needGroupTaskView) {
- taskView = getTaskViewFromPool(TaskView.Type.GROUPED);
- mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]};
+ if (groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK)) {
+ taskView = mUtils.createDesktopTaskViewForActiveDesk(groupedTaskInfo);
+ } else if (groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_SPLIT)) {
+ taskView = getTaskViewFromPool(TaskViewType.GROUPED);
// When we create a placeholder task view mSplitBoundsConfig will be null, but with
// the actual app running we won't need to show the thumbnail until all the tasks
// load later anyways
- ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
- mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
+ ((GroupedTaskView) taskView).bind(Task.from(groupedTaskInfo.getTaskInfo1()),
+ Task.from(groupedTaskInfo.getTaskInfo2()), mOrientationState,
+ mTaskOverlayFactory, mSplitBoundsConfig);
} else {
- taskView = getTaskViewFromPool(TaskView.Type.SINGLE);
- // The temporary running task is only used for the duration between the start of the
- // gesture and the task list is loaded and applied
- mTmpRunningTasks = new Task[]{runningTasks[0]};
- taskView.bind(mTmpRunningTasks[0], mOrientationState, mTaskOverlayFactory);
+ taskView = getTaskViewFromPool(TaskViewType.SINGLE);
+ taskView.bind(Task.from(groupedTaskInfo.getTaskInfo1()), mOrientationState,
+ mTaskOverlayFactory);
}
- addView(taskView, 0);
+ if (mAddDesktopButton != null && wasEmpty) {
+ addView(mAddDesktopButton);
+ }
+ addView(taskView, mUtils.getRunningTaskExpectedIndex(taskView));
runningTaskViewId = taskView.getTaskViewId();
if (wasEmpty) {
addView(mClearAllButton);
@@ -2864,41 +3158,40 @@ private void showCurrentTask(Task[] runningTasks) {
measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
makeMeasureSpec(getMeasuredHeight(), EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
- } else if (getTaskViewByTaskId(runningTasks[0].key.id) != null) {
- runningTaskViewId = getTaskViewByTaskId(runningTasks[0].key.id).getTaskViewId();
+ } else {
+ var runningTaskView = getTaskViewByTaskId(groupedTaskInfo.getTaskInfo1().taskId);
+ if (runningTaskView != null) {
+ runningTaskViewId = runningTaskView.getTaskViewId();
+ }
}
boolean runningTaskTileHidden = mRunningTaskTileHidden;
setCurrentTask(runningTaskViewId);
- mFocusedTaskViewId = enableGridOnlyOverview() ? INVALID_TASK_ID : runningTaskViewId;
+
+ int focusedTaskViewId;
+ if (enableGridOnlyOverview()) {
+ focusedTaskViewId = INVALID_TASK_ID;
+ } else if (enableLargeDesktopWindowingTile()
+ && getRunningTaskView() instanceof DesktopTaskView) {
+ TaskView focusedTaskView = mUtils.getFirstNonDesktopTaskView();
+ focusedTaskViewId =
+ focusedTaskView != null ? focusedTaskView.getTaskViewId() : INVALID_TASK_ID;
+ } else {
+ focusedTaskViewId = runningTaskViewId;
+ }
+ setFocusedTaskViewId(focusedTaskViewId);
+
runOnPageScrollsInitialized(() -> setCurrentPage(getRunningTaskIndex()));
setRunningTaskViewShowScreenshot(false);
setRunningTaskHidden(runningTaskTileHidden);
// Update task size after setting current task.
updateTaskSize();
- updateChildTaskOrientations();
+ mUtils.updateChildTaskOrientations();
// Reload the task list
reloadIfNeeded();
}
- private boolean hasDesktopTask(Task[] runningTasks) {
- try {
- if (!DesktopModeStatus.canEnterDesktopMode(getContext())) {
- return false;
- }
- } catch (NoClassDefFoundError e) {
- // Desktop mode is not supported on this device
- return false;
- }
- for (Task task : runningTasks) {
- if (task.key.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) {
- return true;
- }
- }
- return false;
- }
-
/**
* Sets the running task id, cleaning up the old running task if necessary.
*/
@@ -2909,7 +3202,7 @@ public void setCurrentTask(int runningTaskViewId) {
if (mRunningTaskViewId != -1) {
// Reset the state on the old running task view
- setTaskIconScaledDown(false);
+ setTaskIconVisible(true);
setRunningTaskViewShowScreenshot(true);
setRunningTaskHidden(false);
}
@@ -2917,21 +3210,20 @@ public void setCurrentTask(int runningTaskViewId) {
}
private void setRunningTaskViewId(int runningTaskViewId) {
- int prevRunningTaskViewId = mRunningTaskViewId;
mRunningTaskViewId = runningTaskViewId;
- if (Flags.enableRefactorTaskThumbnail()) {
- TaskView previousRunningTaskView = getTaskViewFromTaskViewId(prevRunningTaskViewId);
- if (previousRunningTaskView != null) {
- previousRunningTaskView.notifyIsRunningTaskUpdated();
- }
- TaskView newRunningTaskView = getTaskViewFromTaskViewId(runningTaskViewId);
- if (newRunningTaskView != null) {
- newRunningTaskView.notifyIsRunningTaskUpdated();
- }
+ if (enableRefactorTaskThumbnail()) {
+ TaskView runningTaskView = getTaskViewFromTaskViewId(runningTaskViewId);
+ mRecentsViewModel.updateRunningTask(
+ runningTaskView != null ? runningTaskView.getTaskIdSet()
+ : Collections.emptySet());
}
}
+ private void setFocusedTaskViewId(int viewId) {
+ mFocusedTaskViewId = viewId;
+ }
+
private int getTaskViewIdFromTaskId(int taskId) {
TaskView taskView = getTaskViewByTaskId(taskId);
return taskView != null ? taskView.getTaskViewId() : -1;
@@ -2942,68 +3234,79 @@ private int getTaskViewIdFromTaskId(int taskId) {
*/
public void setRunningTaskHidden(boolean isHidden) {
mRunningTaskTileHidden = isHidden;
+ // mRunningTaskAttachAlpha can be changed by RUNNING_TASK_ATTACH_ALPHA animation without
+ // changing mRunningTaskTileHidden.
+ mRunningTaskAttachAlpha = isHidden ? 0f : 1f;
TaskView runningTask = getRunningTaskView();
- if (runningTask != null) {
- runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
- if (!isHidden) {
- AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
- AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
- }
+ if (runningTask == null) {
+ return;
+ }
+ applyAttachAlpha();
+ if (!isHidden) {
+ AccessibilityManagerCompat.sendCustomAccessibilityEvent(
+ runningTask, AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
}
}
+ private void applyAttachAlpha() {
+ // Only hide non running task carousel when it's fully off screen, otherwise it needs to
+ // be visible to move to on screen.
+ mUtils.applyAttachAlpha(
+ /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress == 1f);
+ }
+
private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
+ setRunningTaskViewShowScreenshot(showScreenshot, /*updatedThumbnails=*/null);
+ }
+
+ private void setRunningTaskViewShowScreenshot(boolean showScreenshot,
+ @Nullable Map updatedThumbnails) {
mRunningTaskShowScreenshot = showScreenshot;
TaskView runningTaskView = getRunningTaskView();
if (runningTaskView != null) {
- runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot);
+ runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot, updatedThumbnails);
+ }
+ if (enableRefactorTaskThumbnail()) {
+ mRecentsViewModel.setRunningTaskShowScreenshot(showScreenshot);
}
}
- public void setTaskIconScaledDown(boolean isScaledDown) {
- if (mTaskIconScaledDown != isScaledDown) {
- mTaskIconScaledDown = isScaledDown;
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+ /**
+ * Updates icon visibility when going in or out of overview.
+ */
+ public void setTaskIconVisible(boolean isVisible) {
+ if (mTaskIconVisible != isVisible) {
+ mTaskIconVisible = isVisible;
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setIconVisibleForGesture(mTaskIconVisible);
}
}
}
private void animateActionsViewIn() {
if (!showAsGrid() || isFocusedTaskInExpectedScrollPosition()) {
- animateActionsViewAlpha(1, TaskView.SCALE_ICON_DURATION);
- }
- }
-
- public void animateUpTaskIconScale() {
- mTaskIconScaledDown = false;
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
- taskView.animateIconScaleAndDimIntoView();
+ animateActionsViewAlpha(1, TaskView.FADE_IN_ICON_DURATION);
}
}
/**
- * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
- * layout.
- * This method is used when no task dismissal has occurred.
+ * Updates icon visibility when gesture is settled.
*/
- private void updateGridProperties() {
- updateGridProperties(false, Integer.MAX_VALUE);
+ public void startIconFadeInOnGestureComplete() {
+ mTaskIconVisible = true;
+ for (TaskView taskView : getTaskViews()) {
+ taskView.startIconFadeInOnGestureComplete();
+ }
}
/**
* Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
* layout.
*
- * This method is used when task dismissal has occurred, but rebalance is not needed.
- *
- * @param isTaskDismissal indicates if update was called due to task dismissal
+ * Skips rebalance.
*/
- private void updateGridProperties(boolean isTaskDismissal) {
- updateGridProperties(isTaskDismissal, Integer.MAX_VALUE);
+ private void updateGridProperties() {
+ updateGridProperties(null);
}
/**
@@ -3013,13 +3316,11 @@ private void updateGridProperties(boolean isTaskDismissal) {
* This method only calculates the potential position and depends on {@link #setGridProgress} to
* apply the actual scaling and translation.
*
- * @param isTaskDismissal indicates if update was called due to task dismissal
- * @param startRebalanceAfter which view index to start rebalancing from. Use Integer.MAX_VALUE
- * to skip rebalance
+ * @param lastVisibleTaskViewDuringDismiss which TaskView to start rebalancing from. Use
+ * `null` to skip rebalance.
*/
- private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter) {
- int taskCount = getTaskViewCount();
- if (taskCount == 0) {
+ private void updateGridProperties(TaskView lastVisibleTaskViewDuringDismiss) {
+ if (!hasTaskViews()) {
return;
}
@@ -3028,68 +3329,97 @@ private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAft
int topRowWidth = 0;
int bottomRowWidth = 0;
+ int largeTileRowWidth = 0;
float topAccumulatedTranslationX = 0;
float bottomAccumulatedTranslationX = 0;
- // Contains whether the child index is in top or bottom of grid (for non-focused task)
- // Different from mTopRowIdSet, which contains the taskViewId of what task is in top row
- IntSet topSet = new IntSet();
- IntSet bottomSet = new IntSet();
-
- // Horizontal grid translation for each task
- float[] gridTranslations = new float[taskCount];
+ // Horizontal grid translation for each task.
+ Map gridTranslations = new HashMap<>();
- int focusedTaskIndex = Integer.MAX_VALUE;
- int focusedTaskShift = 0;
- int focusedTaskWidthAndSpacing = 0;
+ TaskView lastLargeTaskView = mUtils.getLastLargeTaskView();
+ int focusedTaskViewShift = 0;
+ int largeTaskWidthAndSpacing = 0;
int snappedTaskRowWidth = 0;
+ int expectedCurrentTaskRowWidth = 0;
int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
TaskView snappedTaskView = getTaskViewAt(snappedPage);
TaskView homeTaskView = getHomeTaskView();
+ TaskView expectedCurrentTaskView = mUtils.getExpectedCurrentTask(getRunningTaskView(),
+ getFocusedTaskView());
TaskView nextFocusedTaskView = null;
- if (!isTaskDismissal) {
+ // Don't clear the top row, if the user has dismissed a task, to maintain the task order.
+ if (!mAnyTaskHasBeenDismissed) {
mTopRowIdSet.clear();
}
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
+
+ // Consecutive task views in the top row or bottom row, which means another one set will
+ // be cleared up while starting to add TaskViews to one of them. Also means only one of
+ // them can be non-empty at most.
+ Set lastTopTaskViews = new HashSet<>();
+ Set lastBottomTaskViews = new HashSet<>();
+
+ int largeTasksCount = 0;
+ // True if the last large TaskView has been visited during the TaskViews iteration.
+ boolean encounteredLastLargeTaskView = false;
+ // True if the highest index visible TaskView has been visited during the TaskViews
+ // iteration.
+ boolean encounteredLastVisibleTaskView = false;
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView == lastLargeTaskView) {
+ encounteredLastLargeTaskView = true;
+ }
+ if (taskView == lastVisibleTaskViewDuringDismiss) {
+ encounteredLastVisibleTaskView = true;
+ }
+ float gridTranslation = 0f;
int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing;
// Evenly distribute tasks between rows unless rearranging due to task dismissal, in
// which case keep tasks in their respective rows. For the running task, don't join
// the grid.
- if (taskView.isFocusedTask()) {
- topRowWidth += taskWidthAndSpacing;
- bottomRowWidth += taskWidthAndSpacing;
-
- focusedTaskIndex = i;
- focusedTaskWidthAndSpacing = taskWidthAndSpacing;
- gridTranslations[i] += focusedTaskShift;
- gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
+ if (taskView.isLargeTile()) {
+ largeTasksCount++;
+ // DesktopTaskView`s are hidden during split select state, so we shouldn't count
+ // them when calculating row width.
+ if (!(taskView instanceof DesktopTaskView && isSplitSelectionActive())) {
+ topRowWidth += taskWidthAndSpacing;
+ bottomRowWidth += taskWidthAndSpacing;
+ largeTileRowWidth += taskWidthAndSpacing;
+ }
+ gridTranslation += focusedTaskViewShift;
+ gridTranslation += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
// Center view vertically in case it's from different orientation.
taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
- taskView.getLayoutParams().height) / 2f);
+ largeTaskWidthAndSpacing = taskWidthAndSpacing;
+
if (taskView == snappedTaskView) {
- // If focused task is snapped, the row width is just task width and spacing.
- snappedTaskRowWidth = taskWidthAndSpacing;
+ snappedTaskRowWidth = largeTileRowWidth;
+ }
+ if (taskView == expectedCurrentTaskView) {
+ expectedCurrentTaskRowWidth = largeTileRowWidth;
}
} else {
- if (i > focusedTaskIndex) {
- // For tasks after the focused task, shift by focused task's width and spacing.
- gridTranslations[i] +=
- mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+ if (encounteredLastLargeTaskView) {
+ // For tasks after the last large task, shift by large task's width and spacing.
+ gridTranslation +=
+ mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
} else {
- // For task before the focused task, accumulate the width and spacing to
- // calculate the distance focused task need to shift.
- focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
+ // For TaskViews before the new focused TaskView, accumulate the width and
+ // spacing to calculate the distance the new focused TaskView needs to shift.
+ // This could happen for example after multiple times of dismissing the
+ // focused TaskView, the triggered rebalance might set a non-first TaskView
+ // inside `mChildren` as the new focused TaskView.
+ focusedTaskViewShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
}
int taskViewId = taskView.getTaskViewId();
- // Rebalance the grid starting after a certain index
boolean isTopRow;
- if (isTaskDismissal) {
- if (i > startRebalanceAfter) {
+ if (mAnyTaskHasBeenDismissed) {
+ // Rebalance the grid starting after a certain index.
+ if (encounteredLastVisibleTaskView) {
mTopRowIdSet.remove(taskViewId);
isTopRow = topRowWidth <= bottomRowWidth;
} else {
@@ -3106,47 +3436,47 @@ private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAft
} else {
topRowWidth += taskWidthAndSpacing;
}
- topSet.add(i);
mTopRowIdSet.add(taskViewId);
-
taskView.setGridTranslationY(mTaskGridVerticalDiff);
// Move horizontally into empty space.
float widthOffset = 0;
- for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
- if (j == focusedTaskIndex) {
- continue;
- }
- widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+ for (TaskView bottomTaskView : lastBottomTaskViews) {
+ widthOffset += bottomTaskView.getLayoutParams().width + mPageSpacing;
}
float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
- gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX;
+ gridTranslation += topAccumulatedTranslationX + currentTaskTranslationX;
topAccumulatedTranslationX += currentTaskTranslationX;
+ lastTopTaskViews.add(taskView);
+ lastBottomTaskViews.clear();
} else {
bottomRowWidth += taskWidthAndSpacing;
- bottomSet.add(i);
// Move into bottom row.
taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff);
// Move horizontally into empty space.
float widthOffset = 0;
- for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
- if (j == focusedTaskIndex) {
- continue;
- }
- widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+ for (TaskView topTaskView : lastTopTaskViews) {
+ widthOffset += topTaskView.getLayoutParams().width + mPageSpacing;
}
float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
- gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX;
+ gridTranslation += bottomAccumulatedTranslationX + currentTaskTranslationX;
bottomAccumulatedTranslationX += currentTaskTranslationX;
+ lastBottomTaskViews.add(taskView);
+ lastTopTaskViews.clear();
}
+ int taskViewRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
if (taskView == snappedTaskView) {
- snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
+ snappedTaskRowWidth = taskViewRowWidth;
+ }
+ if (taskView == expectedCurrentTaskView) {
+ expectedCurrentTaskRowWidth = taskViewRowWidth;
}
}
+ gridTranslations.put(taskView, gridTranslation);
}
// We need to maintain snapped task's page scroll invariant between quick switch and
@@ -3157,22 +3487,22 @@ private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAft
if (snappedTaskView != null) {
snappedTaskNonGridScrollAdjustment = snappedTaskView.getScrollAdjustment(
/*gridEnabled=*/false);
- snappedTaskGridTranslationX = gridTranslations[snappedPage];
+ snappedTaskGridTranslationX = gridTranslations.getOrDefault(snappedTaskView, 0f);
}
// Use the accumulated translation of the row containing the last task.
- float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1)
+ float clearAllAccumulatedTranslation = !lastTopTaskViews.isEmpty()
? topAccumulatedTranslationX : bottomAccumulatedTranslationX;
// If the last task is on the shorter row, ClearAllButton will embed into the shorter row
// which is not what we want. Compensate the width difference of the 2 rows in that case.
float shorterRowCompensation = 0;
if (topRowWidth <= bottomRowWidth) {
- if (topSet.contains(taskCount - 1)) {
+ if (!lastTopTaskViews.isEmpty()) {
shorterRowCompensation = bottomRowWidth - topRowWidth;
}
} else {
- if (bottomSet.contains(taskCount - 1)) {
+ if (!lastBottomTaskViews.isEmpty()) {
shorterRowCompensation = topRowWidth - bottomRowWidth;
}
}
@@ -3183,12 +3513,20 @@ private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAft
// accordingly. Update longRowWidth if ClearAllButton has been moved.
float clearAllShortTotalWidthTranslation = 0;
int longRowWidth = Math.max(topRowWidth, bottomRowWidth);
- if (longRowWidth < mLastComputedGridSize.width()) {
- mClearAllShortTotalWidthTranslation =
- (mIsRtl
- ? mLastComputedTaskSize.right
- : deviceProfile.widthPx - mLastComputedTaskSize.left)
- - longRowWidth - deviceProfile.overviewGridSideMargin;
+
+ // If first task is not in the expected position (mLastComputedTaskSize) and being too close
+ // to ClearAllButton, then apply extra translation to ClearAllButton.
+ int rowWidthAfterExpectedCurrentTask = longRowWidth - expectedCurrentTaskRowWidth;
+ int expectedCurrentTaskWidthAndSpacing =
+ (expectedCurrentTaskView != null
+ ? expectedCurrentTaskView.getLayoutParams().width
+ : 0
+ ) + mPageSpacing;
+ int firstTaskStart = mLastComputedGridSize.left + rowWidthAfterExpectedCurrentTask
+ + expectedCurrentTaskWidthAndSpacing;
+ int expectedFirstTaskStart = mLastComputedTaskSize.right;
+ if (firstTaskStart < expectedFirstTaskStart) {
+ mClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
clearAllShortTotalWidthTranslation = mIsRtl
? -mClearAllShortTotalWidthTranslation : mClearAllShortTotalWidthTranslation;
if (snappedTaskRowWidth == longRowWidth) {
@@ -3203,10 +3541,10 @@ private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAft
float clearAllTotalTranslationX =
clearAllAccumulatedTranslation + clearAllShorterRowCompensation
+ clearAllShortTotalWidthTranslation + snappedTaskNonGridScrollAdjustment;
- if (focusedTaskIndex < taskCount) {
+ if (largeTasksCount > 0) {
// Shift by focused task's width and spacing if a task is focused.
clearAllTotalTranslationX +=
- mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+ mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
}
// Make sure there are enough space between snapped page and ClearAllButton, for the case
@@ -3218,27 +3556,33 @@ private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAft
(mIsRtl
? mLastComputedTaskSize.left
: deviceProfile.widthPx - mLastComputedTaskSize.right)
- - deviceProfile.overviewGridSideMargin - mPageSpacing
- + (mTaskWidth - snappedTaskView.getLayoutParams().width)
- - mClearAllShortTotalWidthTranslation;
+ - deviceProfile.overviewGridSideMargin - mPageSpacing
+ + (mTaskWidth - snappedTaskView.getLayoutParams().width)
+ - mClearAllShortTotalWidthTranslation;
if (distanceFromClearAll < minimumDistance) {
int distanceDifference = minimumDistance - distanceFromClearAll;
snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference;
}
}
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
- taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX
- + snappedTaskNonGridScrollAdjustment);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setGridTranslationX(
+ gridTranslations.getOrDefault(taskView, 0f) - snappedTaskGridTranslationX
+ + snappedTaskNonGridScrollAdjustment);
}
- final TaskView runningTask = getRunningTaskView();
- if (showAsGrid() && enableGridOnlyOverview() && runningTask != null) {
- runActionOnRemoteHandles(
- remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
- .taskSecondaryTranslation.value = runningTask.getGridTranslationY()
- );
+ if (mAddDesktopButton != null) {
+ TaskView firstTaskView = getFirstTaskView();
+ float translationX = 0f;
+ if (firstTaskView != null) {
+ translationX += firstTaskView.getGridTranslationX();
+ }
+ if (focusedTaskViewShift != 0) {
+ // If the focused task is inserted between `firstTaskView` and
+ // `mAddDesktopButton`, shift `mAddDesktopButton` to accommodate.
+ translationX += largeTaskWidthAndSpacing;
+ }
+ mAddDesktopButton.setGridTranslationX(translationX);
}
mClearAllButton.setGridTranslationPrimary(
@@ -3246,19 +3590,18 @@ private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAft
mClearAllButton.setGridScrollOffset(
mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left
: mLastComputedTaskSize.right - mLastComputedGridSize.right);
-
setGridProgress(mGridProgress);
}
- private boolean isSameGridRow(TaskView taskView1, TaskView taskView2) {
+ protected boolean isSameGridRow(TaskView taskView1, TaskView taskView2) {
if (taskView1 == null || taskView2 == null) {
return false;
}
- int taskViewId1 = taskView1.getTaskViewId();
- int taskViewId2 = taskView2.getTaskViewId();
- if (taskViewId1 == mFocusedTaskViewId || taskViewId2 == mFocusedTaskViewId) {
+ if (taskView1.isLargeTile() || taskView2.isLargeTile()) {
return false;
}
+ int taskViewId1 = taskView1.getTaskViewId();
+ int taskViewId2 = taskView2.getTaskViewId();
return (mTopRowIdSet.contains(taskViewId1) && mTopRowIdSet.contains(taskViewId2)) || (
!mTopRowIdSet.contains(taskViewId1) && !mTopRowIdSet.contains(taskViewId2));
}
@@ -3271,22 +3614,16 @@ private boolean isSameGridRow(TaskView taskView1, TaskView taskView2) {
private void setGridProgress(float gridProgress) {
mGridProgress = gridProgress;
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- requireTaskViewAt(i).setGridProgress(gridProgress);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setGridProgress(gridProgress);
}
mClearAllButton.setGridProgress(gridProgress);
}
private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
- int taskCount = getTaskViewCount();
- if (taskCount == 0) {
- return;
- }
-
mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha;
- for (int i = 0; i < taskCount; i++) {
- requireTaskViewAt(i).setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
}
}
@@ -3300,12 +3637,12 @@ private void enableLayoutTransitions() {
mLayoutTransition.addTransitionListener(new TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
- View view, int i) {
+ View view, int i) {
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
- View view, int i) {
+ View view, int i) {
// When the unpinned task is added, snap to first page and disable transitions
if (view instanceof TaskView) {
snapToPage(0);
@@ -3358,12 +3695,9 @@ private void addDismissedTaskAnimations(TaskView taskView, long duration,
if (taskView.isRunningTask()) {
anim.addOnFrameCallback(() -> {
if (!mEnableDrawingLiveTile) return;
- runActionOnRemoteHandles(
- remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
- .taskSecondaryTranslation.value = getPagedOrientationHandler()
- .getSecondaryValue(taskView.getTranslationX(),
- taskView.getTranslationY()
- ));
+ runActionOnRemoteHandles(remoteTargetHandle ->
+ remoteTargetHandle.getTaskViewSimulator().taskSecondaryTranslation.value =
+ taskView.getSecondaryDismissTranslationProperty().get(taskView));
redrawLiveTile();
});
}
@@ -3408,6 +3742,7 @@ private void createInitialSplitSelectAnimation(PendingAnimation anim) {
mSplitSelectStateController.getSplitAnimationController().
playAnimPlaceholderToFullscreen(mContainer, view,
Optional.of(() -> resetFromSplitSelectionState())));
+ firstFloatingTaskView.setContentDescription(splitAnimInitProps.getContentDescription());
// SplitInstructionsView: animate in
safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
@@ -3443,11 +3778,7 @@ public void onAnimationStart(Animator animation) {
InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
} else {
// If transition to split select was interrupted, clean up to prevent glitches
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.resetState();
- } else {
- resetFromSplitSelectionState();
- }
+ mSplitSelectStateController.resetState();
InteractionJankMonitorWrapper.cancel(Cuj.CUJ_SPLIT_SCREEN_ENTER);
}
@@ -3457,17 +3788,22 @@ public void onAnimationStart(Animator animation) {
/**
* Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}.
- * @param dismissedTaskView the {@link TaskView} to be dismissed
- * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated
- * @param shouldRemoveTask whether the associated {@link Task} should be removed from
- * ActivityManager after dismissal
- * @param duration duration of the animation
+ *
+ * @param dismissedTaskView the {@link TaskView} to be dismissed
+ * @param animateTaskView whether the {@link TaskView} to be dismissed should be
+ * animated
+ * @param shouldRemoveTask whether the associated {@link Task} should be removed from
+ * ActivityManager after dismissal
+ * @param duration duration of the animation
* @param dismissingForSplitSelection task dismiss animation is used for entering split
* selection state from app icon
+ * @param isExpressiveDismiss runs expressive animations controlled via
+ * {@link RecentsDismissUtils}
*/
- public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissedTaskView,
+ public void createTaskDismissAnimation(PendingAnimation anim,
+ @Nullable TaskView dismissedTaskView,
boolean animateTaskView, boolean shouldRemoveTask, long duration,
- boolean dismissingForSplitSelection) {
+ boolean dismissingForSplitSelection, boolean isExpressiveDismiss) {
if (mPendingAnimation != null) {
mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd();
}
@@ -3480,35 +3816,44 @@ public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissed
boolean showAsGrid = showAsGrid();
int taskCount = getTaskViewCount();
int dismissedIndex = indexOfChild(dismissedTaskView);
- int dismissedTaskViewId = dismissedTaskView.getTaskViewId();
+ int dismissedTaskViewId =
+ dismissedTaskView != null ? dismissedTaskView.getTaskViewId() : INVALID_TASK_ID;
// Grid specific properties.
boolean isFocusedTaskDismissed = false;
boolean isStagingFocusedTask = false;
+ boolean isSlidingTasks = false;
TaskView nextFocusedTaskView = null;
boolean nextFocusedTaskFromTop = false;
float dismissedTaskWidth = 0;
float nextFocusedTaskWidth = 0;
- // Non-grid specific properties.
int[] oldScroll = new int[count];
int[] newScroll = new int[count];
int scrollDiffPerPage = 0;
+ // Non-grid specific properties.
boolean needsCurveUpdates = false;
+ boolean areAllDesktopTasksDismissed = false;
if (showAsGrid) {
- dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
- isFocusedTaskDismissed = dismissedTaskViewId == mFocusedTaskViewId;
+ if (dismissedTaskView != null) {
+ dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
+ }
+ isFocusedTaskDismissed = dismissedTaskViewId != INVALID_TASK_ID
+ && dismissedTaskViewId == mFocusedTaskViewId;
+ if (dismissingForSplitSelection && getTaskViewAt(
+ mCurrentPage) instanceof DesktopTaskView) {
+ areAllDesktopTasksDismissed = true;
+ }
if (isFocusedTaskDismissed) {
if (isSplitSelectionActive()) {
isStagingFocusedTask = true;
} else {
nextFocusedTaskFromTop =
- mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
+ !mTopRowIdSet.isEmpty() && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
// Pick the next focused task from the preferred row.
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
- if (taskView == dismissedTaskView) {
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView == dismissedTaskView || taskView.isLargeTile()) {
continue;
}
boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
@@ -3524,15 +3869,16 @@ public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissed
}
}
}
- } else {
- getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
- getPageScrolls(newScroll, false,
- v -> v.getVisibility() != GONE && v != dismissedTaskView);
- if (count > 1) {
- scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
- }
}
+ getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
+ getPageScrolls(newScroll, false,
+ v -> v.getVisibility() != GONE && v != dismissedTaskView);
+ if (count > 1) {
+ scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
+ }
+
+ isSlidingTasks = isStagingFocusedTask || areAllDesktopTasksDismissed;
float dismissTranslationInterpolationEnd = 1;
boolean closeGapBetweenClearAll = false;
boolean isClearAllHidden = isClearAllHidden();
@@ -3543,31 +3889,46 @@ public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissed
int currentPageScroll = getScrollForPage(mCurrentPage);
int lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView));
boolean currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll;
- if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
+
+ int topGridRowSize = mTopRowIdSet.size();
+ int numLargeTiles = mUtils.getLargeTileCount();
+ int bottomGridRowSize = taskCount - mTopRowIdSet.size() - numLargeTiles;
+ boolean topRowLonger = topGridRowSize > bottomGridRowSize;
+ boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
+ boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
+ boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
+ if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) {
+ topGridRowSize--;
+ }
+ if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) {
+ bottomGridRowSize--;
+ }
+ int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize)
+ * (mLastComputedGridTaskSize.width() + mPageSpacing);
+ if (!enableGridOnlyOverview() && !isStagingFocusedTask) {
+ longRowWidth += mLastComputedTaskSize.width() + mPageSpacing;
+ }
+ // Compensate the removed gap if we don't already have shortTotalCompensation,
+ // and adjust accordingly to the new shortTotalCompensation after dismiss.
+ int newClearAllShortTotalWidthTranslation = 0;
+ if (mClearAllShortTotalWidthTranslation == 0) {
+ // If first task is not in the expected position (mLastComputedTaskSize) and being too
+ // close to ClearAllButton, then apply extra translation to ClearAllButton.
+ int firstTaskStart = mLastComputedGridSize.left + longRowWidth;
+ int expectedFirstTaskStart = mLastComputedTaskSize.right;
+ if (firstTaskStart < expectedFirstTaskStart) {
+ newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
+ }
+ }
+ if (lastGridTaskView != null && (
+ (!isExpressiveDismiss && lastGridTaskView.isVisibleToUser()) || (isExpressiveDismiss
+ && (isTaskViewVisible(lastGridTaskView)
+ || lastGridTaskView == dismissedTaskView)))) {
// After dismissal, animate translation of the remaining tasks to fill any gap left
// between the end of the grid and the clear all button. Only animate if the clear
// all button is visible or would become visible after dismissal.
float longGridRowWidthDiff = 0;
- int topGridRowSize = mTopRowIdSet.size();
- int bottomGridRowSize = taskCount - mTopRowIdSet.size()
- - (enableGridOnlyOverview() ? 0 : 1);
- boolean topRowLonger = topGridRowSize > bottomGridRowSize;
- boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
- boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
- boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
- if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) {
- topGridRowSize--;
- }
- if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) {
- bottomGridRowSize--;
- }
- int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize)
- * (mLastComputedGridTaskSize.width() + mPageSpacing);
- if (!enableGridOnlyOverview() && !isStagingFocusedTask) {
- longRowWidth += mLastComputedTaskSize.width() + mPageSpacing;
- }
-
float gapWidth = 0;
if ((topRowLonger && dismissedTaskFromTop)
|| (bottomRowLonger && dismissedTaskFromBottom)) {
@@ -3579,17 +3940,6 @@ public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissed
}
if (gapWidth > 0) {
if (mClearAllShortTotalWidthTranslation == 0) {
- // Compensate the removed gap if we don't already have shortTotalCompensation,
- // and adjust accordingly to the new shortTotalCompensation after dismiss.
- int newClearAllShortTotalWidthTranslation = 0;
- if (longRowWidth < mLastComputedGridSize.width()) {
- DeviceProfile deviceProfile = mContainer.getDeviceProfile();
- newClearAllShortTotalWidthTranslation =
- (mIsRtl
- ? mLastComputedTaskSize.right
- : deviceProfile.widthPx - mLastComputedTaskSize.left)
- - longRowWidth - deviceProfile.overviewGridSideMargin;
- }
float gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation;
longGridRowWidthDiff += mIsRtl ? -gapCompensation : gapCompensation;
}
@@ -3618,6 +3968,22 @@ public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissed
// the only invariant point in landscape split screen.
snapToLastTask = true;
}
+ if (mUtils.getGridTaskCount() == 1 && dismissedTaskView.isGridTask()) {
+ TaskView lastLargeTile = mUtils.getLastLargeTaskView();
+ if (lastLargeTile != null) {
+ // Calculate the distance to put last large tile back to middle of the screen.
+ int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
+ int lastLargeTileScroll = getScrollForPage(indexOfChild(lastLargeTile));
+ longGridRowWidthDiff = primaryScroll - lastLargeTileScroll;
+
+ if (!isClearAllHidden) {
+ // If ClearAllButton is visible, reduce the distance by scroll difference
+ // between ClearAllButton and the last task.
+ longGridRowWidthDiff += getLastTaskScroll(/*clearAllScroll=*/0,
+ getPagedOrientationHandler().getPrimarySize(mClearAllButton));
+ }
+ }
+ }
// If we need to animate the grid to compensate the clear all gap, we split the second
// half of the dismiss pending animation (in which the non-dismissed tasks slide into
@@ -3635,8 +4001,7 @@ public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissed
END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ (taskCount - 1) * halfAdditionalDismissTranslationOffset,
END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
+ for (TaskView taskView : getTaskViews()) {
anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff,
clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1));
dismissTranslationInterpolationEnd = Utilities.boundToRange(
@@ -3672,140 +4037,99 @@ public void onAnimationEnd(Animator animation) {
SplitAnimationTimings splitTimings =
AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().isTablet);
- int distanceFromDismissedTask = 0;
+ int distanceFromDismissedTask = 1;
+ int slidingTranslation = 0;
+ if (isSlidingTasks) {
+ int nextSnappedPage = indexOfChild(isStagingFocusedTask
+ ? mUtils.getFirstSmallTaskView()
+ : mUtils.getFirstNonDesktopTaskView());
+ slidingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
+ - getScrollForPage(nextSnappedPage);
+ slidingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
+ : -newClearAllShortTotalWidthTranslation;
+ }
+ mTaskViewsDismissPrimaryTranslations.clear();
+ int lastTaskViewIndex = indexOfChild(mUtils.getLastTaskView());
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child == dismissedTaskView) {
- if (animateTaskView) {
- if (dismissingForSplitSelection) {
- createInitialSplitSelectAnimation(anim);
- } else {
- addDismissedTaskAnimations(dismissedTaskView, duration, anim);
- }
+ if (animateTaskView && !dismissingForSplitSelection) {
+ addDismissedTaskAnimations(dismissedTaskView, duration, anim);
}
- } else if (!showAsGrid) {
- // Compute scroll offsets from task dismissal for animation.
- // If we just take newScroll - oldScroll, everything to the right of dragged task
- // translates to the left. We need to offset this in some cases:
- // - In RTL, add page offset to all pages, since we want pages to move to the right
- // Additionally, add a page offset if:
- // - Current page is rightmost page (leftmost for RTL)
- // - Dragging an adjacent page on the left side (right side for RTL)
- int offset = mIsRtl ? scrollDiffPerPage : 0;
- if (mCurrentPage == dismissedIndex) {
- int lastPage = taskCount - 1;
- if (mCurrentPage == lastPage) {
- offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
- }
- } else {
- // Dismissing an adjacent page.
- int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
- if (dismissedIndex == negativeAdjacent) {
- offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
- }
- }
-
+ } else if (!showAsGrid || (enableLargeDesktopWindowingTile()
+ && dismissedTaskView != null && dismissedTaskView.isLargeTile()
+ && nextFocusedTaskView == null && !dismissingForSplitSelection)) {
+ int offset = getOffsetToDismissedTask(scrollDiffPerPage, dismissedIndex,
+ lastTaskViewIndex);
int scrollDiff = newScroll[i] - oldScroll[i] + offset;
if (scrollDiff != 0) {
- FloatProperty translationProperty = child instanceof TaskView
- ? ((TaskView) child).getPrimaryDismissTranslationProperty()
- : getPagedOrientationHandler().getPrimaryViewTranslate();
-
- float additionalDismissDuration =
- ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
- i - dismissedIndex);
-
- // We are in non-grid layout.
- // If dismissing for split select, use split timings.
- // If not, use dismiss timings.
- float animationStartProgress = isSplitSelectionActive()
- ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f)
- : Utilities.boundToRange(
- INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
- + additionalDismissDuration, 0f, 1f);
-
- float animationEndProgress = isSplitSelectionActive()
- ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
- + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
- : 1f;
-
- // Slide tiles in horizontally to fill dismissed area
- anim.setFloat(child, translationProperty, scrollDiff,
- clampToProgress(
- splitTimings.getGridSlidePrimaryInterpolator(),
- animationStartProgress,
- animationEndProgress
- )
- );
-
- if (mEnableDrawingLiveTile && child instanceof TaskView
- && ((TaskView) child).isRunningTask()) {
- anim.addOnFrameCallback(() -> {
- runActionOnRemoteHandles(
- remoteTargetHandle ->
- remoteTargetHandle.getTaskViewSimulator()
- .taskPrimaryTranslation.value =
- getPagedOrientationHandler().getPrimaryValue(
- child.getTranslationX(),
- child.getTranslationY()
- ));
- redrawLiveTile();
- });
- }
- needsCurveUpdates = true;
- }
- } else if (child instanceof TaskView) {
- TaskView taskView = (TaskView) child;
- if (isFocusedTaskDismissed) {
- if (nextFocusedTaskView != null &&
- !isSameGridRow(taskView, nextFocusedTaskView)) {
- continue;
+ if (!isExpressiveDismiss) {
+ translateTaskWhenDismissed(
+ child,
+ Math.abs(i - dismissedIndex),
+ scrollDiff,
+ anim,
+ splitTimings);
}
- } else {
- if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
- continue;
+ if (child instanceof TaskView taskView) {
+ mTaskViewsDismissPrimaryTranslations.put(taskView, scrollDiffPerPage);
}
+ needsCurveUpdates = true;
}
+ } else if (child instanceof TaskView taskView) {
// Animate task with index >= dismissed index and in the same row as the
// dismissed index or next focused index. Offset successive task dismissal
// durations for a staggered effect.
- distanceFromDismissedTask++;
- int staggerColumn = isStagingFocusedTask
+ int staggerColumn = isSlidingTasks
? (int) Math.ceil(distanceFromDismissedTask / 2f)
: distanceFromDismissedTask;
// Set timings based on if user is initiating splitscreen on the focused task,
// or splitting/dismissing some other task.
- float animationStartProgress = isStagingFocusedTask
- ? Utilities.boundToRange(
- splitTimings.getGridSlideStartOffset()
- + (splitTimings.getGridSlideStaggerOffset()
- * staggerColumn),
- 0f,
- dismissTranslationInterpolationEnd)
- : Utilities.boundToRange(
- INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
- + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
- * staggerColumn, 0f, dismissTranslationInterpolationEnd);
- float animationEndProgress = isStagingFocusedTask
- ? Utilities.boundToRange(
- splitTimings.getGridSlideStartOffset()
- + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
- + splitTimings.getGridSlideDurationOffset(),
- 0f,
- dismissTranslationInterpolationEnd)
- : dismissTranslationInterpolationEnd;
- Interpolator dismissInterpolator = isStagingFocusedTask ? OVERSHOOT_0_75 : LINEAR;
+ final float animationStartProgress;
+ if (isSlidingTasks) {
+ float slidingStartOffset = splitTimings.getGridSlideStartOffset()
+ + (splitTimings.getGridSlideStaggerOffset() * staggerColumn);
+ if (areAllDesktopTasksDismissed) {
+ animationStartProgress = Utilities.boundToRange(
+ slidingStartOffset
+ + splitTimings.getDesktopFadeSplitAnimationEndOffset(),
+ 0f,
+ dismissTranslationInterpolationEnd);
+ } else {
+ animationStartProgress = Utilities.boundToRange(
+ slidingStartOffset,
+ 0f,
+ dismissTranslationInterpolationEnd);
+ }
+ } else {
+ animationStartProgress = Utilities.boundToRange(
+ INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ * staggerColumn, 0f, dismissTranslationInterpolationEnd);
+ }
+
+ final float animationEndProgress;
+ if (isSlidingTasks && taskView != nextFocusedTaskView) {
+ animationEndProgress = Utilities.boundToRange(
+ splitTimings.getGridSlideStartOffset()
+ + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
+ + splitTimings.getGridSlideDurationOffset(),
+ 0f,
+ dismissTranslationInterpolationEnd);
+ } else {
+ animationEndProgress = dismissTranslationInterpolationEnd;
+ }
+ Interpolator dismissInterpolator = isSlidingTasks ? EMPHASIZED : LINEAR;
+
+ float primaryTranslation = 0;
if (taskView == nextFocusedTaskView) {
// Enlarge the task to be focused next, and translate into focus position.
float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width();
anim.setFloat(taskView, TaskView.DISMISS_SCALE, scale,
clampToProgress(LINEAR, animationStartProgress,
dismissTranslationInterpolationEnd));
- anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
- mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth,
- clampToProgress(LINEAR, animationStartProgress,
- dismissTranslationInterpolationEnd));
+ primaryTranslation += dismissedTaskWidth;
float secondaryTranslation = -mTaskGridVerticalDiff;
if (!nextFocusedTaskFromTop) {
secondaryTranslation -= mTopBottomRowHeightDiff;
@@ -3813,27 +4137,45 @@ public void onAnimationEnd(Animator animation) {
anim.setFloat(taskView, taskView.getSecondaryDismissTranslationProperty(),
secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
dismissTranslationInterpolationEnd));
- anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
+ anim.add(taskView.getDismissIconFadeOutAnimator(),
clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
- } else {
- float primaryTranslation =
+ } else if ((isFocusedTaskDismissed && nextFocusedTaskView != null && isSameGridRow(
+ taskView, nextFocusedTaskView))
+ || (!isFocusedTaskDismissed && i >= dismissedIndex && isSameGridRow(
+ taskView, dismissedTaskView))) {
+ primaryTranslation +=
nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth;
- if (isStagingFocusedTask) {
- // Moves less if focused task is not in scroll position.
- int focusedTaskScroll = getScrollForPage(dismissedIndex);
- int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
- int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll;
- primaryTranslation +=
- mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff;
- }
+ }
+ if (!(taskView instanceof DesktopTaskView)) {
+ primaryTranslation += mIsRtl ? slidingTranslation : -slidingTranslation;
+ }
- anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
- mIsRtl ? primaryTranslation : -primaryTranslation,
- clampToProgress(dismissInterpolator, animationStartProgress,
- animationEndProgress));
+ if (primaryTranslation != 0) {
+ float finalTranslation = mIsRtl ? primaryTranslation : -primaryTranslation;
+ float startTranslation = 0;
+ if (!(taskView instanceof DesktopTaskView) && slidingTranslation != 0) {
+ startTranslation = isTaskViewVisible(taskView) ? 0
+ : finalTranslation + (mIsRtl ? -mLastComputedTaskSize.right
+ : mLastComputedTaskSize.right);
+ }
+ // Expressive dismiss will animate the translations of taskViews itself.
+ if (!isExpressiveDismiss) {
+ Animator dismissAnimator = ObjectAnimator.ofFloat(taskView,
+ taskView.getPrimaryDismissTranslationProperty(),
+ startTranslation, finalTranslation);
+ dismissAnimator.setInterpolator(
+ clampToProgress(dismissInterpolator, animationStartProgress,
+ animationEndProgress));
+ anim.add(dismissAnimator);
+ }
+ mTaskViewsDismissPrimaryTranslations.put(taskView, (int) finalTranslation);
+ distanceFromDismissedTask++;
}
}
}
+ if (dismissingForSplitSelection) {
+ createInitialSplitSelectAnimation(anim);
+ }
if (needsCurveUpdates) {
anim.addOnFrameCallback(this::updateCurveProperties);
@@ -3842,21 +4184,26 @@ secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
// Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant
// (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we
// want the dragged task to stay above all other views.
- if (animateTaskView) {
+ if (animateTaskView && dismissedTaskView != null) {
dismissedTaskView.setTranslationZ(0.1f);
}
-
+ loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+ if (!dismissingForSplitSelection) {
+ anim.addStartListener(() -> InteractionJankMonitorWrapper.begin(this,
+ Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS));
+ }
mPendingAnimation = anim;
final TaskView finalNextFocusedTaskView = nextFocusedTaskView;
final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll;
final boolean finalSnapToLastTask = snapToLastTask;
final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed;
- mPendingAnimation.addEndListener(new Consumer() {
+ mPendingAnimation.addEndListener(new Consumer<>() {
@Override
public void accept(Boolean success) {
- if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) {
+ if (mEnableDrawingLiveTile && dismissedTaskView != null
+ && dismissedTaskView.isRunningTask() && success) {
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
- () -> onEnd(success));
+ () -> onEnd(true));
} else {
onEnd(success);
}
@@ -3869,17 +4216,16 @@ private void onEnd(boolean success) {
resetTaskVisuals();
if (success) {
- if (shouldRemoveTask) {
+ mAnyTaskHasBeenDismissed = true;
+ if (shouldRemoveTask && dismissedTaskView != null) {
if (dismissedTaskView.isRunningTask()) {
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
- () -> removeTaskInternal(dismissedTaskViewId));
+ () -> removeTaskInternal(dismissedTaskView));
} else {
- removeTaskInternal(dismissedTaskViewId);
+ removeTaskInternal(dismissedTaskView);
}
- announceForAccessibility(
- getResources().getString(R.string.task_view_closed));
mContainer.getStatsLogManager().logger()
- .withItemInfo(dismissedTaskView.getFirstItemInfo())
+ .withItemInfo(dismissedTaskView.getItemInfo())
.log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
}
@@ -3895,7 +4241,7 @@ private void onEnd(boolean success) {
pageToSnapTo = indexOfChild(mClearAllButton);
} else if (isClearAllHidden) {
// Snap to focused task if clear all is hidden.
- pageToSnapTo = 0;
+ pageToSnapTo = indexOfChild(getFirstTaskView());
}
} else {
// Get the id of the task view we will snap to based on the current
@@ -3913,7 +4259,7 @@ private void onEnd(boolean success) {
} else {
// Won't focus next task in split select, so snap to the
// first task.
- pageToSnapTo = 0;
+ pageToSnapTo = indexOfChild(getFirstTaskView());
calculateScrollDiff = false;
}
} else {
@@ -3921,8 +4267,8 @@ private void onEnd(boolean success) {
boolean isSnappedTaskInTopRow = mTopRowIdSet.contains(
snappedTaskViewId);
IntArray taskViewIdArray =
- isSnappedTaskInTopRow ? getTopRowIdArray()
- : getBottomRowIdArray();
+ isSnappedTaskInTopRow ? mUtils.getTopRowIdArray()
+ : mUtils.getBottomRowIdArray();
int snappedIndex = taskViewIdArray.indexOf(snappedTaskViewId);
taskViewIdArray.removeValue(dismissedTaskViewId);
if (finalNextFocusedTaskView != null) {
@@ -3937,8 +4283,8 @@ private void onEnd(boolean success) {
// dismissed row,
// snap to the same column in the other grid row
IntArray inverseRowTaskViewIdArray =
- isSnappedTaskInTopRow ? getBottomRowIdArray()
- : getTopRowIdArray();
+ isSnappedTaskInTopRow ? mUtils.getBottomRowIdArray()
+ : mUtils.getTopRowIdArray();
if (snappedIndex < inverseRowTaskViewIdArray.size()) {
taskViewIdToSnapTo = inverseRowTaskViewIdArray.get(
snappedIndex);
@@ -3954,7 +4300,7 @@ private void onEnd(boolean success) {
mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
}
}
- } else if (dismissedIndex < pageToSnapTo || pageToSnapTo == taskCount - 1) {
+ } else if (dismissedIndex < pageToSnapTo || pageToSnapTo == lastTaskViewIndex) {
pageToSnapTo--;
}
boolean isHomeTaskDismissed = dismissedTaskView == getHomeTaskView();
@@ -3963,6 +4309,7 @@ private void onEnd(boolean success) {
if (taskCount == 1) {
removeViewInLayout(mClearAllButton);
+ removeViewInLayout(mAddDesktopButton);
if (isHomeTaskDismissed) {
updateEmptyMessage();
} else if (!mSplitSelectStateController.isSplitSelectActive()) {
@@ -3971,28 +4318,28 @@ private void onEnd(boolean success) {
} else {
// Update focus task and its size.
if (finalIsFocusedTaskDismissed && finalNextFocusedTaskView != null) {
- mFocusedTaskViewId = enableGridOnlyOverview()
+ setFocusedTaskViewId(enableGridOnlyOverview()
? INVALID_TASK_ID
- : finalNextFocusedTaskView.getTaskViewId();
+ : finalNextFocusedTaskView.getTaskViewId());
mTopRowIdSet.remove(mFocusedTaskViewId);
- finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
+ finalNextFocusedTaskView.getDismissIconFadeInAnimator().start();
}
- updateTaskSize(/*isTaskDismissal=*/ true);
- updateChildTaskOrientations();
+ updateTaskSize();
+ mUtils.updateChildTaskOrientations();
// Update scroll and snap to page.
updateScrollSynchronously();
if (showAsGrid) {
// Rebalance tasks in the grid
- int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
- if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
- TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex);
-
+ TaskView highestVisibleTaskView = getHighestVisibleTaskView();
+ if (highestVisibleTaskView != null) {
boolean shouldRebalance;
int screenStart = getPagedOrientationHandler().getPrimaryScroll(
RecentsView.this);
- int taskStart = getPagedOrientationHandler().getChildStart(taskView)
- + (int) taskView.getOffsetAdjustment(/*gridEnabled=*/ true);
+ int taskStart = getPagedOrientationHandler().getChildStart(
+ highestVisibleTaskView)
+ + (int) highestVisibleTaskView.getOffsetAdjustment(
+ /*gridEnabled=*/true);
// Rebalance only if there is a maximum gap between the task and the
// screen's edge; this ensures that rebalanced tasks are outside the
@@ -4005,26 +4352,33 @@ private void onEnd(boolean success) {
RecentsView.this);
int taskSize = (int) (
getPagedOrientationHandler().getMeasuredSize(
- taskView) * taskView
- .getSizeAdjustment(/*fullscreenEnabled=*/false));
+ highestVisibleTaskView) * highestVisibleTaskView
+ .getSizeAdjustment(/*fullscreenEnabled=*/
+ false));
int taskEnd = taskStart + taskSize;
shouldRebalance = taskEnd >= screenEnd - mPageSpacing;
}
if (shouldRebalance) {
- updateGridProperties(/*isTaskDismissal=*/ true,
- highestVisibleTaskIndex);
+ updateGridProperties(highestVisibleTaskView);
updateScrollSynchronously();
}
}
- IntArray topRowIdArray = getTopRowIdArray();
- IntArray bottomRowIdArray = getBottomRowIdArray();
+ IntArray topRowIdArray = mUtils.getTopRowIdArray();
+ IntArray bottomRowIdArray = mUtils.getBottomRowIdArray();
if (finalSnapToLastTask) {
// If snapping to last task, find the last task after dismissal.
pageToSnapTo = indexOfChild(
getLastGridTaskView(topRowIdArray, bottomRowIdArray));
+
+ if (pageToSnapTo == INVALID_PAGE) {
+ // Snap to latest large tile page after dismissing the
+ // last grid task. This will prevent snapping to page 0 when
+ // desktop task is visible as large tile.
+ pageToSnapTo = indexOfChild(mUtils.getLastLargeTaskView());
+ }
} else if (taskViewIdToSnapTo != -1) {
// If snapping to another page due to indices rearranging, find
// the new index after dismissal & rearrange using the task view id.
@@ -4053,10 +4407,105 @@ private void onEnd(boolean success) {
updateCurrentTaskActionsVisibility();
onDismissAnimationEnds();
mPendingAnimation = null;
+ mTaskViewsDismissPrimaryTranslations.clear();
+
+ if (!dismissingForSplitSelection && success) {
+ InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS);
+ } else if (!dismissingForSplitSelection) {
+ InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS);
+ }
}
});
}
+ /**
+ * Compute scroll offsets from task dismissal for animation.
+ * If we just take newScroll - oldScroll, everything to the right of dragged task
+ * translates to the left. We need to offset this in some cases:
+ * - In RTL, add page offset to all pages, since we want pages to move to the right
+ * Additionally, add a page offset if:
+ * - Current page is rightmost page (leftmost for RTL)
+ * - Dragging an adjacent page on the left side (right side for RTL)
+ */
+ private int getOffsetToDismissedTask(int scrollDiffPerPage, int dismissedIndex,
+ int lastTaskViewIndex) {
+ // If `mCurrentPage` is beyond `lastTaskViewIndex`, use the last TaskView instead to
+ // calculate offset.
+ int currentPage = Math.min(mCurrentPage, lastTaskViewIndex);
+ int offset = mIsRtl ? scrollDiffPerPage : 0;
+ if (currentPage == dismissedIndex) {
+ if (currentPage == lastTaskViewIndex) {
+ offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+ }
+ } else {
+ // Dismissing an adjacent page.
+ int negativeAdjacent = currentPage - 1; // (Right in RTL, left in LTR)
+ if (dismissedIndex == negativeAdjacent) {
+ offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+ }
+ }
+ return offset;
+ }
+
+ private void translateTaskWhenDismissed(
+ View view,
+ int indexDiff,
+ int scrollDiffPerPage,
+ PendingAnimation pendingAnimation,
+ SplitAnimationTimings splitTimings) {
+ // No need to translate the AddDesktopButton on dismissing a TaskView, which should be
+ // always at the right most position, even when dismissing the last TaskView.
+ if (view instanceof AddDesktopButton) {
+ return;
+ }
+ FloatProperty translationProperty = view instanceof TaskView
+ ? ((TaskView) view).getPrimaryDismissTranslationProperty()
+ : getPagedOrientationHandler().getPrimaryViewTranslate();
+
+ float additionalDismissDuration =
+ ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * indexDiff;
+
+ // We are in non-grid layout.
+ // If dismissing for split select, use split timings.
+ // If not, use dismiss timings.
+ float animationStartProgress = isSplitSelectionActive()
+ ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f)
+ : Utilities.boundToRange(
+ INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ + additionalDismissDuration, 0f, 1f);
+
+ float animationEndProgress = isSplitSelectionActive()
+ ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
+ + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
+ : 1f;
+
+ // Slide tiles in horizontally to fill dismissed area
+ pendingAnimation.setFloat(
+ view,
+ translationProperty,
+ scrollDiffPerPage,
+ clampToProgress(
+ splitTimings.getGridSlidePrimaryInterpolator(),
+ animationStartProgress,
+ animationEndProgress
+ )
+ );
+ if (mEnableDrawingLiveTile && view instanceof TaskView
+ && ((TaskView) view).isRunningTask()) {
+ pendingAnimation.addOnFrameCallback(() -> {
+ runActionOnRemoteHandles(
+ remoteTargetHandle ->
+ remoteTargetHandle.getTaskViewSimulator()
+ .taskPrimaryTranslation.value =
+ getPagedOrientationHandler().getPrimaryValue(
+ view.getTranslationX(),
+ view.getTranslationY()
+ ));
+ redrawLiveTile();
+ });
+ }
+ }
+
/**
* Hides all overview actions if user is halfway through split selection, shows otherwise.
* We only show split option if:
@@ -4068,9 +4517,6 @@ private void updateCurrentTaskActionsVisibility() {
boolean isCurrentSplit = taskView instanceof GroupedTaskView;
GroupedTaskView groupedTaskView = isCurrentSplit ? (GroupedTaskView) taskView : null;
// Update flags to see if entire actions bar should be hidden.
- if (!FeatureFlags.enableAppPairs()) {
- mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit);
- }
mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SELECT_ACTIVE, isSplitSelectionActive());
// Update flags to see if actions bar should show buttons for a single task or a pair of
// tasks.
@@ -4087,55 +4533,18 @@ public boolean supportsAppPairs() {
return true;
}
- /**
- * Returns all the tasks in the top row, without the focused task
- */
- private IntArray getTopRowIdArray() {
- if (mTopRowIdSet.isEmpty()) {
- return new IntArray(0);
- }
- IntArray topArray = new IntArray(mTopRowIdSet.size());
- int taskViewCount = getTaskViewCount();
- for (int i = 0; i < taskViewCount; i++) {
- int taskViewId = requireTaskViewAt(i).getTaskViewId();
- if (mTopRowIdSet.contains(taskViewId)) {
- topArray.add(taskViewId);
- }
- }
- return topArray;
- }
-
- /**
- * Returns all the tasks in the bottom row, without the focused task
- */
- private IntArray getBottomRowIdArray() {
- int bottomRowIdArraySize = getBottomRowTaskCountForTablet();
- if (bottomRowIdArraySize <= 0) {
- return new IntArray(0);
- }
- IntArray bottomArray = new IntArray(bottomRowIdArraySize);
- int taskViewCount = getTaskViewCount();
- for (int i = 0; i < taskViewCount; i++) {
- int taskViewId = requireTaskViewAt(i).getTaskViewId();
- if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
- bottomArray.add(taskViewId);
- }
- }
- return bottomArray;
- }
-
/**
* Iterate the grid by columns instead of by TaskView index, starting after the focused task and
* up to the last balanced column.
*
- * @return the highest visible TaskView index between both rows
+ * @return the highest visible TaskView between both rows
*/
- private int getHighestVisibleTaskIndex() {
- if (mTopRowIdSet.isEmpty()) return Integer.MAX_VALUE; // return earlier
+ private TaskView getHighestVisibleTaskView() {
+ if (mTopRowIdSet.isEmpty()) return null; // return earlier
- int lastVisibleIndex = Integer.MAX_VALUE;
- IntArray topRowIdArray = getTopRowIdArray();
- IntArray bottomRowIdArray = getBottomRowIdArray();
+ TaskView lastVisibleTaskView = null;
+ IntArray topRowIdArray = mUtils.getTopRowIdArray();
+ IntArray bottomRowIdArray = mUtils.getBottomRowIdArray();
int balancedColumns = Math.min(bottomRowIdArray.size(), topRowIdArray.size());
for (int i = 0; i < balancedColumns; i++) {
@@ -4143,25 +4552,42 @@ private int getHighestVisibleTaskIndex() {
if (isTaskViewVisible(topTask)) {
TaskView bottomTask = getTaskViewFromTaskViewId(bottomRowIdArray.get(i));
- lastVisibleIndex = Math.max(indexOfChild(topTask), indexOfChild(bottomTask));
- } else if (lastVisibleIndex < Integer.MAX_VALUE) {
+ lastVisibleTaskView =
+ indexOfChild(topTask) > indexOfChild(bottomTask) ? topTask : bottomTask;
+ } else if (lastVisibleTaskView != null) {
break;
}
}
- return lastVisibleIndex;
+ return lastVisibleTaskView;
}
- private void removeTaskInternal(int dismissedTaskViewId) {
- int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId);
- UI_HELPER_EXECUTOR.getHandler().post(
- () -> {
- for (int taskId : taskIds) {
- if (taskId != -1) {
- ActivityManagerWrapper.getInstance().removeTask(taskId);
- }
- }
- });
+ private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
+ UI_HELPER_EXECUTOR
+ .getHandler()
+ .post(
+ () -> {
+ if (dismissedTaskView instanceof DesktopTaskView desktopTaskView) {
+ removeDesktopTaskView(desktopTaskView);
+ } else {
+ for (int taskId : dismissedTaskView.getTaskIds()) {
+ ActivityManagerWrapper.getInstance().removeTask(taskId);
+ }
+ }
+ });
+ }
+
+ private void removeDesktopTaskView(DesktopTaskView desktopTaskView) {
+ if (areMultiDesksFlagsEnabled()) {
+ SystemUiProxy.INSTANCE
+ .get(getContext())
+ .removeDesk(desktopTaskView.getDeskId());
+ } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+ SystemUiProxy.INSTANCE
+ .get(getContext())
+ .removeDefaultDeskInDisplay(
+ mContainer.getDisplay().getDisplayId());
+ }
}
protected void onDismissAnimationEnds() {
@@ -4175,19 +4601,24 @@ public PendingAnimation createAllTasksDismissAnimation(long duration) {
}
PendingAnimation anim = new PendingAnimation(duration);
- int count = getTaskViewCount();
- for (int i = 0; i < count; i++) {
- addDismissedTaskAnimations(requireTaskViewAt(i), duration, anim);
+ for (TaskView taskView : getTaskViews()) {
+ addDismissedTaskAnimations(taskView, duration, anim);
}
mPendingAnimation = anim;
mPendingAnimation.addEndListener(isSuccess -> {
if (isSuccess) {
+ // Remove desktops first, since desks can be empty (so they have no recent tasks),
+ // and closing all tasks on a desk doesn't always necessarily mean that the desk
+ // will be removed. So, there are no guarantees that the below call to
+ // `ActivityManagerWrapper::removeAllRecentTasks()` will be enough.
+ SystemUiProxy.INSTANCE.get(getContext()).removeAllDesks();
+
// Remove all the task views now
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
UI_HELPER_EXECUTOR.getHandler().post(
ActivityManagerWrapper.getInstance()::removeAllRecentTasks);
- removeTasksViewsAndClearAllButton();
+ removeAllTaskViews();
startHome();
});
}
@@ -4197,7 +4628,7 @@ public PendingAnimation createAllTasksDismissAnimation(long duration) {
}
private boolean snapToPageRelative(int delta, boolean cycle,
- @TaskGridNavHelper.TASK_NAV_DIRECTION int direction) {
+ TaskGridNavHelper.TaskNavDirection direction) {
// Set next page if scroll animation is still running, otherwise cannot snap to the
// next page on successive key presses. Setting the current page aborts the scroll.
if (!mScroller.isFinished()) {
@@ -4216,32 +4647,41 @@ private boolean snapToPageRelative(int delta, boolean cycle,
return true;
}
- private int getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction,
+ private int getNextPageInternal(int delta, TaskGridNavHelper.TaskNavDirection direction,
boolean cycle) {
if (!showAsGrid()) {
return getNextPage() + delta;
}
// Init task grid nav helper with top/bottom id arrays.
- TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
- getBottomRowIdArray(), mFocusedTaskViewId);
+ TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(mUtils.getTopRowIdArray(),
+ mUtils.getBottomRowIdArray(), mUtils.getLargeTaskViewIds(),
+ mAddDesktopButton != null);
// Get current page's task view ID.
TaskView currentPageTaskView = getCurrentPageTaskView();
int currentPageTaskViewId;
+ final int clearAllButtonIndex = indexOfChild(mClearAllButton);
+ final int addDesktopButtonIndex = indexOfChild(mAddDesktopButton);
if (currentPageTaskView != null) {
currentPageTaskViewId = currentPageTaskView.getTaskViewId();
- } else if (mCurrentPage == indexOfChild(mClearAllButton)) {
+ } else if (mCurrentPage == clearAllButtonIndex) {
currentPageTaskViewId = TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
+ } else if (mCurrentPage == addDesktopButtonIndex) {
+ currentPageTaskViewId = TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID;
} else {
return INVALID_PAGE;
}
- int nextGridPage =
+ final int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
- return nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
- ? indexOfChild(mClearAllButton)
- : indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
+ if (nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID) {
+ return clearAllButtonIndex;
+ }
+ if (nextGridPage == TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID) {
+ return addDesktopButtonIndex;
+ }
+ return indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
}
private void runDismissAnimation(PendingAnimation pendingAnim) {
@@ -4252,18 +4692,39 @@ private void runDismissAnimation(PendingAnimation pendingAnim) {
}
@UiThread
- private void dismissTask(int taskId) {
+ public void dismissTask(int taskId, boolean animate, boolean removeTask) {
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView == null) {
+ Log.d(TAG, "dismissTask: " + taskId + ", no associated TaskView");
return;
}
- dismissTask(taskView, true /* animate */, false /* removeTask */);
+ Log.d(TAG, "dismissTask: " + taskId);
+
+ if (enableDesktopExplodedView() && taskView instanceof DesktopTaskView desktopTaskView) {
+ desktopTaskView.removeTaskFromExplodedView(taskId, animate);
+
+ if (removeTask) {
+ ActivityManagerWrapper.getInstance().removeTask(taskId);
+ }
+ } else {
+ dismissTaskView(taskView, animate, removeTask);
+ }
}
- public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
+ /** Dismisses the entire [taskView]. */
+ public void dismissTaskView(TaskView taskView, boolean animateTaskView, boolean removeTask) {
PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION);
createTaskDismissAnimation(pa, taskView, animateTaskView, removeTask, DISMISS_TASK_DURATION,
- false /* dismissingForSplitSelection*/);
+ false /* dismissingForSplitSelection*/, false /* isExpressiveDismiss */);
+ runDismissAnimation(pa);
+ }
+
+ protected void expressiveDismissTaskView(TaskView taskView, Function0 onEndRunnable) {
+ PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION);
+ createTaskDismissAnimation(pa, taskView, false /* animateTaskView */, true /* removeTask */,
+ DISMISS_TASK_DURATION, false /* dismissingForSplitSelection*/,
+ true /* isExpressiveDismiss */);
+ pa.addEndListener((success) -> onEndRunnable.invoke());
runDismissAnimation(pa);
}
@@ -4276,27 +4737,42 @@ private void dismissAllTasks(View view) {
private void dismissCurrentTask() {
TaskView taskView = getNextPageTaskView();
if (taskView != null) {
- dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
+ dismissTaskView(taskView, true /*animateTaskView*/, true /*removeTask*/);
}
}
+ private void createDesk(View view) {
+ SystemUiProxy.INSTANCE
+ .get(getContext())
+ .createDesk(mContainer.getDisplay().getDisplayId());
+ }
+
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (isHandlingTouch() || event.getAction() != KeyEvent.ACTION_DOWN) {
return super.dispatchKeyEvent(event);
}
+
+ if (mUtils.shouldInterceptKeyEvent(event)) {
+ return super.dispatchKeyEvent(event);
+ }
+
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_TAB:
return snapToPageRelative(event.isShiftPressed() ? -1 : 1, true /* cycle */,
- DIRECTION_TAB);
+ TaskGridNavHelper.TaskNavDirection.TAB);
case KeyEvent.KEYCODE_DPAD_RIGHT:
- return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */, DIRECTION_RIGHT);
+ return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.RIGHT);
case KeyEvent.KEYCODE_DPAD_LEFT:
- return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */, DIRECTION_LEFT);
+ return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.LEFT);
case KeyEvent.KEYCODE_DPAD_UP:
- return snapToPageRelative(1, false /* cycle */, DIRECTION_UP);
+ return snapToPageRelative(1, false /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.UP);
case KeyEvent.KEYCODE_DPAD_DOWN:
- return snapToPageRelative(1, false /* cycle */, DIRECTION_DOWN);
+ return snapToPageRelative(1, false /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.DOWN);
case KeyEvent.KEYCODE_DEL:
case KeyEvent.KEYCODE_FORWARD_DEL:
dismissCurrentTask();
@@ -4340,15 +4816,14 @@ public void setContentAlpha(float alpha) {
alpha = Utilities.boundToRange(alpha, 0, 1);
mContentAlpha = alpha;
- TaskView runningTaskView = getRunningTaskView();
- for (int i = getTaskViewCount() - 1; i >= 0; i--) {
- TaskView child = requireTaskViewAt(i);
- if (runningTaskView != null && mRunningTaskTileHidden && child == runningTaskView) {
- continue;
- }
- child.setStableAlpha(alpha);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setStableAlpha(alpha);
}
mClearAllButton.setContentAlpha(mContentAlpha);
+
+ if (mAddDesktopButton != null) {
+ mAddDesktopButton.setContentAlpha(mContentAlpha);
+ }
int alphaInt = Math.round(alpha * 255);
mEmptyMessagePaint.setAlpha(alphaInt);
mEmptyIcon.setAlpha(alphaInt);
@@ -4404,6 +4879,12 @@ public void updateRecentsRotation() {
}
}
+ public void reapplyActiveRotation() {
+ RotationTouchHelper rotationTouchHelper = RotationTouchHelper.INSTANCE.get(getContext());
+ setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+ rotationTouchHelper.getDisplayRotation());
+ }
+
public void setLayoutRotation(int touchRotation, int displayRotation) {
if (mOrientationState.update(touchRotation, displayRotation)) {
updateOrientationHandler();
@@ -4423,6 +4904,20 @@ public TaskView getNextTaskView() {
return getTaskViewAt(getRunningTaskIndex() + 1);
}
+ @Nullable
+ public TaskView getPreviousTaskView() {
+ return getTaskViewAt(getRunningTaskIndex() - 1);
+ }
+
+ @Nullable
+ public TaskView getLastLargeTaskView() {
+ return mUtils.getLastLargeTaskView();
+ }
+
+ public int getLargeTilesCount() {
+ return mUtils.getLargeTileCount();
+ }
+
@Nullable
public TaskView getCurrentPageTaskView() {
return getTaskViewAt(getCurrentPage());
@@ -4448,11 +4943,10 @@ public TaskView getTaskViewAt(int index) {
}
/**
- * A version of {@link #getTaskViewAt} when the caller is sure about the input index.
+ * Returns iterable [TaskView] children.
*/
- @NonNull
- private TaskView requireTaskViewAt(int index) {
- return Objects.requireNonNull(getTaskViewAt(index));
+ public RecentsViewUtils.TaskViewsIterable getTaskViews() {
+ return mUtils.getTaskViews();
}
public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
@@ -4460,13 +4954,14 @@ public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener liste
}
public void updateEmptyMessage() {
- boolean isEmpty = getTaskViewCount() == 0;
+ boolean isEmpty = !hasTaskViews();
boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
|| mLastMeasureSize.y != getHeight();
if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
return;
}
setContentDescription(isEmpty ? mEmptyMessage : "");
+ setFocusable(isEmpty);
mShowEmptyMessage = isEmpty;
updateEmptyStateUi(hasSizeChanged);
invalidate();
@@ -4502,29 +4997,19 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto
}
private void updatePivots() {
- if (mOverviewSelectEnabled) {
- if (enableGridOnlyOverview()) {
- getModalTaskSize(mTempRect);
- Rect selectedTaskPosition = getSelectedTaskBounds();
- Utilities.getPivotsForScalingRectToRect(mTempRect, selectedTaskPosition,
- mTempPointF);
- } else {
- mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
- }
+ if (mOverviewSelectEnabled && !enableGridOnlyOverview()) {
+ mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
} else {
- // Only update pivot when it is tablet and not in grid yet, so the pivot is correct
- // for non-current tasks when swiping up to overview
- if (enableGridOnlyOverview() && mContainer.getDeviceProfile().isTablet
- && !mOverviewGridEnabled) {
- mTempRect.set(mLastComputedCarouselTaskSize);
- } else {
- mTempRect.set(mLastComputedTaskSize);
- }
+ mTempRect.set(mLastComputedTaskSize);
getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect,
mContainer.getDeviceProfile(), mTempPointF);
}
setPivotX(mTempPointF.x);
setPivotY(mTempPointF.y);
+ if (enableGridOnlyOverview()) {
+ runActionOnRemoteHandles(remoteTargetHandle ->
+ remoteTargetHandle.getTaskViewSimulator().setPivotOverride(mTempPointF));
+ }
}
/**
@@ -4551,10 +5036,15 @@ private void updatePageOffsets() {
? (runningTask == null ? INVALID_PAGE : indexOfChild(runningTask))
: mOffsetMidpointIndexOverride;
int modalMidpoint = getCurrentPage();
- boolean isModalGridWithoutFocusedTask =
- showAsGrid && enableGridOnlyOverview() && mTaskModalness > 0;
- if (isModalGridWithoutFocusedTask) {
- modalMidpoint = indexOfChild(mSelectedTask);
+ TaskView carouselHiddenMidpointTask = runningTask != null ? runningTask
+ : mUtils.getFirstTaskViewInCarousel(/*nonRunningTaskCarouselHidden=*/true,
+ /*runningTaskView=*/null);
+ int carouselHiddenMidpoint = indexOfChild(carouselHiddenMidpointTask);
+ boolean shouldCalculateOffsetForAllTasks = showAsGrid
+ && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile())
+ && mTaskModalness > 0;
+ if (shouldCalculateOffsetForAllTasks) {
+ modalMidpoint = indexOfChild(getSelectedTaskView());
}
float midpointOffsetSize = 0;
@@ -4569,6 +5059,7 @@ private void updatePageOffsets() {
float modalLeftOffsetSize = 0;
float modalRightOffsetSize = 0;
float gridOffsetSize = 0;
+ float carouselHiddenOffsetSize = 0;
if (showAsGrid) {
// In grid, we only focus the task on the side. The reference index used for offset
@@ -4586,27 +5077,52 @@ private void updatePageOffsets() {
: 0;
}
+ int primarySize = getPagedOrientationHandler().getPrimaryValue(getWidth(), getHeight());
+ float maxOverscroll = primarySize * OverScroll.OVERSCROLL_DAMP_FACTOR;
for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
float translation = i == midpoint
? midpointOffsetSize
: i < midpoint
? leftOffsetSize
: rightOffsetSize;
- if (isModalGridWithoutFocusedTask) {
+ if (shouldCalculateOffsetForAllTasks) {
gridOffsetSize = getHorizontalOffsetSize(i, modalMidpoint, modalOffset);
gridOffsetSize = Math.abs(gridOffsetSize) * (i <= modalMidpoint ? 1 : -1);
}
+ if (enableLargeDesktopWindowingTile()) {
+ if (child instanceof TaskView
+ && !mUtils.isVisibleInCarousel((TaskView) child,
+ runningTask, /*nonRunningTaskCarouselHidden=*/true)) {
+ // Increment carouselHiddenOffsetSize by maxOverscroll so it won't be on screen
+ // even when user overscroll.
+ carouselHiddenOffsetSize = (Math.abs(getMaxHorizontalOffsetSize(i,
+ carouselHiddenMidpoint)) + maxOverscroll)
+ * mDesktopCarouselDetachProgress;
+ carouselHiddenOffsetSize = carouselHiddenOffsetSize * (
+ i <= carouselHiddenMidpoint ? 1 : -1);
+ } else {
+ carouselHiddenOffsetSize = 0;
+ }
+ }
float modalTranslation = i == modalMidpoint
? modalMidpointOffsetSize
: showAsGrid
? gridOffsetSize
: i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize;
- float totalTranslationX = translation + modalTranslation;
- View child = getChildAt(i);
- FloatProperty translationPropertyX = child instanceof TaskView
- ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
- : getPagedOrientationHandler().getPrimaryViewTranslate();
- translationPropertyX.set(child, totalTranslationX);
+ boolean skipTranslationOffset = enableDesktopTaskAlphaAnimation()
+ && i == getRunningTaskIndex()
+ && child instanceof DesktopTaskView;
+ float totalTranslationX = (skipTranslationOffset ? 0f : translation) + modalTranslation
+ + carouselHiddenOffsetSize;
+ if (child instanceof TaskView taskView) {
+ taskView.getPrimaryTaskOffsetTranslationProperty().set(taskView, totalTranslationX);
+ } else if (child instanceof ClearAllButton) {
+ getPagedOrientationHandler().getPrimaryViewTranslate().set(child,
+ totalTranslationX);
+ } else if (child instanceof AddDesktopButton addDesktopButton) {
+ addDesktopButton.setOffsetTranslationX(totalTranslationX);
+ }
if (mEnableDrawingLiveTile && i == getRunningTaskIndex()) {
runActionOnRemoteHandles(
remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
@@ -4614,11 +5130,11 @@ private void updatePageOffsets() {
redrawLiveTile();
}
- if (showAsGrid && enableGridOnlyOverview() && child instanceof TaskView) {
- float totalTranslationY = getVerticalOffsetSize(i, modalOffset);
- FloatProperty translationPropertyY =
- ((TaskView) child).getSecondaryTaskOffsetTranslationProperty();
- translationPropertyY.set(child, totalTranslationY);
+ if (showAsGrid && enableGridOnlyOverview() && child instanceof TaskView taskView) {
+ float totalTranslationY = getVerticalOffsetSize(taskView, modalOffset);
+ FloatProperty translationPropertyY =
+ taskView.getSecondaryTaskOffsetTranslationProperty();
+ translationPropertyY.set(taskView, totalTranslationY);
}
}
updateCurveProperties();
@@ -4650,6 +5166,7 @@ private void getPersistentChildPosition(int childIndex, int midPointScroll, Rect
/**
* Computes the distance to offset the given child such that it is completely offscreen when
* translating away from the given midpoint.
+ *
* @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
*/
private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
@@ -4658,6 +5175,14 @@ private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float o
return 0;
}
+ return getMaxHorizontalOffsetSize(childIndex, midpointIndex) * offsetProgress;
+ }
+
+ /**
+ * Computes the distance to offset the given child such that it is completely offscreen when
+ * translating away from the given midpoint.
+ */
+ private float getMaxHorizontalOffsetSize(int childIndex, int midpointIndex) {
// First, get the position of the task relative to the midpoint. If there is no midpoint
// then we just use the normal (centered) task position.
RectF taskPosition = mTempRectF;
@@ -4717,7 +5242,7 @@ private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float o
}
distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
}
- return distanceToOffscreen * offsetProgress;
+ return distanceToOffscreen;
}
/**
@@ -4725,19 +5250,18 @@ private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float o
*
* @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
*/
- private float getVerticalOffsetSize(int childIndex, float offsetProgress) {
+ private float getVerticalOffsetSize(TaskView taskView, float offsetProgress) {
if (offsetProgress == 0 || !(showAsGrid() && enableGridOnlyOverview())
- || mSelectedTask == null) {
+ || getSelectedTaskView() == null) {
// Don't bother calculating everything below if we won't offset vertically.
return 0;
}
// First, get the position of the task relative to the top row.
- TaskView child = getTaskViewAt(childIndex);
- Rect taskPosition = getTaskBounds(child);
+ Rect taskPosition = getTaskBounds(taskView);
- boolean isSelectedTaskTopRow = mTopRowIdSet.contains(mSelectedTask.getTaskViewId());
- boolean isChildTopRow = mTopRowIdSet.contains(child.getTaskViewId());
+ boolean isSelectedTaskTopRow = mTopRowIdSet.contains(getSelectedTaskView().getTaskViewId());
+ boolean isChildTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
// Whether the task should be shifted to the top.
boolean isTopShift = !isSelectedTaskTopRow && isChildTopRow;
boolean isBottomShift = isSelectedTaskTopRow && !isChildTopRow;
@@ -4754,9 +5278,9 @@ private float getVerticalOffsetSize(int childIndex, float offsetProgress) {
protected void setTaskViewsResistanceTranslation(float translation) {
mTaskViewsSecondaryTranslation = translation;
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView task = requireTaskViewAt(i);
- task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
+ for (TaskView taskView : getTaskViews()) {
+ taskView.getTaskResistanceTranslationProperty().set(taskView,
+ translation / getScaleY());
}
runActionOnRemoteHandles(
remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
@@ -4764,23 +5288,21 @@ protected void setTaskViewsResistanceTranslation(float translation) {
}
private void updateTaskViewsSnapshotRadius() {
- for (int i = 0; i < getTaskViewCount(); i++) {
- requireTaskViewAt(i).updateSnapshotRadius();
+ for (TaskView taskView : getTaskViews()) {
+ taskView.updateFullscreenParams();
}
}
protected void setTaskViewsPrimarySplitTranslation(float translation) {
mTaskViewsPrimarySplitTranslation = translation;
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView task = requireTaskViewAt(i);
- task.getPrimarySplitTranslationProperty().set(task, translation);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.getPrimarySplitTranslationProperty().set(taskView, translation);
}
}
protected void setTaskViewsSecondarySplitTranslation(float translation) {
mTaskViewsSecondarySplitTranslation = translation;
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView taskView = requireTaskViewAt(i);
+ for (TaskView taskView : getTaskViews()) {
if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) {
continue;
}
@@ -4792,8 +5314,8 @@ protected void setTaskViewsSecondarySplitTranslation(float translation) {
* Resets the visuals when exit modal state.
*/
public void resetModalVisuals() {
- if (mSelectedTask != null) {
- mSelectedTask.taskContainers.forEach(
+ if (getSelectedTaskView() != null) {
+ getSelectedTaskView().taskContainers.forEach(
taskContainer -> taskContainer.getOverlay().resetModalVisuals());
}
}
@@ -4802,22 +5324,23 @@ public void resetModalVisuals() {
* Primarily used by overview actions to initiate split from focused task, logs the source
* of split invocation as such.
*/
- public void initiateSplitSelect(TaskView taskView) {
+ public void initiateSplitSelect(TaskContainer taskContainer) {
int defaultSplitPosition = getPagedOrientationHandler()
.getDefaultSplitPosition(mContainer.getDeviceProfile());
- initiateSplitSelect(taskView, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT);
+ initiateSplitSelect(taskContainer, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT);
}
/** TODO(b/266477929): Consolidate this call w/ the one below */
- public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition,
+ public void initiateSplitSelect(TaskContainer taskContainer,
+ @StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent) {
+ TaskView taskView = taskContainer.getTaskView();
mSplitHiddenTaskView = taskView;
mSplitSelectStateController.setInitialTaskSelect(null /*intent*/, stagePosition,
- taskView.getFirstItemInfo(), splitEvent, taskView.getFirstTask().key.id);
+ taskContainer.getItemInfo(), splitEvent, taskContainer.getTask().key.id);
mSplitSelectStateController.setAnimateCurrentTaskDismissal(
true /*animateCurrentTaskDismissal*/);
mSplitHiddenTaskViewIndex = indexOfChild(taskView);
- updateDesktopTaskVisibility(false /* visible */);
}
/**
@@ -4831,20 +5354,67 @@ public void initiateSplitSelect(SplitSelectSource splitSelectSource) {
mSplitHiddenTaskView = getTaskViewByTaskId(splitSelectSource.alreadyRunningTaskId);
mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
mSplitSelectStateController
- .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal);
+ .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal
+ && mSplitHiddenTaskView != null
+ && !(mSplitHiddenTaskView instanceof DesktopTaskView));
// Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
&& mSplitHiddenTaskView instanceof GroupedTaskView);
mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
- splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
+ splitSelectSource.position.stagePosition, splitSelectSource.getItemInfo(),
splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
- updateDesktopTaskVisibility(false /* visible */);
}
- private void updateDesktopTaskVisibility(boolean visible) {
- if (mDesktopTaskView != null) {
- mDesktopTaskView.setVisibility(visible ? VISIBLE : GONE);
+ /**
+ * Animate DesktopTaskView(s) to hide in split select
+ */
+ public void handleDesktopTaskInSplitSelectState(PendingAnimation builder,
+ Interpolator deskTopFadeInterPolator) {
+ SplitAnimationTimings timings = AnimUtils.getDeviceOverviewToSplitTimings(
+ mContainer.getDeviceProfile().isTablet);
+ if (enableLargeDesktopWindowingTile()) {
+ getTaskViews().forEachWithIndexInParent((index, taskView) -> {
+ if (taskView instanceof DesktopTaskView) {
+ // Setting pivot to scale down from screen centre.
+ if (isTaskViewVisible(taskView)) {
+ float pivotX = 0f;
+ if (index < mCurrentPage) {
+ pivotX = mIsRtl ? taskView.getWidth() / 2f - mPageSpacing
+ - taskView.getWidth()
+ : taskView.getWidth() / 2f + mPageSpacing + taskView.getWidth();
+ } else if (index == mCurrentPage) {
+ pivotX = taskView.getWidth() / 2f;
+ } else {
+ pivotX = mIsRtl ? taskView.getWidth() + mPageSpacing
+ + taskView.getWidth()
+ : taskView.getWidth() - mPageSpacing - taskView.getWidth();
+ }
+ taskView.setPivotX(pivotX);
+ taskView.setPivotY(taskView.getHeight() / 2f);
+ builder.add(ObjectAnimator
+ .ofFloat(taskView, TaskView.DISMISS_SCALE, 0.95f),
+ clampToProgress(timings.getDesktopTaskScaleInterpolator(), 0f,
+ timings.getDesktopFadeSplitAnimationEndOffset()));
+ }
+ builder.addFloat(taskView, SPLIT_ALPHA, 1f, 0f,
+ clampToProgress(deskTopFadeInterPolator, 0f,
+ timings.getDesktopFadeSplitAnimationEndOffset()));
+ }
+ });
+ }
+ }
+
+ /**
+ * While exiting from split mode, show all existing DesktopTaskViews.
+ */
+ public void resetDesktopTaskFromSplitSelectState() {
+ if (enableLargeDesktopWindowingTile()) {
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView instanceof DesktopTaskView) {
+ taskView.setSplitAlpha(1f);
+ }
+ }
}
}
@@ -4857,32 +5427,46 @@ public void createSplitSelectInitAnimation(PendingAnimation builder, int duratio
boolean isInitiatingTaskViewSplitPair =
mSplitSelectStateController.isDismissingFromSplitPair();
if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair
- && mSplitHiddenTaskView instanceof GroupedTaskView) {
+ && mSplitHiddenTaskView instanceof GroupedTaskView groupedTaskView) {
// Splitting from Overview for split pair task
createInitialSplitSelectAnimation(builder);
// Animate pair thumbnail into full thumbnail
- boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIds()[0]
+ boolean primaryTaskSelected = groupedTaskView.getLeftTopTaskContainer().getTask().key.id
== mSplitSelectStateController.getInitialTaskId();
- TaskContainer taskContainer = mSplitHiddenTaskView
- .getTaskContainers().get(primaryTaskSelected ? 1 : 0);
- TaskThumbnailViewDeprecated thumbnail = taskContainer.getThumbnailViewDeprecated();
+ TaskContainer taskContainer =
+ primaryTaskSelected ? groupedTaskView.getRightBottomTaskContainer()
+ : groupedTaskView.getLeftTopTaskContainer();
mSplitSelectStateController.getSplitAnimationController()
.addInitialSplitFromPair(taskContainer, builder,
mContainer.getDeviceProfile(),
- mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
+ mSplitHiddenTaskView.getLayoutParams().width,
+ mSplitHiddenTaskView.getLayoutParams().height,
primaryTaskSelected);
- builder.addOnFrameCallback(() ->{
- thumbnail.refreshSplashView();
- mSplitHiddenTaskView.updateSnapshotRadius();
+ builder.addOnFrameCallback(() -> {
+ if (!enableRefactorTaskThumbnail()) {
+ taskContainer.getThumbnailViewDeprecated().refreshSplashView();
+ }
+ mSplitHiddenTaskView.updateFullscreenParams();
});
} else if (isInitiatingSplitFromTaskView) {
+ if (Flags.enableHoverOfChildElementsInTaskview()) {
+ mSplitHiddenTaskView.setBorderEnabled(false);
+ }
// Splitting from Overview for fullscreen task
createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration,
- true /* dismissingForSplitSelection*/);
+ true /* dismissingForSplitSelection*/, false /* isExpressiveDismiss */);
} else {
// Splitting from Home
- createInitialSplitSelectAnimation(builder);
+ TaskView currentPageTaskView = getTaskViewAt(mCurrentPage);
+ // When current page is a Desktop task it needs special handling to
+ // display correct animation in split mode
+ if (currentPageTaskView instanceof DesktopTaskView) {
+ createTaskDismissAnimation(builder, null, true, false, duration,
+ true /* dismissingForSplitSelection*/, false /* isExpressiveDismiss */);
+ } else {
+ createInitialSplitSelectAnimation(builder);
+ }
}
}
@@ -4893,17 +5477,21 @@ public void createSplitSelectInitAnimation(PendingAnimation builder, int duratio
* @param containerTaskView If our second selected app is currently running in Recents, this is
* the "container" TaskView from Recents. If we are starting a fresh
* instance of the app from an Intent, this will be null.
- * @param task The Task corresponding to our second selected app. If we are starting a fresh
- * instance of the app from an Intent, this will be null.
- * @param drawable The Drawable corresponding to our second selected app's icon.
- * @param secondView The View representing the current space on the screen where the second app
- * is (either the ThumbnailView or the tapped icon).
- * @param intent If we are launching a fresh instance of the app, this is the Intent for it. If
- * the second app is already running in Recents, this will be null.
- * @param user If we are launching a fresh instance of the app, this is the UserHandle for it.
- * If the second app is already running in Recents, this will be null.
+ * @param task The Task corresponding to our second selected app. If we are
+ * starting a fresh
+ * instance of the app from an Intent, this will be null.
+ * @param drawable The Drawable corresponding to our second selected app's icon.
+ * @param secondView The View representing the current space on the screen where the
+ * second app
+ * is (either the ThumbnailView or the tapped icon).
+ * @param intent If we are launching a fresh instance of the app, this is the Intent
+ * for it. If
+ * the second app is already running in Recents, this will be null.
+ * @param user If we are launching a fresh instance of the app, this is the
+ * UserHandle for it.
+ * If the second app is already running in Recents, this will be null.
* @return true if waiting for confirmation of second app or if split animations are running,
- * false otherwise
+ * false otherwise
*/
public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable,
View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user,
@@ -4972,12 +5560,8 @@ public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawabl
pendingAnimation.addEndListener(aBoolean -> {
mSplitSelectStateController.launchSplitTasks(
aBoolean1 -> {
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.resetState();
- } else {
- resetFromSplitSelectionState();
- }
InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
+ mSplitSelectStateController.resetState();
});
});
@@ -4991,7 +5575,7 @@ public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawabl
"Second tile selected");
// Fade out all other views underneath placeholders
- ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0);
+ ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA, 1, 0);
pendingAnimation.add(tvFade, DECELERATE_2, SpringProperty.DEFAULT);
pendingAnimation.buildAnim().start();
return true;
@@ -4999,17 +5583,14 @@ public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawabl
@SuppressLint("WrongCall")
protected void resetFromSplitSelectionState() {
- if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1 ||
- FeatureFlags.enableSplitContextually()) {
- safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
- safeRemoveDragLayerView(mSecondFloatingTaskView);
- safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
- safeRemoveDragLayerView(mSplitScrim);
- mSecondFloatingTaskView = null;
- mSplitSelectSource = null;
- mSplitSelectStateController.getSplitAnimationController()
- .removeSplitInstructionsView(mContainer);
- }
+ safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
+ safeRemoveDragLayerView(mSecondFloatingTaskView);
+ safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
+ safeRemoveDragLayerView(mSplitScrim);
+ mSecondFloatingTaskView = null;
+ mSplitSelectSource = null;
+ mSplitSelectStateController.getSplitAnimationController()
+ .removeSplitInstructionsView(mContainer);
if (mSecondSplitHiddenView != null) {
mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
@@ -5021,11 +5602,6 @@ protected void resetFromSplitSelectionState() {
setTaskViewsPrimarySplitTranslation(0);
setTaskViewsSecondarySplitTranslation(0);
- if (!FeatureFlags.enableSplitContextually()) {
- // When flag is on, this method gets called from resetState() call below, let's avoid
- // infinite recursion today
- mSplitSelectStateController.resetState();
- }
if (mSplitHiddenTaskViewIndex == -1) {
return;
}
@@ -5044,9 +5620,15 @@ protected void resetFromSplitSelectionState() {
mSplitHiddenTaskViewIndex = -1;
if (mSplitHiddenTaskView != null) {
mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
+ // mSplitHiddenTaskView is set when split select animation starts. The TaskView is only
+ // removed when when the animation finishes. So in the case of overview being dismissed
+ // during the animation, we should not call clearAndRecycleTaskView() because it has
+ // not been removed yet.
+ if (mSplitHiddenTaskView.getParent() == null) {
+ clearAndRecycleTaskView(mSplitHiddenTaskView);
+ }
mSplitHiddenTaskView = null;
}
- updateDesktopTaskVisibility(true /* visible */);
}
private void safeRemoveDragLayerView(@Nullable View viewToRemove) {
@@ -5096,7 +5678,7 @@ protected void onRotateInSplitSelectionState() {
firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
- Pair, FloatProperty> taskViewsFloat =
+ Pair>, FloatProperty>> taskViewsFloat =
orientationHandler.getSplitSelectTaskOffset(
TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
mContainer.getDeviceProfile());
@@ -5118,15 +5700,8 @@ private void updateDeadZoneRects() {
mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin);
}
- // Get the deadzone rect between the task views
- mTaskViewDeadZoneRect.setEmpty();
- int count = getTaskViewCount();
- if (count > 0) {
- final View taskView = requireTaskViewAt(0);
- requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
- mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
- taskView.getBottom());
- }
+ mUtils.updateTaskViewDeadZoneRect(mTaskViewDeadZoneRect, mTopRowDeadZoneRect,
+ mBottomRowDeadZoneRect);
}
private void updateEmptyStateUi(boolean sizeChanged) {
@@ -5139,7 +5714,7 @@ private void updateEmptyStateUi(boolean sizeChanged) {
if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
- mEmptyMessagePaint, availableWidth)
+ mEmptyMessagePaint, availableWidth)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
int totalHeight = mEmptyTextLayout.getHeight()
@@ -5181,23 +5756,56 @@ protected void maybeDrawEmptyMessage(Canvas canvas) {
* to the right.
*/
@SuppressLint("Recycle")
- public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
+ public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView taskView) {
AnimatorSet anim = new AnimatorSet();
- int taskIndex = indexOfChild(tv);
+ int taskIndex = indexOfChild(taskView);
int centerTaskIndex = getCurrentPage();
float toScale = getMaxScaleForFullScreen();
boolean showAsGrid = showAsGrid();
- boolean launchingCenterTask = showAsGrid
- ? tv.isFocusedTask() && isTaskViewFullyVisible(tv)
- : taskIndex == centerTaskIndex;
- if (launchingCenterTask) {
+ boolean zoomInTaskView = showAsGrid ? taskView.isLargeTile() : taskIndex == centerTaskIndex;
+ if (zoomInTaskView) {
anim.play(ObjectAnimator.ofFloat(this, RECENTS_SCALE_PROPERTY, toScale));
anim.play(ObjectAnimator.ofFloat(this, FULLSCREEN_PROGRESS, 1));
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ taskView.getThumbnailBounds(mTempRect, /*relativeToDragLayer=*/true);
+ getTaskDimension(mContext, mContainer.getDeviceProfile(), mTempPointF);
+ Rect fullscreenBounds = new Rect(0, 0, (int) mTempPointF.x,
+ (int) mTempPointF.y);
+ Utilities.getPivotsForScalingRectToRect(mTempRect, fullscreenBounds,
+ mTempPointF);
+ setPivotX(mTempPointF.x);
+ setPivotY(mTempPointF.y);
+
+ // If live tile is not launching, apply pivot to live tile as well and bring it
+ // above RecentsView to avoid wallpaper blur from being applied to it.
+ if (!taskView.isRunningTask()) {
+ runActionOnRemoteHandles(
+ remoteTargetHandle ->
+ remoteTargetHandle.getTaskViewSimulator()
+ .setPivotOverride(mTempPointF));
+ mBlurUtils.setDrawLiveTileBelowRecents(false);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // If live tile is not launching, reset the pivot applied above.
+ if (!taskView.isRunningTask()) {
+ runActionOnRemoteHandles(
+ remoteTargetHandle -> {
+ remoteTargetHandle.getTaskViewSimulator().setPivotOverride(
+ null);
+ });
+ }
+ }
+ });
} else if (!showAsGrid) {
// We are launching an adjacent task, so parallax the center and other adjacent task.
- float displacementX = tv.getWidth() * (toScale - 1f);
+ float displacementX = taskView.getWidth() * (toScale - 1f);
float primaryTranslation = mIsRtl ? -displacementX : displacementX;
anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation));
@@ -5225,6 +5833,20 @@ && getRemoteTargetHandles() != null) {
}
}
anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0, 1));
+ if (taskView instanceof DesktopTaskView) {
+ anim.play(ObjectAnimator.ofArgb(mContainer.getScrimView(), VIEW_BACKGROUND_COLOR,
+ Color.TRANSPARENT));
+ if (enableDesktopExplodedView()) {
+ anim.play(ObjectAnimator.ofFloat(this, DESK_EXPLODE_PROGRESS, 1f, 0f));
+ }
+ }
+ DepthController depthController = getDepthController();
+ if (depthController != null) {
+ float targetDepth = taskView instanceof DesktopTaskView ? 0 : BACKGROUND_APP.getDepth(
+ mContainer);
+ anim.play(ObjectAnimator.ofFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
+ targetDepth));
+ }
return anim;
}
@@ -5232,31 +5854,28 @@ && getRemoteTargetHandles() != null) {
* Returns the scale up required on the view, so that it coves the screen completely
*/
public float getMaxScaleForFullScreen() {
- if (enableGridOnlyOverview() && mContainer.getDeviceProfile().isTablet
- && !mOverviewGridEnabled) {
- if (mLastComputedCarouselTaskSize.isEmpty()) {
- mSizeStrategy.calculateCarouselTaskSize(mContainer, mContainer.getDeviceProfile(),
- mLastComputedCarouselTaskSize, getPagedOrientationHandler());
- }
- mTempRect.set(mLastComputedCarouselTaskSize);
- } else {
- if (mLastComputedTaskSize.isEmpty()) {
- getTaskSize(mLastComputedTaskSize);
- }
- mTempRect.set(mLastComputedTaskSize);
+ if (mLastComputedTaskSize.isEmpty()) {
+ getTaskSize(mLastComputedTaskSize);
}
+ mTempRect.set(mLastComputedTaskSize);
return getPagedViewOrientedState().getFullScreenScaleAndPivot(
mTempRect, mContainer.getDeviceProfile(), mTempPointF);
}
+ /**
+ * Clears the existing PendingAnimation.
+ */
+ public void clearPendingAnimation() {
+ mPendingAnimation = null;
+ }
+
public PendingAnimation createTaskLaunchAnimation(
- TaskView tv, long duration, Interpolator interpolator) {
+ TaskView taskView, long duration, Interpolator interpolator) {
if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
throw new IllegalStateException("Another pending animation is still running");
}
- int count = getTaskViewCount();
- if (count == 0) {
+ if (!hasTaskViews()) {
return new PendingAnimation(duration);
}
@@ -5265,13 +5884,13 @@ public PendingAnimation createTaskLaunchAnimation(
updateGridProperties();
updateScrollSynchronously();
- int targetSysUiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
- final boolean[] passedOverviewThreshold = new boolean[] {false};
- ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
- progressAnim.addUpdateListener(animator -> {
+ int targetSysUiFlags = taskView.getSysUiStatusNavFlags();
+ final boolean[] passedOverviewThreshold = new boolean[]{false};
+ AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(taskView);
+ anim.play(new AnimatedFloat(v -> {
// Once we pass a certain threshold, update the sysui flags to match the target
// tasks' flags
- if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) {
+ if (v > UPDATE_SYSUI_FLAGS_THRESHOLD) {
mContainer.getSystemUiController().updateUiState(
UI_STATE_FULLSCREEN_TASK, targetSysUiFlags);
} else {
@@ -5279,8 +5898,7 @@ public PendingAnimation createTaskLaunchAnimation(
}
// Passing the threshold from taskview to fullscreen app will vibrate
- final boolean passed = animator.getAnimatedFraction() >=
- SUCCESS_TRANSITION_PROGRESS;
+ final boolean passed = v >= SUCCESS_TRANSITION_PROGRESS;
if (passed != passedOverviewThreshold[0]) {
passedOverviewThreshold[0] = passed;
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
@@ -5290,30 +5908,26 @@ public PendingAnimation createTaskLaunchAnimation(
mRecentsAnimationController.setWillFinishToHome(!passed);
}
}
- });
-
- AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
-
- DepthController depthController = getDepthController();
- if (depthController != null) {
- ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController.stateDepth,
- MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(mContainer));
- anim.play(depthAnimator);
- }
- anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0f, 1f));
-
- anim.play(progressAnim);
+ }).animateToValue(0f, 1f));
anim.setInterpolator(interpolator);
mPendingAnimation = new PendingAnimation(duration);
mPendingAnimation.add(anim);
- runActionOnRemoteHandles(
- remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
- .addOverviewToAppAnim(mPendingAnimation, interpolator));
- mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+ if (taskView.isRunningTask()) {
+ runActionOnRemoteHandles(
+ remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
+ .addOverviewToAppAnim(mPendingAnimation, interpolator));
+ mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+ }
+ mPendingAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mBlurUtils.setDrawLiveTileBelowRecents(false);
+ }
+ });
mPendingAnimation.addEndListener(isSuccess -> {
if (isSuccess) {
- if (tv instanceof GroupedTaskView && hasAllValidTaskIds(tv.getTaskIds())
+ if (taskView instanceof GroupedTaskView && hasAllValidTaskIds(taskView.getTaskIds())
&& mRemoteTargetHandles != null) {
// TODO(b/194414938): make this part of the animations instead.
TaskViewUtils.createSplitAuxiliarySurfacesAnimator(
@@ -5323,13 +5937,13 @@ public PendingAnimation createTaskLaunchAnimation(
dividerAnimator.end();
});
}
- if (tv.isRunningTask()) {
+ if (taskView.isRunningTask()) {
finishRecentsAnimation(false /* toRecents */, null);
onTaskLaunchAnimationEnd(true /* success */);
} else {
- tv.launchTask(this::onTaskLaunchAnimationEnd);
+ taskView.launchWithoutAnimation(this::onTaskLaunchAnimationEnd);
}
- mContainer.getStatsLogManager().logger().withItemInfo(tv.getFirstItemInfo())
+ mContainer.getStatsLogManager().logger().withItemInfo(taskView.getItemInfo())
.log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
} else {
onTaskLaunchAnimationEnd(false);
@@ -5342,6 +5956,12 @@ public PendingAnimation createTaskLaunchAnimation(
protected Unit onTaskLaunchAnimationEnd(boolean success) {
if (success) {
resetTaskVisuals();
+ } else {
+ // If launch animation didn't complete i.e. user dragged live tile down and then
+ // back up and returned to Overview, then we need to ensure we reset the
+ // view to draw below recents so that it can't be interacted with.
+ mBlurUtils.setDrawLiveTileBelowRecents(true);
+ redrawLiveTile();
}
return Unit.INSTANCE;
}
@@ -5352,6 +5972,9 @@ protected void notifyPageSwitchListener(int prevPage) {
updateCurrentTaskActionsVisibility();
loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
updateEnabledOverlays();
+ if (enableRefactorTaskThumbnail()) {
+ mUtils.updateCentralTask();
+ }
}
@Override
@@ -5361,34 +5984,33 @@ protected String getCurrentPageDescription() {
@Override
public void addChildrenForAccessibility(ArrayList outChildren) {
- // Add children in reverse order
- for (int i = getChildCount() - 1; i >= 0; --i) {
- outChildren.add(getChildAt(i));
- }
+ outChildren.addAll(getAccessibilityChildren());
+ }
+
+ public List getAccessibilityChildren() {
+ return mUtils.getAccessibilityChildren();
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
final AccessibilityNodeInfo.CollectionInfo
- collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
- 1, getTaskViewCount(), false,
- AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
+ collectionInfo = new AccessibilityNodeInfo.CollectionInfo(
+ 1, getAccessibilityChildren().size(), false);
info.setCollectionInfo(collectionInfo);
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
-
- final int taskViewCount = getTaskViewCount();
- event.setScrollable(taskViewCount > 0);
+ event.setScrollable(hasTaskViews());
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ final List accessibilityChildren = getAccessibilityChildren();
final int[] visibleTasks = getVisibleChildrenRange();
- event.setFromIndex(taskViewCount - visibleTasks[1]);
- event.setToIndex(taskViewCount - visibleTasks[0]);
- event.setItemCount(taskViewCount);
+ event.setFromIndex(accessibilityChildren.indexOf(getChildAt(visibleTasks[1])));
+ event.setToIndex(accessibilityChildren.indexOf(getChildAt(visibleTasks[0])));
+ event.setItemCount(accessibilityChildren.size());
}
}
@@ -5407,6 +6029,10 @@ public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) {
mEnableDrawingLiveTile = enableDrawingLiveTile;
}
+ public boolean getEnableDrawingLiveTile() {
+ return mEnableDrawingLiveTile;
+ }
+
public void redrawLiveTile() {
runActionOnRemoteHandles(remoteTargetHandle -> {
TransformParams params = remoteTargetHandle.getTransformParams();
@@ -5416,6 +6042,7 @@ public void redrawLiveTile() {
});
}
+ @Nullable
public RemoteTargetHandle[] getRemoteTargetHandles() {
return mRemoteTargetHandles;
}
@@ -5433,10 +6060,11 @@ public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimati
}
RemoteTargetGluer gluer;
- if (recentsAnimationTargets.hasDesktopTasks()) {
+ if (recentsAnimationTargets.hasDesktopTasks(mContext)) {
gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
true /* forDesktop */);
- mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
+ mRemoteTargetHandles = gluer.assignTargetsForDesktop(
+ recentsAnimationTargets, /* transitionInfo= */ null);
} else {
gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
false);
@@ -5449,6 +6077,14 @@ public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimati
// mSyncTransactionApplier doesn't get transferred over
runActionOnRemoteHandles(remoteTargetHandle -> {
final TransformParams params = remoteTargetHandle.getTransformParams();
+ if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
+ params.setHomeBuilderProxy((builder, app, transformParams) -> {
+ mTmpMatrix.setScale(
+ 1f, 1f, app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
+ builder.setMatrix(mTmpMatrix).setAlpha(1f).setShow();
+ });
+ }
+
if (mSyncTransactionApplier != null) {
params.setSyncTransactionApplier(mSyncTransactionApplier);
params.getTargetSet().addReleaseCheck(mSyncTransactionApplier);
@@ -5488,12 +6124,19 @@ public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinis
finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete);
}
+ /**
+ * Finish recents animation.
+ */
+ public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
+ @Nullable Runnable onFinishComplete) {
+ finishRecentsAnimation(toRecents, shouldPip, false, onFinishComplete);
+ }
/**
* NOTE: Whatever value gets passed through to the toRecents param may need to also be set on
* {@link #mRecentsAnimationController#setWillFinishToHome}.
*/
public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
- @Nullable Runnable onFinishComplete) {
+ boolean allAppTargetsAreTranslucent, @Nullable Runnable onFinishComplete) {
Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: "
+ mRecentsAnimationController);
// TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
@@ -5507,8 +6150,9 @@ public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
}
final boolean sendUserLeaveHint = toRecents && shouldPip;
- if (sendUserLeaveHint) {
+ if (sendUserLeaveHint && !com.android.wm.shell.Flags.enablePip2()) {
// Notify the SysUI to use fade-in animation when entering PiP from live tile.
+ // Note: PiP2 handles entering differently, so skip if enable_pip2=true.
final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext());
systemUiProxy.setPipAnimationTypeToAlpha();
systemUiProxy.setShelfHeight(true, mContainer.getDeviceProfile().hotseatBarSizePx);
@@ -5525,7 +6169,7 @@ public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
tx, null /* overlay */);
}
}
- mRecentsAnimationController.finish(toRecents, () -> {
+ mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> {
if (onFinishComplete != null) {
onFinishComplete.run();
}
@@ -5552,6 +6196,9 @@ public void onRecentsAnimationComplete() {
mRecentsAnimationController = null;
mSplitSelectStateController.setRecentsAnimationRunning(false);
executeSideTaskLaunchCallback();
+ if (enableOverviewBackgroundWallpaperBlur()) {
+ mBlurUtils.setDrawLiveTileBelowRecents(false);
+ }
}
public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
@@ -5560,6 +6207,17 @@ public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
updateMinAndMaxScrollX();
}
}
+ /**
+ * Update the value of [mDisallowScrollToAddDesk]
+ */
+ public void setDisallowScrollToAddDesk(boolean disallowScrollToAddDesk) {
+ if (mDisallowScrollToAddDesk != disallowScrollToAddDesk) {
+ mDisallowScrollToAddDesk = disallowScrollToAddDesk;
+ updateMinAndMaxScrollX();
+ }
+ }
+
+
/**
* Updates page scroll synchronously after measure and layout child views.
@@ -5598,7 +6256,7 @@ protected void updateMinAndMaxScrollX() {
@Override
protected int computeMinScroll() {
- if (getTaskViewCount() <= 0) {
+ if (!hasTaskViews()) {
return super.computeMinScroll();
}
@@ -5607,7 +6265,7 @@ protected int computeMinScroll() {
@Override
protected int computeMaxScroll() {
- if (getTaskViewCount() <= 0) {
+ if (!hasTaskViews()) {
return super.computeMaxScroll();
}
@@ -5615,26 +6273,42 @@ protected int computeMaxScroll() {
}
private int getFirstViewIndex() {
- TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null;
- return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0;
+ final View firstView;
+ if (mShowAsGridLastOnLayout) {
+ // For grid Overview, it always start if a large tile (focused task or desktop task) if
+ // they exist, otherwise it start with the first task.
+ TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView();
+ if (firstLargeTaskView != null) {
+ firstView = firstLargeTaskView;
+ } else {
+ firstView = mUtils.getFirstSmallTaskView();
+ }
+ } else {
+ firstView = mUtils.getFirstTaskViewInCarousel(
+ /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0);
+ }
+ return indexOfChild(firstView);
}
private int getLastViewIndex() {
+ final View lastView;
if (!mDisallowScrollToClearAll) {
- return indexOfChild(mClearAllButton);
- }
-
- if (!mShowAsGridLastOnLayout) {
- return getTaskViewCount() - 1;
- }
-
- TaskView lastGridTaskView = getLastGridTaskView();
- if (lastGridTaskView != null) {
- return indexOfChild(lastGridTaskView);
+ // When ClearAllButton is present, it always end with ClearAllButton.
+ lastView = mClearAllButton;
+ } else if (mShowAsGridLastOnLayout) {
+ // When ClearAllButton is absent, for the grid Overview, it always end with a grid task
+ // if they exist, otherwise it ends with a large tile (focused task or desktop task).
+ TaskView lastGridTaskView = getLastGridTaskView();
+ if (lastGridTaskView != null) {
+ lastView = lastGridTaskView;
+ } else {
+ lastView = mUtils.getLastLargeTaskView();
+ }
+ } else {
+ lastView = mUtils.getLastTaskViewInCarousel(
+ /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0);
}
-
- // Returns focus task if there are no grid tasks.
- return indexOfChild(getFocusedTaskView());
+ return indexOfChild(lastView);
}
/**
@@ -5660,42 +6334,55 @@ protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff);
}
- boolean pageScrollChanged = false;
-
+ int[] oldPageScrolls = Arrays.copyOf(outPageScrolls, outPageScrolls.length);
int clearAllIndex = indexOfChild(mClearAllButton);
int clearAllScroll = 0;
int clearAllWidth = getPagedOrientationHandler().getPrimarySize(mClearAllButton);
if (clearAllIndex != -1 && clearAllIndex < outPageScrolls.length) {
float scrollDiff = mClearAllButton.getScrollAdjustment(showAsFullscreen, showAsGrid);
- clearAllScroll = newPageScrolls[clearAllIndex] + (int) scrollDiff;
- if (outPageScrolls[clearAllIndex] != clearAllScroll) {
- pageScrollChanged = true;
- outPageScrolls[clearAllIndex] = clearAllScroll;
- }
+ clearAllScroll = newPageScrolls[clearAllIndex] + Math.round(scrollDiff);
+ outPageScrolls[clearAllIndex] = clearAllScroll;
}
- final int taskCount = getTaskViewCount();
int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
+ getTaskViews().forEachWithIndexInParent((index, taskView) -> {
float scrollDiff = taskView.getScrollAdjustment(showAsGrid);
- int pageScroll = newPageScrolls[i] + Math.round(scrollDiff);
+ int pageScroll = newPageScrolls[index] + Math.round(scrollDiff);
if ((mIsRtl && pageScroll < lastTaskScroll)
|| (!mIsRtl && pageScroll > lastTaskScroll)) {
pageScroll = lastTaskScroll;
}
- if (outPageScrolls[i] != pageScroll) {
- pageScrollChanged = true;
- outPageScrolls[i] = pageScroll;
+ outPageScrolls[index] = pageScroll;
+ if (DEBUG) {
+ Log.d(TAG,
+ "getPageScrolls - outPageScrolls[" + index + "]: " + outPageScrolls[index]);
+ }
+ });
+
+ int addDesktopButtonIndex = indexOfChild(mAddDesktopButton);
+ if (addDesktopButtonIndex >= 0 && addDesktopButtonIndex < outPageScrolls.length) {
+ int firstViewIndex = getFirstViewIndex();
+ if (firstViewIndex >= 0 && firstViewIndex < outPageScrolls.length) {
+ // If we can scroll to [AddDesktopButton], make its page scroll equal to
+ // the first [TaskView]. Otherwise, make its page scroll out of range of
+ // [minScroll, maxScroll].
+ if (!mDisallowScrollToAddDesk) {
+ outPageScrolls[addDesktopButtonIndex] = outPageScrolls[firstViewIndex];
+ } else {
+ outPageScrolls[addDesktopButtonIndex] =
+ outPageScrolls[firstViewIndex] + (mIsRtl ? 1 : -1);
+ }
}
+
if (DEBUG) {
- Log.d(TAG, "getPageScrolls - outPageScrolls[" + i + "]: " + outPageScrolls[i]);
+ Log.d(TAG, "getPageScrolls - addDesktopButtonScroll: "
+ + outPageScrolls[addDesktopButtonIndex]);
}
}
if (DEBUG) {
Log.d(TAG, "getPageScrolls - clearAllScroll: " + clearAllScroll);
}
- return pageScrollChanged;
+ return !Arrays.equals(oldPageScrolls, outPageScrolls);
}
@Override
@@ -5712,12 +6399,12 @@ protected int getChildOffset(int index) {
}
@Override
- protected int getChildVisibleSize(int index) {
- final TaskView taskView = getTaskViewAt(index);
+ protected int getChildVisibleSize(int childIndex) {
+ final TaskView taskView = getTaskViewAt(childIndex);
if (taskView == null) {
- return super.getChildVisibleSize(index);
+ return super.getChildVisibleSize(childIndex);
}
- return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
+ return (int) (super.getChildVisibleSize(childIndex) * taskView.getSizeAdjustment(
showAsFullscreen()));
}
@@ -5725,6 +6412,11 @@ public ClearAllButton getClearAllButton() {
return mClearAllButton;
}
+ @Nullable
+ public AddDesktopButton getAddDeskButton() {
+ return mAddDesktopButton;
+ }
+
/**
* @return How many pixels the running task is offset on the currently laid out dominant axis.
*/
@@ -5749,6 +6441,7 @@ public int getScrollOffsetForKeyboardTaskFocus() {
* Sets whether or not we should clamp the scroll offset.
* This is used to avoid x-axis movement when swiping up transient taskbar.
* Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
+ *
* @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp threshold is
* met.
*/
@@ -5775,17 +6468,17 @@ public int getScrollOffset(int pageIndex) {
* Returns how many pixels the page is offset on the currently laid out dominant axis.
*/
private int getUnclampedScrollOffset(int pageIndex) {
- if (pageIndex == -1) {
+ if (pageIndex == INVALID_PAGE) {
return 0;
}
// Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
// the page can move freely given there's no visual indication why it shouldn't.
int overScrollShift = mAdjacentPageHorizontalOffset > 0
- ? (int) Utilities.mapRange(
- mAdjacentPageHorizontalOffset,
- getOverScrollShift(),
- getUndampedOverScrollShift())
- : getOverScrollShift();
+ ? (int) Utilities.mapRange(
+ mAdjacentPageHorizontalOffset,
+ getOverScrollShift(),
+ getUndampedOverScrollShift())
+ : getOverScrollShift();
return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
+ overScrollShift + getOffsetFromScrollPosition(pageIndex);
}
@@ -5794,7 +6487,8 @@ private int getUnclampedScrollOffset(int pageIndex) {
* Returns how many pixels the page is offset from its scroll position.
*/
private int getOffsetFromScrollPosition(int pageIndex) {
- return getOffsetFromScrollPosition(pageIndex, getTopRowIdArray(), getBottomRowIdArray());
+ return getOffsetFromScrollPosition(pageIndex, mUtils.getTopRowIdArray(),
+ mUtils.getBottomRowIdArray());
}
private int getOffsetFromScrollPosition(
@@ -5844,12 +6538,12 @@ private int getPositionInRow(
}
/**
- * @return true if the task in on the top of the grid
+ * @return true if the task in on the bottom of the grid
*/
public boolean isOnGridBottomRow(TaskView taskView) {
return showAsGrid()
&& !mTopRowIdSet.contains(taskView.getTaskViewId())
- && taskView.getTaskViewId() != mFocusedTaskViewId;
+ && !taskView.isLargeTile();
}
public Consumer getEventDispatcher(float navbarRotation) {
@@ -5882,19 +6576,27 @@ public Consumer getEventDispatcher(float navbarRotation) {
}
private void updateEnabledOverlays() {
- TaskView focusedTaskView = getFocusedTaskView();
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = requireTaskViewAt(i);
- if (taskView == focusedTaskView) {
- continue;
+ if (enableRefactorTaskThumbnail()) {
+ Set fullyVisibleTaskIds = new HashSet<>();
+ for (TaskView taskView : getTaskViews()) {
+ if (isTaskViewFullyVisible(taskView)) {
+ fullyVisibleTaskIds.addAll(taskView.getTaskIdSet());
+ }
+ }
+ mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
+ } else {
+ TaskView focusedTaskView = getFocusedTaskView();
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView == focusedTaskView) {
+ continue;
+ }
+ taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
+ }
+ // Focus task overlay should be enabled and refreshed at last
+ if (focusedTaskView != null) {
+ focusedTaskView.setOverlayEnabled(
+ mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
}
- taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
- }
- // Focus task overlay should be enabled and refreshed at last
- if (focusedTaskView != null) {
- focusedTaskView.setOverlayEnabled(
- mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
}
}
@@ -5902,6 +6604,10 @@ public void setOverlayEnabled(boolean overlayEnabled) {
if (mOverlayEnabled != overlayEnabled) {
mOverlayEnabled = overlayEnabled;
updateEnabledOverlays();
+
+ if (enableRefactorTaskThumbnail()) {
+ mRecentsViewModel.setOverlayEnabled(overlayEnabled);
+ }
}
}
@@ -5949,32 +6655,19 @@ public void switchToScreenshot(Runnable onFinishRunnable) {
return;
}
- switchToScreenshotInternal(onFinishRunnable);
- }
-
- private void switchToScreenshotInternal(Runnable onFinishRunnable) {
TaskView taskView = getRunningTaskView();
if (taskView == null) {
onFinishRunnable.run();
return;
}
- setRunningTaskViewShowScreenshot(true);
- for (TaskContainer container : taskView.getTaskContainers()) {
- if (container == null) {
- continue;
- }
-
- ThumbnailData td =
- mRecentsAnimationController.screenshotTask(container.getTask().key.id);
- TaskThumbnailViewDeprecated thumbnailView = container.getThumbnailViewDeprecated();
- if (td != null) {
- thumbnailView.setThumbnail(container.getTask(), td);
- } else {
- thumbnailView.refresh();
- }
+ Map updatedThumbnails = mUtils.screenshotTasks(taskView);
+ if (enableRefactorTaskThumbnail()) {
+ mHelper.switchToScreenshot(taskView, updatedThumbnails, onFinishRunnable);
+ } else {
+ setRunningTaskViewShowScreenshot(true, updatedThumbnails);
+ ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
}
- ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
}
/**
@@ -5988,9 +6681,12 @@ public void switchToScreenshot(@Nullable HashMap thumbna
Runnable onFinishRunnable) {
final TaskView taskView = getRunningTaskView();
if (taskView != null) {
- taskView.setShouldShowScreenshot(true);
- taskView.refreshThumbnails(thumbnailDatas);
- ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
+ if (enableRefactorTaskThumbnail()) {
+ mHelper.switchToScreenshot(taskView, thumbnailDatas, onFinishRunnable);
+ } else {
+ taskView.setShouldShowScreenshot(true, thumbnailDatas);
+ ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
+ }
} else {
onFinishRunnable.run();
}
@@ -6003,8 +6699,14 @@ public void switchToScreenshot(@Nullable HashMap thumbna
private void setTaskModalness(float modalness) {
mTaskModalness = modalness;
updatePageOffsets();
- if (mSelectedTask != null) {
- mSelectedTask.setModalness(modalness);
+ if (getSelectedTaskView() != null) {
+ if (enableGridOnlyOverview()) {
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setModalness(modalness);
+ }
+ } else {
+ getSelectedTaskView().setModalness(modalness);
+ }
} else if (getCurrentPageTaskView() != null) {
getCurrentPageTaskView().setModalness(modalness);
}
@@ -6035,6 +6737,12 @@ public BaseContainerInterface getSizeStrategy() {
return mSizeStrategy;
}
+
+ /**
+ * Returns the container interface
+ */
+ protected abstract BaseContainerInterface getContainerInterface(int displayId);
+
/**
* Set all the task views to color tint scrim mode, dimming or tinting them all. Allows the
* tasks to be dimmed while other elements in the recents view are left alone.
@@ -6055,12 +6763,11 @@ public void showForegroundScrim(boolean show) {
}
/** Tint the RecentsView and TaskViews in to simulate a scrim. */
- // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView
private void setColorTint(float tintAmount) {
mColorTint = tintAmount;
- for (int i = 0; i < getTaskViewCount(); i++) {
- requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
+ for (TaskView taskView : getTaskViews()) {
+ taskView.setColorTint(mColorTint, mTintingColor);
}
Drawable scrimBg = mContainer.getScrimView().getBackground();
@@ -6087,10 +6794,10 @@ private float getColorTint() {
public boolean showAsGrid() {
return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
&& mSizeStrategy.stateFromGestureEndTarget(mCurrentGestureEndTarget)
- .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+ .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
}
- private boolean showAsFullscreen() {
+ protected boolean showAsFullscreen() {
return mOverviewFullscreenEnabled
&& mCurrentGestureEndTarget != GestureState.GestureEndTarget.RECENTS;
}
@@ -6128,7 +6835,7 @@ public void removeOnScrollChangedListener(OnScrollChangedListener listener) {
/**
* @return Corner radius in pixel value for PiP window, which is updated via
- * {@link #mIPipAnimationListener}
+ * {@link #mIPipAnimationListener}
*/
public int getPipCornerRadius() {
return mPipCornerRadius;
@@ -6136,7 +6843,7 @@ public int getPipCornerRadius() {
/**
* @return Shadow radius in pixel value for PiP window, which is updated via
- * {@link #mIPipAnimationListener}
+ * {@link #mIPipAnimationListener}
*/
public int getPipShadowRadius() {
return mPipShadowRadius;
@@ -6300,6 +7007,68 @@ public void onExpandPip() {
}
}
+ @Override
+ public void onCanCreateDesksChanged(boolean canCreateDesks) {
+ // TODO: b/389209338 - update the AddDesktopButton's visibility on this.
+ }
+
+ @Override
+ public void onDeskAdded(int displayId, int deskId) {
+ // Ignore desk changes that don't belong to this display.
+ if (displayId != mContainer.getDisplay().getDisplayId()) {
+ return;
+ }
+
+ if (mUtils.getDesktopTaskViewForDeskId(deskId) != null) {
+ Log.e(TAG, "A task view for this desk has already been added.");
+ return;
+ }
+
+ TaskView currentTaskView = getTaskViewAt(mCurrentPage);
+
+ // We assume that a newly added desk is always empty and gets added to the left of the
+ // `AddNewDesktopButton`.
+ DesktopTaskView desktopTaskView =
+ (DesktopTaskView) getTaskViewFromPool(TaskViewType.DESKTOP);
+ desktopTaskView.bind(new DesktopTask(deskId, displayId, new ArrayList<>()),
+ mOrientationState, mTaskOverlayFactory);
+
+ Objects.requireNonNull(mAddDesktopButton);
+ final int insertionIndex = 1 + indexOfChild(mAddDesktopButton);
+ addView(desktopTaskView, insertionIndex);
+
+ updateTaskSize();
+ mUtils.updateChildTaskOrientations();
+ updateScrollSynchronously();
+
+ // Set Current Page based on the stored TaskView.
+ if (currentTaskView != null) {
+ setCurrentPage(indexOfChild(currentTaskView));
+ }
+ }
+
+ @Override
+ public void onDeskRemoved(int displayId, int deskId) {
+ // Ignore desk changes that don't belong to this display.
+ if (displayId != mContainer.getDisplay().getDisplayId()) {
+ return;
+ }
+
+ // We need to distinguish between desk removals that are triggered from outside of overview
+ // vs. the ones that were initiated from overview by dismissing the corresponding desktop
+ // task view.
+ var taskView = mUtils.getDesktopTaskViewForDeskId(deskId);
+ if (taskView != null) {
+ dismissTaskView(taskView, true, true);
+ }
+ }
+
+ @Override
+ public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {
+ // TODO: b/400870600 - We may need to add code here to special case when an empty desk gets
+ // activated, since `RemoteDesktopLaunchTransitionRunner` doesn't always get triggered.
+ }
+
/** Get the color used for foreground scrimming the RecentsView for sharing. */
public static int getForegroundScrimDimColor(Context context) {
return context.getColor(R.color.overview_foreground_scrim_color);
@@ -6350,11 +7119,31 @@ private void moveTaskToDesktopInternal(TaskContainer taskContainer,
if (mDesktopRecentsTransitionController == null) {
return;
}
- mDesktopRecentsTransitionController.moveToDesktop(taskContainer.getTask().key.id,
- transitionSource);
+
+ mDesktopRecentsTransitionController.moveToDesktop(taskContainer, transitionSource,
+ successCallback);
+ }
+
+ /**
+ * Move the provided task into external display and invoke {@code successCallback} if succeeded.
+ */
+ public void moveTaskToExternalDisplay(TaskContainer taskContainer, Runnable successCallback) {
+ if (!DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ return;
+ }
+ switchToScreenshot(() -> finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false,
+ () -> moveTaskToDesktopInternal(taskContainer, successCallback)));
+ }
+
+ private void moveTaskToDesktopInternal(TaskContainer taskContainer, Runnable successCallback) {
+ if (mDesktopRecentsTransitionController == null) {
+ return;
+ }
+ mDesktopRecentsTransitionController.moveToExternalDisplay(taskContainer.getTask().key.id);
successCallback.run();
}
+
// Logs when the orientation of Overview changes. We log both real and fake orientation changes.
private void logOrientationChanged() {
// Only log when Overview is showing.
@@ -6373,7 +7162,47 @@ private void logOrientationChanged() {
}
}
+ private int getFontWeight() {
+ int fontWeightAdjustment = getResources().getConfiguration().fontWeightAdjustment;
+ if (fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
+ return Typeface.Builder.NORMAL_WEIGHT + fontWeightAdjustment;
+ }
+ return Typeface.Builder.NORMAL_WEIGHT;
+ }
+
+ /**
+ * Creates the spring animations which run as a task settles back into its place in overview.
+ *
+ * When a task dismiss is cancelled, the task will return to its original position via a
+ * spring animation. As it passes the threshold of its settling state, its neighbors will
+ * spring in response to the perceived impact of the settling task.
+ */
+ public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
+ float velocity, boolean isDismissing, int dismissLength,
+ Function0 onEndRunnable) {
+ return mDismissUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity,
+ isDismissing, dismissLength, onEndRunnable);
+ }
+
+ /**
+ * Animates RecentsView's scale to the provided value, using spring animations.
+ */
+ public SpringAnimation animateRecentsScale(float scale) {
+ return mDismissUtils.animateRecentsScale(scale);
+ }
+
public interface TaskLaunchListener {
void onTaskLaunched();
}
+
+ /**
+ * Sets whether the remote animation targets should draw below the recents view.
+ *
+ * @param drawBelowRecents whether the surface should draw below Recents.
+ * @param remoteTargetHandles collection of remoteTargetHandles in Recents.
+ */
+ public void setDrawBelowRecents(boolean drawBelowRecents,
+ RemoteTargetHandle[] remoteTargetHandles) {
+ mBlurUtils.setDrawBelowRecents(drawBelowRecents, remoteTargetHandles);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index 060c71e4467..e61d4026985 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -16,7 +16,6 @@
package com.android.quickstep.views;
-import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.LocusId;
@@ -26,9 +25,11 @@
import android.view.View;
import android.view.Window;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.BaseActivity;
import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.util.SystemUiController;
+import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.ScrimView;
@@ -41,7 +42,7 @@ public interface RecentsViewContainer extends ActivityContext {
* Returns an instance of an implementation of RecentsViewContainer
* @param context will find instance of recentsViewContainer from given context.
*/
- static T containerFromContext(Context context) {
+ static T containerFromContext(Context context) {
if (context instanceof RecentsViewContainer) {
return (T) context;
} else if (context instanceof ContextWrapper) {
@@ -51,11 +52,6 @@ static T containerFromContext(Context context)
}
}
- /**
- * Returns {@link SystemUiController} to manage various window flags to control system UI.
- */
- SystemUiController getSystemUiController();
-
/**
* Returns {@link ScrimView}
*/
@@ -66,19 +62,6 @@ static T containerFromContext(Context context)
*/
T getOverviewPanel();
- /**
- * Returns the RootView
- */
- View getRootView();
-
- /**
- * Dispatches a generic motion event to the view hierarchy.
- * Returns the current RecentsViewContainer as context
- */
- default Context asContext() {
- return (Context) this;
- }
-
/**
* @see Window.Callback#dispatchGenericMotionEvent(MotionEvent)
*/
@@ -92,7 +75,7 @@ default Context asContext() {
/**
* Returns overview actions view as a view
*/
- View getActionsView();
+ OverviewActionsView getActionsView();
/**
* @see BaseActivity#addForceInvisibleFlag(int)
@@ -139,12 +122,6 @@ default Context asContext() {
*/
void runOnBindToTouchInteractionService(Runnable r);
- /**
- * @see Activity#getWindow()
- * @return Window
- */
- Window getWindow();
-
/**
* @see
* BaseActivity#addMultiWindowModeChangedListener(BaseActivity.MultiWindowModeChangedListener)
@@ -173,6 +150,25 @@ void removeMultiWindowModeChangedListener(
*/
boolean isRecentsViewVisible();
+ /**
+ * Begins transition to start home through container
+ */
+ default void startHome(){
+ // no op
+ }
+
+ /**
+ * Checks container to see if we can start home transition safely
+ */
+ boolean canStartHomeSafely();
+
+
+ /**
+ * Enter staged split directly from the current running app.
+ * @param leftOrTop if the staged split will be positioned left or top.
+ */
+ default void enterStageSplitFromRunningApp(boolean leftOrTop){}
+
/**
* Overwrites any logged item in Launcher that doesn't have a container with the
* {@link com.android.launcher3.touch.PagedOrientationHandler} in use for Overview.
@@ -198,4 +194,8 @@ default void applyOverwritesToLogItem(LauncherAtom.ItemInfo.Builder itemInfoBuil
.setOrientationHandler(orientationForLogging))
.build());
}
+
+ void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController);
+
+ @Nullable TaskbarUIController getTaskbarUIController();
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
new file mode 100644
index 00000000000..ff711da70fe
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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.views
+
+import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.quickstep.ViewUtils
+import com.android.quickstep.recents.viewmodel.RecentsViewModel
+import com.android.systemui.shared.recents.model.ThumbnailData
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** Helper for [RecentsView] to interact with the [RecentsViewModel]. */
+class RecentsViewModelHelper(
+ private val recentsViewModel: RecentsViewModel,
+ private val recentsCoroutineScope: CoroutineScope,
+ private val dispatcherProvider: DispatcherProvider,
+) {
+ fun onDestroy() {
+ recentsCoroutineScope.cancel("RecentsView is being destroyed")
+ }
+
+ fun switchToScreenshot(
+ taskView: TaskView,
+ updatedThumbnails: Map?,
+ onFinishRunnable: Runnable,
+ ) {
+ // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside
+ // viewAttachedScope.
+ recentsViewModel.setRunningTaskShowScreenshot(true)
+ recentsCoroutineScope.launch(dispatcherProvider.background) {
+ recentsViewModel.waitForRunningTaskShowScreenshotToUpdate()
+ recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails)
+ withContext(Dispatchers.Main.immediate) {
+ ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
new file mode 100644
index 00000000000..b265b13393b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2024 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.views
+
+import android.graphics.PointF
+import android.graphics.Rect
+import android.util.FloatProperty
+import android.view.KeyEvent
+import android.view.View
+import android.view.View.LAYOUT_DIRECTION_LTR
+import android.view.View.LAYOUT_DIRECTION_RTL
+import androidx.core.view.children
+import com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU
+import com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType
+import com.android.launcher3.Flags.enableGridOnlyOverview
+import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
+import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
+import com.android.launcher3.Utilities.getPivotsForScalingRectToRect
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID
+import com.android.launcher3.util.IntArray
+import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.isExternalDisplay
+import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.wm.shell.shared.GroupedTaskInfo
+import java.util.function.BiConsumer
+import kotlin.math.min
+import kotlin.reflect.KMutableProperty1
+
+/**
+ * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
+ * RecentsView to facilitate the implementation of unit tests.
+ */
+class RecentsViewUtils(private val recentsView: RecentsView<*, *>) {
+ val taskViews = TaskViewsIterable(recentsView)
+
+ /** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */
+ fun screenshotTasks(taskView: TaskView): Map {
+ val recentsAnimationController = recentsView.recentsAnimationController ?: return emptyMap()
+ return taskView.taskContainers.associate {
+ it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id)
+ }
+ }
+
+ /**
+ * Sorts task groups to move desktop tasks to the end of the list.
+ *
+ * @param tasks List of group tasks to be sorted.
+ * @return Sorted list of GroupTasks to be used in the RecentsView.
+ */
+ fun sortDesktopTasksToFront(tasks: List): List {
+ var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+ if (areMultiDesksFlagsEnabled()) {
+ // Desk IDs of newer desks are larger than those of older desks, hence we can use them
+ // to sort desks from old to new.
+ desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId }
+ }
+ return otherTasks + desktopTasks
+ }
+
+ fun sortExternalDisplayTasksToFront(tasks: List): List {
+ val (externalDisplayTasks, otherTasks) =
+ tasks.partition { it.tasks.firstOrNull().isExternalDisplay }
+ return otherTasks + externalDisplayTasks
+ }
+
+ class TaskViewsIterable(val recentsView: RecentsView<*, *>) : Iterable {
+ /** Iterates TaskViews when its index inside the RecentsView is needed. */
+ fun forEachWithIndexInParent(consumer: BiConsumer) {
+ recentsView.children.forEachIndexed { index, child ->
+ (child as? TaskView)?.let { consumer.accept(index, it) }
+ }
+ }
+
+ override fun iterator(): Iterator =
+ recentsView.children.mapNotNull { it as? TaskView }.iterator()
+ }
+
+ /** Counts [TaskView]s that are [DesktopTaskView] instances. */
+ private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
+
+ /** Counts [TaskView]s that are not [DesktopTaskView] instances. */
+ fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView }
+
+ /** Returns a list of all large TaskView Ids from [TaskView]s */
+ fun getLargeTaskViewIds(): List = taskViews.filter { it.isLargeTile }.map { it.taskViewId }
+
+ /** Returns a list of all large TaskViews [TaskView]s */
+ fun getLargeTaskViews(): List = taskViews.filter { it.isLargeTile }
+
+ /** Returns all the TaskViews in the top row, without the focused task */
+ fun getTopRowTaskViews(): List =
+ taskViews.filter { recentsView.mTopRowIdSet.contains(it.taskViewId) }
+
+ /** Returns all the task Ids in the top row, without the focused task */
+ fun getTopRowIdArray(): IntArray = getTopRowTaskViews().map { it.taskViewId }.toIntArray()
+
+ /** Returns all the TaskViews in the bottom row, without the focused task */
+ fun getBottomRowTaskViews(): List =
+ taskViews.filter { !recentsView.mTopRowIdSet.contains(it.taskViewId) && !it.isLargeTile }
+
+ /** Returns all the task Ids in the bottom row, without the focused task */
+ fun getBottomRowIdArray(): IntArray = getBottomRowTaskViews().map { it.taskViewId }.toIntArray()
+
+ private fun List.toIntArray() = IntArray(size).apply { this@toIntArray.forEach(::add) }
+
+ /** Counts [TaskView]s that are large tiles. */
+ fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile }
+
+ /** Counts [TaskView]s that are grid tasks. */
+ fun getGridTaskCount(): Int = taskViews.count { it.isGridTask }
+
+ /** Returns the first TaskView that should be displayed as a large tile. */
+ fun getFirstLargeTaskView(): TaskView? =
+ taskViews.firstOrNull {
+ it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView)
+ }
+
+ /**
+ * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist.
+ */
+ fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? {
+ if (deskId == INACTIVE_DESK_ID) {
+ return null
+ }
+ return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId }
+ as? DesktopTaskView
+ }
+
+ /** Returns the active desk ID of the display that contains the [recentsView] instance. */
+ fun getActiveDeskIdOnThisDisplay(): Int =
+ DesktopVisibilityController.INSTANCE.get(recentsView.context)
+ .getActiveDeskId(recentsView.mContainer.display.displayId)
+
+ /** Returns the expected focus task. */
+ fun getFirstNonDesktopTaskView(): TaskView? =
+ if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
+ else taskViews.firstOrNull()
+
+ /**
+ * Returns the [TaskView] that should be the current page during task binding, in the following
+ * priorities:
+ * 1. Running task
+ * 2. Focused task
+ * 3. First non-desktop task
+ * 4. Last desktop task
+ * 5. null otherwise
+ */
+ fun getExpectedCurrentTask(runningTaskView: TaskView?, focusedTaskView: TaskView?): TaskView? =
+ runningTaskView
+ ?: focusedTaskView
+ ?: taskViews.firstOrNull {
+ it !is DesktopTaskView &&
+ !(enableSeparateExternalDisplayTasks() && it.isExternalDisplay)
+ }
+ ?: taskViews.lastOrNull()
+
+ private fun getDeviceProfile() = (recentsView.mContainer as RecentsViewContainer).deviceProfile
+
+ fun getRunningTaskExpectedIndex(runningTaskView: TaskView): Int {
+ if (areMultiDesksFlagsEnabled() && runningTaskView is DesktopTaskView) {
+ // Use the [deskId] to keep desks in the order of their creation, as a newer desk
+ // always has a larger [deskId] than the older desks.
+ val desktopTaskView =
+ taskViews.firstOrNull {
+ it is DesktopTaskView &&
+ it.deskId != INACTIVE_DESK_ID &&
+ it.deskId <= runningTaskView.deskId
+ }
+ if (desktopTaskView != null) return recentsView.indexOfChild(desktopTaskView)
+ }
+ val firstTaskViewIndex = recentsView.indexOfChild(getFirstTaskView())
+ return if (getDeviceProfile().isTablet) {
+ var index = firstTaskViewIndex
+ if (enableLargeDesktopWindowingTile() && runningTaskView !is DesktopTaskView) {
+ // For fullsreen tasks, skip over Desktop tasks in its section
+ index +=
+ if (enableSeparateExternalDisplayTasks()) {
+ if (runningTaskView.isExternalDisplay) {
+ taskViews.count { it is DesktopTaskView && it.isExternalDisplay }
+ } else {
+ taskViews.count { it is DesktopTaskView && !it.isExternalDisplay }
+ }
+ } else {
+ getDesktopTaskViewCount()
+ }
+ }
+ if (enableSeparateExternalDisplayTasks() && !runningTaskView.isExternalDisplay) {
+ // For main display section, skip over external display tasks
+ index += taskViews.count { it.isExternalDisplay }
+ }
+ index
+ } else {
+ val currentIndex: Int = recentsView.indexOfChild(runningTaskView)
+ return if (currentIndex != -1) {
+ currentIndex // Keep the position if running task already in layout.
+ } else {
+ // New running task are added to the front to begin with.
+ firstTaskViewIndex
+ }
+ }
+ }
+
+ /** Returns the first TaskView if it exists, or null otherwise. */
+ fun getFirstTaskView(): TaskView? = taskViews.firstOrNull()
+
+ /** Returns the last TaskView if it exists, or null otherwise. */
+ fun getLastTaskView(): TaskView? = taskViews.lastOrNull()
+
+ /** Returns the first TaskView that is not large */
+ fun getFirstSmallTaskView(): TaskView? = taskViews.firstOrNull { !it.isLargeTile }
+
+ /** Returns the last TaskView that should be displayed as a large tile. */
+ fun getLastLargeTaskView(): TaskView? = taskViews.lastOrNull { it.isLargeTile }
+
+ /**
+ * Gets the list of accessibility children. Currently all the children of RecentsViews are
+ * added, and in the reverse order to the list.
+ */
+ fun getAccessibilityChildren(): List = recentsView.children.toList().reversed()
+
+ @JvmOverloads
+ /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */
+ fun getFirstTaskViewInCarousel(
+ nonRunningTaskCarouselHidden: Boolean,
+ runningTaskView: TaskView? = recentsView.runningTaskView,
+ ): TaskView? =
+ taskViews.firstOrNull {
+ it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
+ }
+
+ /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */
+ fun getLastTaskViewInCarousel(nonRunningTaskCarouselHidden: Boolean): TaskView? =
+ taskViews.lastOrNull {
+ it.isVisibleInCarousel(recentsView.runningTaskView, nonRunningTaskCarouselHidden)
+ }
+
+ /** Returns if any small tasks are fully visible */
+ fun isAnySmallTaskFullyVisible(): Boolean =
+ taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) }
+
+ /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */
+ fun applyAttachAlpha(nonRunningTaskCarouselHidden: Boolean) {
+ taskViews.forEach { taskView ->
+ taskView.attachAlpha =
+ if (taskView == recentsView.runningTaskView) {
+ RUNNING_TASK_ATTACH_ALPHA.get(recentsView)
+ } else {
+ if (
+ taskView.isVisibleInCarousel(
+ recentsView.runningTaskView,
+ nonRunningTaskCarouselHidden,
+ )
+ )
+ 1f
+ else 0f
+ }
+ }
+ }
+
+ fun TaskView.isVisibleInCarousel(
+ runningTaskView: TaskView?,
+ nonRunningTaskCarouselHidden: Boolean,
+ ): Boolean =
+ if (!nonRunningTaskCarouselHidden) true
+ else getCarouselType() == runningTaskView.getCarouselType()
+
+ /** Returns the carousel type of the TaskView, and default to fullscreen if it's null. */
+ private fun TaskView?.getCarouselType(): TaskViewCarousel =
+ if (this is DesktopTaskView) TaskViewCarousel.DESKTOP else TaskViewCarousel.FULL_SCREEN
+
+ private enum class TaskViewCarousel {
+ FULL_SCREEN,
+ DESKTOP,
+ }
+
+ /** Returns true if there are at least one TaskView has been added to the RecentsView. */
+ fun hasTaskViews() = taskViews.any()
+
+ fun getTaskContainerById(taskId: Int) =
+ taskViews.firstNotNullOfOrNull { it.getTaskContainerById(taskId) }
+
+ private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) {
+ outRowRect.setEmpty()
+ firstView?.let {
+ it.getHitRect(TEMP_RECT)
+ outRowRect.union(TEMP_RECT)
+ }
+ lastView?.let {
+ it.getHitRect(TEMP_RECT)
+ outRowRect.union(TEMP_RECT)
+ }
+ }
+
+ private fun getRowRect(rowTaskViewIds: IntArray, outRowRect: Rect) {
+ if (rowTaskViewIds.isEmpty) {
+ outRowRect.setEmpty()
+ return
+ }
+ getRowRect(
+ recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(0)),
+ recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(rowTaskViewIds.size() - 1)),
+ outRowRect,
+ )
+ }
+
+ fun updateTaskViewDeadZoneRect(
+ outTaskViewRowRect: Rect,
+ outTopRowRect: Rect,
+ outBottomRowRect: Rect,
+ ) {
+ if (!getDeviceProfile().isTablet) {
+ getRowRect(getFirstTaskView(), getLastTaskView(), outTaskViewRowRect)
+ return
+ }
+ getRowRect(getFirstLargeTaskView(), getLastLargeTaskView(), outTaskViewRowRect)
+ getRowRect(getTopRowIdArray(), outTopRowRect)
+ getRowRect(getBottomRowIdArray(), outBottomRowRect)
+
+ // Expand large tile Rect to include space between top/bottom row.
+ val nonEmptyRowRect =
+ when {
+ !outTopRowRect.isEmpty -> outTopRowRect
+ !outBottomRowRect.isEmpty -> outBottomRowRect
+ else -> return
+ }
+ if (recentsView.isRtl) {
+ if (outTaskViewRowRect.left > nonEmptyRowRect.right) {
+ outTaskViewRowRect.left = nonEmptyRowRect.right
+ }
+ } else {
+ if (outTaskViewRowRect.right < nonEmptyRowRect.left) {
+ outTaskViewRowRect.right = nonEmptyRowRect.left
+ }
+ }
+
+ // Expand the shorter row Rect to include the space between the 2 rows.
+ if (outTopRowRect.isEmpty || outBottomRowRect.isEmpty) return
+ if (outTopRowRect.width() <= outBottomRowRect.width()) {
+ if (outTopRowRect.bottom < outBottomRowRect.top) {
+ outTopRowRect.bottom = outBottomRowRect.top
+ }
+ } else {
+ if (outBottomRowRect.top > outTopRowRect.bottom) {
+ outBottomRowRect.top = outTopRowRect.bottom
+ }
+ }
+ }
+
+ private fun getTaskMenu(): TaskMenuView? =
+ getTopOpenViewWithType(recentsView.mContainer, TYPE_TASK_MENU) as? TaskMenuView
+
+ fun shouldInterceptKeyEvent(event: KeyEvent): Boolean {
+ if (enableOverviewIconMenu()) {
+ return getTaskMenu()?.isOpen == true || event.keyCode == KeyEvent.KEYCODE_TAB
+ }
+ return false
+ }
+
+ fun updateChildTaskOrientations() {
+ with(recentsView) {
+ taskViews.forEach { it.setOrientationState(mOrientationState) }
+ if (enableOverviewIconMenu()) {
+ children.forEach {
+ it.layoutDirection = if (isRtl) LAYOUT_DIRECTION_LTR else LAYOUT_DIRECTION_RTL
+ }
+ }
+
+ // Return when it's not fake landscape
+ if (mOrientationState.isRecentsActivityRotationAllowed) return@with
+
+ // Rotation is supported on phone (details at b/254198019#comment4)
+ getTaskMenu()?.onRotationChanged()
+ }
+ }
+
+ fun updateCentralTask() {
+ val isTablet: Boolean = getDeviceProfile().isTablet
+ val actionsViewCanRelateToTaskView = !(isTablet && enableGridOnlyOverview())
+ val focusedTaskView = recentsView.focusedTaskView
+ val currentPageTaskView = recentsView.currentPageTaskView
+
+ fun isInExpectedScrollPosition(taskView: TaskView?) =
+ taskView?.let { recentsView.isTaskInExpectedScrollPosition(it) } ?: false
+
+ val centralTaskIds: Set =
+ when {
+ !actionsViewCanRelateToTaskView -> emptySet()
+ isTablet && isInExpectedScrollPosition(focusedTaskView) ->
+ focusedTaskView!!.taskIdSet
+ isInExpectedScrollPosition(currentPageTaskView) -> currentPageTaskView!!.taskIdSet
+ else -> emptySet()
+ }
+
+ recentsView.mRecentsViewModel.updateCentralTaskIds(centralTaskIds)
+ }
+
+ var deskExplodeProgress: Float = 0f
+ set(value) {
+ field = value
+ taskViews.filterIsInstance().forEach { it.explodeProgress = field }
+ }
+
+ var selectedTaskView: TaskView? = null
+ set(newValue) {
+ val oldValue = field
+ field = newValue
+ if (oldValue != newValue) {
+ onSelectedTaskViewUpdated(oldValue, newValue)
+ }
+ }
+
+ private fun onSelectedTaskViewUpdated(
+ oldSelectedTaskView: TaskView?,
+ newSelectedTaskView: TaskView?,
+ ) {
+ if (!enableGridOnlyOverview()) return
+ with(recentsView) {
+ oldSelectedTaskView?.modalScale = 1f
+ oldSelectedTaskView?.modalPivot = null
+
+ if (newSelectedTaskView == null) return
+
+ val modalTaskBounds = mTempRect
+ getModalTaskSize(modalTaskBounds)
+ val selectedTaskBounds = getTaskBounds(newSelectedTaskView)
+
+ // Map bounds to selectedTaskView's coordinate system.
+ modalTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top)
+ selectedTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top)
+
+ val modalScale =
+ min(
+ (modalTaskBounds.height().toFloat() / selectedTaskBounds.height()),
+ (modalTaskBounds.width().toFloat() / selectedTaskBounds.width()),
+ )
+ val modalPivot = PointF()
+ getPivotsForScalingRectToRect(modalTaskBounds, selectedTaskBounds, modalPivot)
+
+ newSelectedTaskView.modalScale = modalScale
+ newSelectedTaskView.modalPivot = modalPivot
+ }
+ }
+
+ /**
+ * Creates a [DesktopTaskView] for the currently active desk on this display, which contains the
+ * tasks with the given [groupedTaskInfo].
+ */
+ fun createDesktopTaskViewForActiveDesk(groupedTaskInfo: GroupedTaskInfo): DesktopTaskView {
+ val desktopTaskView =
+ recentsView.getTaskViewFromPool(TaskViewType.DESKTOP) as DesktopTaskView
+ val tasks: List = groupedTaskInfo.taskInfoList.map { taskInfo -> Task.from(taskInfo) }
+ desktopTaskView.bind(
+ DesktopTask(groupedTaskInfo.deskId, groupedTaskInfo.deskDisplayId, tasks),
+ recentsView.mOrientationState,
+ recentsView.mTaskOverlayFactory,
+ )
+ return desktopTaskView
+ }
+
+ companion object {
+ class RecentsViewFloatProperty(
+ private val utilsProperty: KMutableProperty1
+ ) : FloatProperty>(utilsProperty.name) {
+ override fun get(recentsView: RecentsView<*, *>): Float =
+ utilsProperty.get(recentsView.mUtils)
+
+ override fun setValue(recentsView: RecentsView<*, *>, value: Float) {
+ utilsProperty.set(recentsView.mUtils, value)
+ }
+ }
+
+ @JvmField
+ val DESK_EXPLODE_PROGRESS = RecentsViewFloatProperty(RecentsViewUtils::deskExplodeProgress)
+
+ val TEMP_RECT = Rect()
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index e86b5a0fd9a..fb23039c62a 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,12 +16,10 @@
package com.android.quickstep.views;
-import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -40,11 +38,11 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
+import com.android.wm.shell.shared.TypefaceUtils;
+import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
/**
* A rounded rectangular component containing a single TextView.
@@ -56,7 +54,6 @@
public class SplitInstructionsView extends LinearLayout {
private static final int BOUNCE_DURATION = 250;
private static final float BOUNCE_HEIGHT = 20;
- private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
private final RecentsViewContainer mContainer;
public boolean mIsCurrentlyAnimating = false;
@@ -126,36 +123,35 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
}
private void init() {
- TextView cancelTextView = findViewById(R.id.split_instructions_text);
+ TextView cancelTextView = findViewById(R.id.split_instructions_text_cancel);
TextView instructionTextView = findViewById(R.id.split_instructions_text);
- if (FeatureFlags.enableSplitContextually()) {
- cancelTextView.setVisibility(VISIBLE);
- cancelTextView.setOnClickListener((v) -> exitSplitSelection());
- instructionTextView.setText(R.string.toast_contextual_split_select_app);
-
- // After layout, expand touch target of cancel button to meet minimum a11y measurements.
- post(() -> {
- int minTouchSize = getResources()
- .getDimensionPixelSize(R.dimen.settingslib_preferred_minimum_touch_target);
- Rect r = new Rect();
- cancelTextView.getHitRect(r);
-
- if (r.width() < minTouchSize) {
- // add 1 to ensure ceiling on int division
- int expandAmount = (minTouchSize + 1 - r.width()) / 2;
- r.left -= expandAmount;
- r.right += expandAmount;
- }
- if (r.height() < minTouchSize) {
- int expandAmount = (minTouchSize + 1 - r.height()) / 2;
- r.top -= expandAmount;
- r.bottom += expandAmount;
- }
+ cancelTextView.setVisibility(VISIBLE);
+ cancelTextView.setOnClickListener((v) -> exitSplitSelection());
+ instructionTextView.setText(R.string.toast_contextual_split_select_app);
+ TypefaceUtils.setTypeface(instructionTextView, FontFamily.GSF_BODY_MEDIUM);
+
+ // After layout, expand touch target of cancel button to meet minimum a11y measurements.
+ post(() -> {
+ int minTouchSize = getResources()
+ .getDimensionPixelSize(R.dimen.settingslib_preferred_minimum_touch_target);
+ Rect r = new Rect();
+ cancelTextView.getHitRect(r);
+
+ if (r.width() < minTouchSize) {
+ // add 1 to ensure ceiling on int division
+ int expandAmount = (minTouchSize + 1 - r.width()) / 2;
+ r.left -= expandAmount;
+ r.right += expandAmount;
+ }
+ if (r.height() < minTouchSize) {
+ int expandAmount = (minTouchSize + 1 - r.height()) / 2;
+ r.top -= expandAmount;
+ r.bottom += expandAmount;
+ }
- setTouchDelegate(new TouchDelegate(r, cancelTextView));
- });
- }
+ setTouchDelegate(new TouchDelegate(r, cancelTextView));
+ });
// Set accessibility title, will be announced by a11y tools.
if (Utilities.ATLEAST_P) {
@@ -166,25 +162,11 @@ private void init() {
private void exitSplitSelection() {
RecentsView recentsView = mContainer.getOverviewPanel();
SplitSelectStateController splitSelectController = recentsView.getSplitSelectController();
-
StateManager stateManager = recentsView.getStateManager();
- BaseState startState = stateManager.getState();
- long duration = startState.getTransitionDuration(mContainer.asContext(), false);
- if (duration == 0) {
- // Case where we're in contextual on workspace (NORMAL), which by default has 0
- // transition duration
- duration = DURATION_DEFAULT_SPLIT_DISMISS;
- }
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = duration;
- AnimatorSet stateAnim = stateManager.createAtomicAnimation(
- startState, NORMAL, config);
- AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController()
- .createPlaceholderDismissAnim(mContainer,
- LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration);
- stateAnim.play(dismissAnim);
- stateManager.setCurrentAnimation(stateAnim, NORMAL);
- stateAnim.start();
+
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON,
+ splitSelectController.getSplitAnimationController());
}
void ensureProperRotation() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
new file mode 100644
index 00000000000..086ac774f17
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2024 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.views
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.util.Log
+import android.view.View
+import android.view.View.OnClickListener
+import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.model.data.TaskViewItemInfo
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.TransformingTouchDelegate
+import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.ViewUtils.addAccessibleChildToList
+import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
+import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
+import com.android.quickstep.recents.ui.viewmodel.TaskData
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+
+/** Holder for all Task dependent information. */
+class TaskContainer(
+ val taskView: TaskView,
+ val task: Task,
+ val snapshotView: View,
+ val iconView: TaskViewIcon,
+ /**
+ * This technically can be a vanilla [android.view.TouchDelegate] class, however that class
+ * requires setting the touch bounds at construction, so we'd repeatedly be created many
+ * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows touch
+ * delegated bounds only to be updated.
+ */
+ val iconTouchDelegate: TransformingTouchDelegate,
+ /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */
+ @SplitConfigurationOptions.StagePosition val stagePosition: Int,
+ val digitalWellBeingToast: DigitalWellBeingToast?,
+ val showWindowsView: View?,
+ taskOverlayFactory: TaskOverlayFactory,
+) {
+ val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
+ var thumbnailPosition: ThumbnailPosition? = null
+ private var overlayEnabledStatus = false
+
+ init {
+ if (enableRefactorTaskThumbnail()) {
+ require(snapshotView is TaskThumbnailView)
+ } else {
+ require(snapshotView is TaskThumbnailViewDeprecated)
+ }
+ }
+
+ internal var thumbnailData: ThumbnailData? = null
+ private set
+
+ val thumbnail: Bitmap?
+ /** If possible don't use this. It should be replaced as part of b/331753115. */
+ get() =
+ if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail
+ else thumbnailViewDeprecated.thumbnail
+
+ val thumbnailView: TaskThumbnailView
+ get() {
+ require(enableRefactorTaskThumbnail())
+ return snapshotView as TaskThumbnailView
+ }
+
+ val thumbnailViewDeprecated: TaskThumbnailViewDeprecated
+ get() {
+ require(!enableRefactorTaskThumbnail())
+ return snapshotView as TaskThumbnailViewDeprecated
+ }
+
+ var isThumbnailValid: Boolean = false
+ internal set
+
+ val shouldShowSplashView: Boolean
+ get() =
+ if (enableRefactorTaskThumbnail()) taskView.shouldShowSplash()
+ else thumbnailViewDeprecated.shouldShowSplashView()
+
+ /** Builds proto for logging */
+ val itemInfo: TaskViewItemInfo
+ get() = TaskViewItemInfo(taskView, this)
+
+ fun bind() = {
+ digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
+ if (!enableRefactorTaskThumbnail()) {
+ thumbnailViewDeprecated.bind(task, overlay, taskView)
+ }
+ }
+
+ fun destroy() = {
+ digitalWellBeingToast?.destroy()
+ snapshotView.scaleX = 1f
+ snapshotView.scaleY = 1f
+ overlay.reset()
+ if (enableRefactorTaskThumbnail()) {
+ isThumbnailValid = false
+ thumbnailData = null
+ thumbnailView.onRecycle()
+ } else {
+ thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
+ }
+
+ if (enableOverviewIconMenu()) {
+ (iconView as IconAppChipView).reset()
+ }
+ }
+
+ fun setOverlayEnabled(enabled: Boolean) {
+ if (!enableRefactorTaskThumbnail()) {
+ thumbnailViewDeprecated.setOverlayEnabled(enabled)
+ }
+ }
+
+ fun setOverlayEnabled(enabled: Boolean, thumbnailPosition: ThumbnailPosition?) {
+ if (enableRefactorTaskThumbnail()) {
+ if (overlayEnabledStatus != enabled || this.thumbnailPosition != thumbnailPosition) {
+ overlayEnabledStatus = enabled
+
+ refreshOverlay(thumbnailPosition)
+ }
+ }
+ }
+
+ fun refreshOverlay(thumbnailPosition: ThumbnailPosition?) = {
+ this.thumbnailPosition = thumbnailPosition
+ when {
+ !overlayEnabledStatus -> overlay.reset()
+ thumbnailPosition == null -> {
+ Log.e(TAG, "Thumbnail position was null during overlay refresh", Exception())
+ overlay.reset()
+ }
+ else ->
+ overlay.initOverlay(
+ task,
+ thumbnailData?.thumbnail,
+ thumbnailPosition.matrix,
+ thumbnailPosition.isRotated,
+ )
+ }
+ }
+
+ fun addChildForAccessibility(outChildren: ArrayList