Merge "Taskbar in Desktop Windowing Mode" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
index 8189913..3649c4e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
@@ -40,12 +40,17 @@
*/
class DesktopTaskbarRunningAppsController(
private val recentsModel: RecentsModel,
- private val desktopVisibilityController: DesktopVisibilityController?,
+ // Pass a provider here instead of the actual DesktopVisibilityController instance since that
+ // instance might not be available when this constructor is called.
+ private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?,
) : TaskbarRecentAppsController() {
private var apps: Array<AppInfo>? = null
private var allRunningDesktopAppInfos: List<AppInfo>? = null
+ private val desktopVisibilityController: DesktopVisibilityController?
+ get() = desktopVisibilityControllerProvider()
+
private val isInDesktopMode: Boolean
get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 9cdf331..8845813 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -313,7 +313,7 @@
if (enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()) {
return new DesktopTaskbarRunningAppsController(
RecentsModel.INSTANCE.get(this),
- LauncherActivityInterface.INSTANCE.getDesktopVisibilityController());
+ LauncherActivityInterface.INSTANCE::getDesktopVisibilityController);
}
return TaskbarRecentAppsController.DEFAULT;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 2162a73..182ff7e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -319,8 +319,7 @@
updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, isStashedInAppAuto);
updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup);
updateStateForFlag(FLAG_IN_SETUP, isInSetup);
- updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, mActivity.isPhoneMode()
- && !mActivity.isThreeButtonNav());
+ updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, mActivity.isPhoneGestureNavMode());
// For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell
// us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
updateStateForFlag(FLAG_IN_APP, true);
@@ -356,6 +355,7 @@
boolean hideTaskbar = isVisible || !mActivity.isUserSetupComplete();
updateStateForFlag(FLAG_IN_SETUP, hideTaskbar);
updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, hideTaskbar);
+ updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, mActivity.isPhoneGestureNavMode());
applyState(hideTaskbar ? 0 : getStashDuration());
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 981c9f9..66e5302 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -419,9 +419,7 @@
}
if (update.bubbleBarLocation != null) {
if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) {
- // Animate when receiving updates. Skip it if we received the initial state.
- boolean animate = !update.initialState;
- updateBubbleBarLocationInternal(update.bubbleBarLocation, animate);
+ updateBubbleBarLocationInternal(update.bubbleBarLocation);
}
}
}
@@ -483,15 +481,21 @@
* Updates the value locally in Launcher and in WMShell.
*/
public void updateBubbleBarLocation(BubbleBarLocation location) {
- updateBubbleBarLocationInternal(location, false /* animate */);
+ updateBubbleBarLocationInternal(location);
mSystemUiProxy.setBubbleBarLocation(location);
}
- private void updateBubbleBarLocationInternal(BubbleBarLocation location, boolean animate) {
- mBubbleBarViewController.setBubbleBarLocation(location, animate);
+ private void updateBubbleBarLocationInternal(BubbleBarLocation location) {
+ mBubbleBarViewController.setBubbleBarLocation(location);
mBubbleStashController.setBubbleBarLocation(location);
}
+ @Override
+ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+ mMainExecutor.execute(
+ () -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
+ }
+
//
// Loading data for the bubbles
//
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 8c6cbc9..60e8abe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -153,8 +153,6 @@
private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED;
- private boolean mLocationChangePending;
-
public BubbleBarView(Context context) {
this(context, null);
}
@@ -188,7 +186,7 @@
mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS);
mWidthAnimator.addUpdateListener(animation -> {
- updateChildrenRenderNodeProperties();
+ updateChildrenRenderNodeProperties(mBubbleBarLocation);
invalidate();
});
mWidthAnimator.addListener(new Animator.AnimatorListener() {
@@ -262,7 +260,7 @@
setPivotY(mRelativePivotY * getHeight());
// Position the views
- updateChildrenRenderNodeProperties();
+ updateChildrenRenderNodeProperties(mBubbleBarLocation);
}
@Override
@@ -278,7 +276,6 @@
@SuppressLint("RtlHardcoded")
private void onBubbleBarLocationChanged() {
- mLocationChangePending = false;
final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
mBubbleBarBackground.setAnchorLeft(onLeft);
mRelativePivotX = onLeft ? 0f : 1f;
@@ -299,11 +296,18 @@
/**
* Update {@link BubbleBarLocation}
*/
- public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, boolean animate) {
- if (animate) {
- animateToBubbleBarLocation(bubbleBarLocation);
- } else {
- setBubbleBarLocationInternal(bubbleBarLocation);
+ public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+ if (mBubbleBarLocationAnimator != null) {
+ mBubbleBarLocationAnimator.removeAllListeners();
+ mBubbleBarLocationAnimator.cancel();
+ mBubbleBarLocationAnimator = null;
+ }
+ setTranslationX(0f);
+ setAlpha(1f);
+ if (bubbleBarLocation != mBubbleBarLocation) {
+ mBubbleBarLocation = bubbleBarLocation;
+ onBubbleBarLocationChanged();
+ invalidate();
}
}
@@ -316,51 +320,37 @@
}
mDragging = dragging;
setElevation(dragging ? mDragElevation : mBubbleElevation);
- if (!dragging && mLocationChangePending) {
- // During drag finish animation we may update the translation x value to shift the
- // bubble to the new drop target. Clear the translation here.
- setTranslationX(0f);
- onBubbleBarLocationChanged();
- }
}
/**
- * Adjust resting position for the bubble bar while it is being dragged.
- * <p>
- * Bubble bar is laid out on left or right side of the screen. When it is being dragged to
- * the opposite side, the resting position should be on that side. Calculate any additional
- * translation that may be required to move the bubble bar to the new side.
+ * Get translation for bubble bar when drag is released and it needs to animate back to the
+ * resting position.
+ * Resting position is based on the supplied location. If the supplied location is different
+ * from the internal location that was used to lay out the bubble bar, translation values are
+ * calculated to position the bar at the desired location.
*
- * @param restingPosition relative resting position of the bubble bar from the laid out position
+ * @param initialTranslation initial bubble bar translation at the start of drag
+ * @param location desired location of the bubble bar when drag is released
+ * @return point with x and y values representing translation on x and y-axis
*/
- @SuppressLint("RtlHardcoded")
- void adjustRelativeRestingPosition(PointF restingPosition) {
- final boolean locationOnLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
- // Bubble bar is placed left or right with gravity. Check where it is currently.
- final int absoluteGravity = Gravity.getAbsoluteGravity(
- ((LayoutParams) getLayoutParams()).gravity, getLayoutDirection());
- final boolean gravityOnLeft =
- (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT;
-
- // Bubble bar is pinned to the same side per gravity and the desired location.
- // Resting translation does not need to be adjusted.
- if (locationOnLeft == gravityOnLeft) {
- return;
- }
-
+ public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation,
+ BubbleBarLocation location) {
+ // Start with the initial translation. Value on y-axis can be reused.
+ final PointF dragEndTranslation = new PointF(initialTranslation);
// Bubble bar is laid out on left or right side of the screen. And the desired new
- // location is on the other side. Calculate x translation value required to shift the
+ // location is on the other side. Calculate x translation value required to shift
// bubble bar from one side to the other.
- float x = getDistanceFromOtherSide();
- if (locationOnLeft) {
+ final float shift = getDistanceFromOtherSide();
+ if (location.isOnLeft(isLayoutRtl())) {
// New location is on the left, shift left
// before -> |......ooo.| after -> |.ooo......|
- restingPosition.x = -x;
+ dragEndTranslation.x = -shift;
} else {
// New location is on the right, shift right
// before -> |.ooo......| after -> |......ooo.|
- restingPosition.x = x;
+ dragEndTranslation.x = shift;
}
+ return dragEndTranslation;
}
private float getDistanceFromOtherSide() {
@@ -374,49 +364,40 @@
return (float) (displayWidth - getWidth() - margin);
}
- private void setBubbleBarLocationInternal(BubbleBarLocation bubbleBarLocation) {
- if (bubbleBarLocation != mBubbleBarLocation) {
- mBubbleBarLocation = bubbleBarLocation;
- if (mDragging) {
- mLocationChangePending = true;
- } else {
- onBubbleBarLocationChanged();
- invalidate();
- }
- }
- }
-
- private void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
- if (bubbleBarLocation == mBubbleBarLocation) {
- // nothing to do, already at expected location
- return;
- }
+ /**
+ * Animate bubble bar to the given location transiently. Does not modify the layout or the value
+ * returned by {@link #getBubbleBarLocation()}.
+ */
+ public void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
if (mBubbleBarLocationAnimator != null && mBubbleBarLocationAnimator.isRunning()) {
+ mBubbleBarLocationAnimator.removeAllListeners();
mBubbleBarLocationAnimator.cancel();
}
// Location animation uses two separate animators.
// First animator hides the bar.
- // After it completes, location update is sent to layout the bar in the new location.
+ // After it completes, bubble positions in the bar and arrow position is updated.
// Second animator is started to show the bar.
- mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator();
+ mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator(bubbleBarLocation);
mBubbleBarLocationAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- // Bubble bar is not visible, update the location
- setBubbleBarLocationInternal(bubbleBarLocation);
+ updateChildrenRenderNodeProperties(bubbleBarLocation);
+ mBubbleBarBackground.setAnchorLeft(bubbleBarLocation.isOnLeft(isLayoutRtl()));
+
// Animate it in
- mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator();
+ mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator(bubbleBarLocation);
mBubbleBarLocationAnimator.start();
}
});
mBubbleBarLocationAnimator.start();
}
- private AnimatorSet getLocationUpdateFadeOutAnimator() {
+ private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation bubbleBarLocation) {
final float shift =
getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT;
- final float tx = mBubbleBarLocation.isOnLeft(isLayoutRtl()) ? shift : -shift;
+ final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
+ final float tx = getTranslationX() + (onLeft ? shift : -shift);
ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X, tx)
.setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS);
@@ -431,14 +412,31 @@
return animatorSet;
}
- private Animator getLocationUpdateFadeInAnimator() {
+ private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation animatedLocation) {
final float shift =
getResources().getDisplayMetrics().widthPixels * FADE_IN_ANIM_POSITION_SHIFT;
- final float startTx = mBubbleBarLocation.isOnLeft(isLayoutRtl()) ? shift : -shift;
+
+ final boolean onLeft = animatedLocation.isOnLeft(isLayoutRtl());
+ final float startTx;
+ final float finalTx;
+ if (animatedLocation == mBubbleBarLocation) {
+ // Animated location matches layout location.
+ finalTx = 0;
+ } else {
+ // We are animating in to a transient location, need to move the bar accordingly.
+ finalTx = getDistanceFromOtherSide() * (onLeft ? -1 : 1);
+ }
+ if (onLeft) {
+ // Bar will be shown on the left side. Start point is shifted right.
+ startTx = finalTx + shift;
+ } else {
+ // Bar will be shown on the right side. Start point is shifted left.
+ startTx = finalTx - shift;
+ }
ValueAnimator positionAnim = new SpringAnimationBuilder(getContext())
.setStartValue(startTx)
- .setEndValue(0)
+ .setEndValue(finalTx)
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
.setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
.build(this, VIEW_TRANSLATE_X);
@@ -547,7 +545,7 @@
* Updates the z order, positions, and badge visibility of the bubble views in the bar based
* on the expanded state.
*/
- private void updateChildrenRenderNodeProperties() {
+ private void updateChildrenRenderNodeProperties(BubbleBarLocation bubbleBarLocation) {
final float widthState = (float) mWidthAnimator.getAnimatedValue();
final float currentWidth = getWidth();
final float expandedWidth = expandedWidth();
@@ -555,7 +553,7 @@
int bubbleCount = getChildCount();
final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
final boolean animate = getVisibility() == VISIBLE;
- final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
+ final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
// elevation state is opposite to widthState - when expanded all icons are flat
float elevationState = (1 - widthState);
for (int i = 0; i < bubbleCount; i++) {
@@ -613,8 +611,9 @@
}
// update the arrow position
- final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed();
- final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded();
+ final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed(
+ bubbleBarLocation);
+ final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded(bubbleBarLocation);
final float interpolatedWidth =
widthState * (expandedWidth - collapsedWidth) + collapsedWidth;
final float arrowPosition;
@@ -661,7 +660,7 @@
addViewInLayout(child, i, child.getLayoutParams());
}
}
- updateChildrenRenderNodeProperties();
+ updateChildrenRenderNodeProperties(mBubbleBarLocation);
}
}
@@ -702,7 +701,7 @@
return;
}
// Find the center of the bubble when it's expanded, set the arrow position to it.
- final float tx = arrowPositionForSelectedWhenExpanded();
+ final float tx = arrowPositionForSelectedWhenExpanded(mBubbleBarLocation);
final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX();
if (tx == currentArrowPosition) {
// arrow position remains unchanged
@@ -727,10 +726,10 @@
}
}
- private float arrowPositionForSelectedWhenExpanded() {
+ private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) {
final int index = indexOfChild(mSelectedBubbleView);
final int bubblePosition;
- if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+ if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
// Bubble positions are reversed. First bubble is on the right.
bubblePosition = getChildCount() - index - 1;
} else {
@@ -740,10 +739,10 @@
+ mIconSize / 2f;
}
- private float arrowPositionForSelectedWhenCollapsed() {
+ private float arrowPositionForSelectedWhenCollapsed(BubbleBarLocation bubbleBarLocation) {
final int index = indexOfChild(mSelectedBubbleView);
final int bubblePosition;
- if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+ if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
// Bubble positions are reversed. First bubble may be shifted, if there are more
// bubbles than the current bubble and overflow.
bubblePosition = index == 0 && getChildCount() > 2 ? 1 : 0;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0b92748..dc48a66 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -215,8 +215,17 @@
/**
* Update bar {@link BubbleBarLocation}
*/
- public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, boolean animate) {
- mBarView.setBubbleBarLocation(bubbleBarLocation, animate);
+ public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+ mBarView.setBubbleBarLocation(bubbleBarLocation);
+ }
+
+ /**
+ * Animate bubble bar to the given location. The location change is transient. It does not
+ * update the state of the bubble bar.
+ * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}.
+ */
+ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+ mBarView.animateToBubbleBarLocation(bubbleBarLocation);
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
index 8b811d9..49f114a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -110,25 +110,25 @@
/**
* Animates the dragged bubble movement back to the initial position.
*
- * @param initialPosition the position to animate to
+ * @param restingPosition the position to animate to
* @param velocity the initial velocity to use for the spring animation
* @param endActions gets called when the animation completes or gets cancelled
*/
- public void animateToInitialState(@NonNull PointF initialPosition, @NonNull PointF velocity,
+ public void animateToRestingState(@NonNull PointF restingPosition, @NonNull PointF velocity,
@Nullable Runnable endActions) {
mBubbleAnimator.cancel();
mBubbleAnimator
.spring(DynamicAnimation.SCALE_X, 1f)
.spring(DynamicAnimation.SCALE_Y, 1f)
- .spring(DynamicAnimation.TRANSLATION_X, initialPosition.x, velocity.x,
+ .spring(DynamicAnimation.TRANSLATION_X, restingPosition.x, velocity.x,
mTranslationConfig)
- .spring(DynamicAnimation.TRANSLATION_Y, initialPosition.y, velocity.y,
+ .spring(DynamicAnimation.TRANSLATION_Y, restingPosition.y, velocity.y,
mTranslationConfig)
.addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
boolean allRelevantPropertyAnimationsEnded) -> {
if (canceled || allRelevantPropertyAnimationsEnded) {
- resetAnimatedViews(initialPosition);
+ resetAnimatedViews(restingPosition);
if (endActions != null) {
endActions.run();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 5ffc6d8..d1c9da7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -26,6 +26,8 @@
import androidx.annotation.Nullable;
import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
/**
* Controls bubble bar drag interactions.
@@ -37,6 +39,7 @@
*/
public class BubbleDragController {
private final TaskbarActivityContext mActivity;
+ private BubbleBarController mBubbleBarController;
private BubbleBarViewController mBubbleBarViewController;
private BubbleDismissController mBubbleDismissController;
private BubbleBarPinController mBubbleBarPinController;
@@ -51,11 +54,10 @@
* controllers may still be waiting for init().
*/
public void init(@NonNull BubbleControllers bubbleControllers) {
+ mBubbleBarController = bubbleControllers.bubbleBarController;
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
mBubbleDismissController = bubbleControllers.bubbleDismissController;
mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
- mBubbleBarPinController.setListener(
- bubbleControllers.bubbleBarController::updateBubbleBarLocation);
mBubbleDismissController.setListener(
stuck -> mBubbleBarPinController.setDropTargetHidden(stuck));
}
@@ -96,6 +98,17 @@
PointF initialRelativePivot = new PointF();
bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
+ @Nullable
+ private BubbleBarLocation mReleasedLocation;
+
+ private final LocationChangeListener mLocationChangeListener =
+ new LocationChangeListener() {
+ @Override
+ public void onRelease(@NonNull BubbleBarLocation location) {
+ mReleasedLocation = location;
+ }
+ };
+
@Override
protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
if (bubbleBarView.isExpanded()) return false;
@@ -104,6 +117,7 @@
@Override
void onDragStart() {
+ mBubbleBarPinController.setListener(mLocationChangeListener);
initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
bubbleBarView.getRelativePivotY());
// By default the bubble bar view pivot is in bottom right corner, while dragging
@@ -134,13 +148,17 @@
// Restoring the initial pivot for the bubble bar view
bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
bubbleBarView.setIsDragging(false);
+ mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
}
@Override
protected PointF getRestingPosition() {
- PointF restingPosition = super.getRestingPosition();
- bubbleBarView.adjustRelativeRestingPosition(restingPosition);
- return restingPosition;
+ if (mReleasedLocation == null
+ || mReleasedLocation == bubbleBarView.getBubbleBarLocation()) {
+ return getInitialPosition();
+ }
+ return bubbleBarView.getBubbleBarDragReleaseTranslation(getInitialPosition(),
+ mReleasedLocation);
}
});
}
@@ -229,6 +247,13 @@
}
/**
+ * Get the initial position of the view when drag started
+ */
+ protected PointF getInitialPosition() {
+ return mViewInitialPosition;
+ }
+
+ /**
* Get the resting position of the view when drag is released
*/
protected PointF getRestingPosition() {
@@ -362,7 +387,7 @@
mAnimator.animateDismiss(mViewInitialPosition, onComplete);
} else {
onDragRelease();
- mAnimator.animateToInitialState(getRestingPosition(), getCurrentVelocity(),
+ mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(),
onComplete);
}
mBubbleDismissController.hideDismissView();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4acddee..4184ab2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -166,6 +166,8 @@
import com.android.launcher3.util.TouchController;
import com.android.launcher3.widget.LauncherWidgetHolder;
import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskUtils;
@@ -264,6 +266,10 @@
getDepthController(), getStatsLogManager(),
systemUiProxy, RecentsModel.INSTANCE.get(this),
() -> onStateBack());
+ RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(asContext());
+ // TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
+ OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
+ asContext(), deviceState);
if (enableDesktopWindowingMode()) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
getStateManager(), systemUiProxy, getIApplicationThread(),
@@ -272,7 +278,7 @@
overviewPanel.init(mActionsView, mSplitSelectStateController,
mDesktopRecentsTransitionController);
mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
- mSplitSelectStateController);
+ mSplitSelectStateController, overviewComponentObserver, deviceState);
mSplitToWorkspaceController = new SplitToWorkspaceController(this,
mSplitSelectStateController);
mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
@@ -287,7 +293,8 @@
if (enableDesktopWindowingMode()) {
mDesktopVisibilityController = new DesktopVisibilityController(this);
mDesktopVisibilityController.registerSystemUiListener();
- mSplitSelectStateController.initSplitFromDesktopController(this);
+ mSplitSelectStateController.initSplitFromDesktopController(this,
+ overviewComponentObserver);
}
mHotseatPredictionController = new HotseatPredictionController(this);
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index c8a91df..81c9d4a 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -186,6 +186,7 @@
private final long mSwipeUpStartTimeMs = SystemClock.uptimeMillis();
private boolean mHandlingAtomicEvent;
+ private boolean mIsInExtendedSlopRegion;
public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
mHomeIntent = componentObserver.getHomeIntent();
@@ -494,6 +495,25 @@
}
/**
+ * Set whether it's in long press nav handle (LPNH)'s extended touch slop region, e.g., second
+ * stage region in order to continue respect LPNH and ignore other touch slop logic.
+ * This will only be set to true when flag ENABLE_LPNH_TWO_STAGES is turned on.
+ */
+ public void setIsInExtendedSlopRegion(boolean isInExtendedSlopRegion) {
+ if (DeviceConfigWrapper.get().getEnableLpnhTwoStages()) {
+ mIsInExtendedSlopRegion = isInExtendedSlopRegion;
+ }
+ }
+
+ /**
+ * Returns whether it's in LPNH's extended touch slop region. This is only valid when flag
+ * ENABLE_LPNH_TWO_STAGES is turned on.
+ */
+ public boolean isInExtendedSlopRegion() {
+ return mIsInExtendedSlopRegion;
+ }
+
+ /**
* Returns and clears the canceled animation thumbnail data. This call only returns a value
* while STATE_RECENTS_ANIMATION_CANCELED state is being set, and the caller is responsible for
* calling {@link RecentsAnimationController#cleanupScreenshot()}.
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 0a02e99..a71e314 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -109,6 +109,8 @@
* Sets a listener for changes in {@link #isHomeAndOverviewSame()}
*/
public void setOverviewChangeListener(Consumer<Boolean> overviewChangeListener) {
+ // TODO(b/337861962): This method should be able to support multiple listeners instead of
+ // one so that we can reuse the same instance of this class across multiple places
mOverviewChangeListener = overviewChangeListener;
}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 832f4e1..f94a29c 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -1018,7 +1018,7 @@
.append("TaskbarActivityContext != null, ")
.append("using TaskbarUnstashInputConsumer");
base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
- mOverviewCommandHelper);
+ mOverviewCommandHelper, mGestureState);
}
}
if (enableBubblesLongPressNavHandle()) {
@@ -1046,7 +1046,7 @@
}
reasonString.append("using NavHandleLongPressInputConsumer");
base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat,
- mDeviceState, navHandle);
+ mDeviceState, navHandle, mGestureState);
}
if (!enableBubblesLongPressNavHandle()) {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d881a1f..b79586b 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -53,6 +53,7 @@
import com.android.systemui.shared.recents.model.Task;
import java.util.ArrayList;
+import java.util.Arrays;
public class FallbackRecentsView extends RecentsView<RecentsActivity, RecentsState>
implements StateListener<RecentsState> {
@@ -144,8 +145,9 @@
@Override
public void setCurrentTask(int runningTaskViewId) {
super.setCurrentTask(runningTaskViewId);
- int runningTaskId = getTaskIdsForRunningTaskView()[0];
- if (mHomeTask != null && mHomeTask.key.id != runningTaskId) {
+ int[] runningTaskIds = getTaskIdsForRunningTaskView();
+ if (mHomeTask != null
+ && Arrays.stream(runningTaskIds).noneMatch(taskId -> taskId == mHomeTask.key.id)) {
mHomeTask = null;
setRunningTaskHidden(false);
}
@@ -182,13 +184,14 @@
// as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
// track the index of the next task appropriately, as if we are switching on any other app.
// TODO(b/195607777) Confirm home task info is front-most task and not mixed in with others
- int runningTaskId = getTaskIdsForRunningTaskView()[0];
- if (mHomeTask != null && mHomeTask.key.id == runningTaskId
+ int[] runningTaskIds = getTaskIdsForRunningTaskView();
+ if (mHomeTask != null
+ && Arrays.stream(runningTaskIds).allMatch(taskId -> taskId == mHomeTask.key.id)
&& !taskGroups.isEmpty()) {
// Check if the task list has running task
boolean found = false;
for (GroupTask group : taskGroups) {
- if (group.containsTask(runningTaskId)) {
+ if (Arrays.stream(runningTaskIds).allMatch(group::containsTask)) {
found = true;
break;
}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index 075e539..848a43a 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -31,6 +31,7 @@
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.GestureState;
import com.android.quickstep.InputConsumer;
import com.android.quickstep.NavHandle;
import com.android.quickstep.RecentsAnimationDeviceState;
@@ -61,13 +62,14 @@
private final NavHandle mNavHandle;
private final StatsLogManager mStatsLogManager;
private final TopTaskTracker mTopTaskTracker;
+ private final GestureState mGestureState;
private MotionEvent mCurrentDownEvent;
private boolean mDeepPressLogged; // Whether deep press has been logged for the current touch.
public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState,
- NavHandle navHandle) {
+ NavHandle navHandle, GestureState gestureState) {
super(delegate, inputMonitor);
mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress();
@@ -82,6 +84,8 @@
mTouchSlopSquaredOriginal = deviceState.getSquaredTouchSlop();
mTouchSlopSquared = mTouchSlopSquaredOriginal;
mOuterTouchSlopSquared = mTouchSlopSquared * (twoStageMultiplier * twoStageMultiplier);
+ mGestureState = gestureState;
+ mGestureState.setIsInExtendedSlopRegion(false);
if (DEBUG_NAV_HANDLE) {
Log.d(TAG, "mLongPressTimeout=" + mLongPressTimeout);
Log.d(TAG, "mOuterLongPressTimeout=" + mOuterLongPressTimeout);
@@ -126,6 +130,7 @@
}
mCurrentDownEvent = MotionEvent.obtain(ev);
mTouchSlopSquared = mTouchSlopSquaredOriginal;
+ mGestureState.setIsInExtendedSlopRegion(false);
mDeepPressLogged = false;
if (isInNavBarHorizontalArea(ev.getRawX())) {
mNavHandleLongPressHandler.onTouchStarted(mNavHandle);
@@ -154,6 +159,7 @@
- (int) (ev.getEventTime() - ev.getDownTime());
MAIN_EXECUTOR.getHandler().postDelayed(mTriggerLongPress, delay);
mTouchSlopSquared = mOuterTouchSlopSquared;
+ mGestureState.setIsInExtendedSlopRegion(true);
if (DEBUG_NAV_HANDLE) {
Log.d(TAG, "Touch in middle region!");
}
@@ -219,6 +225,7 @@
if (DEBUG_NAV_HANDLE) {
Log.d(TAG, "cancelLongPress");
}
+ mGestureState.setIsInExtendedSlopRegion(false);
MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
mNavHandleLongPressHandler.onTouchFinished(mNavHandle, reason);
}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 9f39476..0d450c6 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -284,8 +284,9 @@
float horizontalDist = Math.abs(displacementX);
float upDist = -displacement;
- boolean passedSlop = mGestureState.isTrackpadGesture() || squaredHypot(
- displacementX, displacementY) >= mSquaredTouchSlop;
+ boolean passedSlop = mGestureState.isTrackpadGesture()
+ || (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop
+ && !mGestureState.isInExtendedSlopRegion());
if (!mPassedSlopOnThisGesture && passedSlop) {
mPassedSlopOnThisGesture = true;
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index bb8d1d7..c61f71d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -48,6 +48,7 @@
private final BaseContainerInterface<?, T> mContainerInterface;
private final BaseDragLayer mTarget;
private final InputMonitorCompat mInputMonitor;
+ private final GestureState mGestureState;
private final int[] mLocationOnScreen = new int[2];
@@ -62,6 +63,7 @@
mInputMonitor = inputMonitor;
mStartingInActivityBounds = startingInActivityBounds;
mContainerInterface = gestureState.getContainerInterface();
+ mGestureState = gestureState;
mTarget = container.getDragLayer();
mTarget.getLocationOnScreen(mLocationOnScreen);
@@ -84,7 +86,10 @@
ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR);
}
ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
- boolean handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
+ boolean handled = false;
+ if (mGestureState == null || !mGestureState.isInExtendedSlopRegion()) {
+ handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
+ }
ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
ev.setEdgeFlags(flags);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index cd180ba..6b3e6e9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -42,6 +42,7 @@
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.GestureState;
import com.android.quickstep.InputConsumer;
import com.android.quickstep.OverviewCommandHelper;
import com.android.systemui.shared.system.InputMonitorCompat;
@@ -76,10 +77,11 @@
private final int mStashedTaskbarBottomEdge;
private final @Nullable TransitionCallback mTransitionCallback;
+ private final GestureState mGestureState;
public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext,
- OverviewCommandHelper overviewCommandHelper) {
+ OverviewCommandHelper overviewCommandHelper, GestureState gestureState) {
super(delegate, inputMonitor);
mTaskbarActivityContext = taskbarActivityContext;
mOverviewCommandHelper = overviewCommandHelper;
@@ -103,6 +105,7 @@
mTransitionCallback = mIsTransientTaskbar
? taskbarActivityContext.getTranslationCallbacks()
: null;
+ mGestureState = gestureState;
}
@Override
@@ -111,6 +114,11 @@
}
@Override
+ public boolean allowInterceptByParent() {
+ return super.allowInterceptByParent() && !mHasPassedTaskbarNavThreshold;
+ }
+
+ @Override
public void onMotionEvent(MotionEvent ev) {
if (mState != STATE_ACTIVE) {
boolean isStashedTaskbarHovered = isMouseEvent(ev)
@@ -173,7 +181,8 @@
boolean passedTaskbarNavThreshold = dY < 0
&& Math.abs(dY) >= mTaskbarNavThreshold;
- if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
+ if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold
+ && !mGestureState.isInExtendedSlopRegion()) {
mHasPassedTaskbarNavThreshold = true;
if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) {
mTaskbarActivityContext.onSwipeToOpenBubblebar();
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index b3f5d82..07f2d68 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -56,4 +56,10 @@
public DesktopTask copy() {
return new DesktopTask(tasks);
}
+
+ @Override
+ public String toString() {
+ return "type=" + taskViewType + " tasks=" + tasks;
+ }
+
}
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index 9c49647..7dd6afc 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -70,4 +70,10 @@
task2 != null ? new Task(task2) : null,
mSplitBounds);
}
+
+ @Override
+ public String toString() {
+ return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2;
+ }
+
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index f430d79..ee2c2e1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -23,6 +23,7 @@
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.content.Context
import android.graphics.Bitmap
@@ -50,6 +51,7 @@
import com.android.launcher3.apppairs.AppPairIcon
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.logging.StatsLogManager.EventEnum
+import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.statehandlers.DepthController
import com.android.launcher3.statemanager.StateManager
import com.android.launcher3.taskbar.TaskbarActivityContext
@@ -69,6 +71,7 @@
import com.android.quickstep.views.TaskView
import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
import com.android.quickstep.views.TaskViewIcon
+import com.android.wm.shell.shared.TransitionUtil
import java.util.Optional
import java.util.function.Supplier
@@ -553,8 +556,14 @@
check(info != null && t != null) {
"trying to launch an app pair icon, but encountered an unexpected null"
}
-
- composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+ val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info)
+ if (appPairLaunchingAppIndex == -1) {
+ // Launch split app pair animation
+ composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+ } else {
+ composeFullscreenIconSplitLaunchAnimator(launchingIconView, info, t,
+ finishCallback, appPairLaunchingAppIndex)
+ }
} else {
// Fallback case: simple fade-in animation
check(info != null && t != null) {
@@ -619,6 +628,39 @@
}
/**
+ * @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise
+ * the integer index corresponding to [launchingIconView]'s contents for the single app
+ * to be animated
+ */
+ fun hasChangesForBothAppPairs(launchingIconView: AppPairIcon,
+ transitionInfo: TransitionInfo) : Int {
+ val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
+ val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
+ var launchFullscreenAppIndex = -1
+ for (change in transitionInfo.changes) {
+ val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+ if (TransitionUtil.isOpeningType(change.mode) &&
+ taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN) {
+ val baseIntent = taskInfo.baseIntent.component?.packageName
+ if (baseIntent == intent1) {
+ if (launchFullscreenAppIndex > -1) {
+ launchFullscreenAppIndex = -1
+ break
+ }
+ launchFullscreenAppIndex = 0
+ } else if (baseIntent == intent2) {
+ if (launchFullscreenAppIndex > -1) {
+ launchFullscreenAppIndex = -1
+ break
+ }
+ launchFullscreenAppIndex = 1
+ }
+ }
+ }
+ return launchFullscreenAppIndex
+ }
+
+ /**
* When the user taps an app pair icon to launch split, this will play the tasks' launch
* animation from the position of the icon.
*
@@ -653,7 +695,8 @@
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
// use the scale-up animation
if (launchingIconView.context is TaskbarActivityContext) {
- composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback)
+ composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
+ WINDOWING_MODE_MULTI_WINDOW)
return
}
@@ -663,11 +706,6 @@
// Create an AnimatorSet that will run both shell and launcher transitions together
val launchAnimation = AnimatorSet()
- val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
- val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
- progressUpdater.setDuration(timings.getDuration().toLong())
- progressUpdater.interpolator = Interpolators.LINEAR
-
var rootCandidate: Change? = null
for (change in transitionInfo.changes) {
@@ -711,27 +749,13 @@
// Make sure nothing weird happened, like getChange() returning null.
check(rootCandidate != null) { "Failed to find a root leash" }
- // Shell animation: the apps are revealed toward end of the launch animation
- progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
- val progress =
- Interpolators.clampToProgress(
- Interpolators.LINEAR,
- valueAnimator.animatedFraction,
- timings.appRevealStartOffset,
- timings.appRevealEndOffset
- )
-
- // Set the alpha of the shell layer (2 apps + divider)
- t.setAlpha(rootCandidate.leash, progress)
- t.apply()
- }
-
// Create a new floating view in Launcher, positioned above the launching icon
val drawableArea = launchingIconView.iconDrawableArea
val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context)
val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context)
appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+
val floatingView =
FloatingAppPairView.getFloatingAppPairView(
launcher,
@@ -742,84 +766,189 @@
)
floatingView.bringToFront()
- // Launcher animation: animate the floating view, expanding to fill the display surface
- progressUpdater.addUpdateListener(
- object : MultiValueUpdateListener() {
- var mDx =
- FloatProp(
- floatingView.startingPosition.left,
- dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
- Interpolators.clampToProgress(
- timings.getStagedRectXInterpolator(),
- timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
- )
- var mDy =
- FloatProp(
- floatingView.startingPosition.top,
- dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
- Interpolators.clampToProgress(
- Interpolators.EMPHASIZED,
- timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
- )
- var mScaleX =
- FloatProp(
- 1f /* start */,
- dp.widthPx / floatingView.startingPosition.width(),
- Interpolators.clampToProgress(
- Interpolators.EMPHASIZED,
- timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
- )
- var mScaleY =
- FloatProp(
- 1f /* start */,
- dp.heightPx / floatingView.startingPosition.height(),
- Interpolators.clampToProgress(
- Interpolators.EMPHASIZED,
- timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
- )
-
- override fun onUpdate(percent: Float, initOnly: Boolean) {
- floatingView.progress = percent
- floatingView.x = mDx.value
- floatingView.y = mDy.value
- floatingView.scaleX = mScaleX.value
- floatingView.scaleY = mScaleY.value
- floatingView.invalidate()
- }
- }
- )
-
- // When animation ends, remove the floating view and run finishCallback
- progressUpdater.addListener(
- object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- safeRemoveViewFromDragLayer(launcher, floatingView)
- finishCallback.run()
- }
- }
- )
-
- launchAnimation.play(progressUpdater)
+ launchAnimation.play(
+ getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
+ rootCandidate))
launchAnimation.start()
}
/**
+ * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate
+ * a single fullscreen icon + background instead of for a pair
+ */
+ @VisibleForTesting
+ fun composeFullscreenIconSplitLaunchAnimator(
+ launchingIconView: AppPairIcon,
+ transitionInfo: TransitionInfo,
+ t: Transaction,
+ finishCallback: Runnable,
+ launchFullscreenIndex: Int
+ ) {
+ // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
+ // use the scale-up animation
+ if (launchingIconView.context is TaskbarActivityContext) {
+ composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
+ WINDOWING_MODE_FULLSCREEN)
+ return
+ }
+
+ // Else we are in Launcher and can launch with the full icon stretch-and-split animation.
+ val launcher = QuickstepLauncher.getLauncher(launchingIconView.context)
+ val dp = launcher.deviceProfile
+
+ // Create an AnimatorSet that will run both shell and launcher transitions together
+ val launchAnimation = AnimatorSet()
+
+ val appInfo = launchingIconView.info
+ .getContents()[launchFullscreenIndex] as WorkspaceItemInfo
+ val intentToLaunch = appInfo.intent.component?.packageName
+ var rootCandidate: Change? = null
+ for (change in transitionInfo.changes) {
+ val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+ val baseIntent = taskInfo.baseIntent.component?.packageName
+ if (TransitionUtil.isOpeningType(change.mode) &&
+ taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN &&
+ baseIntent == intentToLaunch) {
+ rootCandidate = change
+ }
+ }
+
+ // If we could not find a proper root candidate, something went wrong.
+ check(rootCandidate != null) { "Could not find a split root candidate" }
+
+ // Recurse up the tree until parent is null, then we've found our root.
+ var parentToken: WindowContainerToken? = rootCandidate.parent
+ while (parentToken != null) {
+ rootCandidate = transitionInfo.getChange(parentToken) ?: break
+ parentToken = rootCandidate.parent
+ }
+
+ // Make sure nothing weird happened, like getChange() returning null.
+ check(rootCandidate != null) { "Failed to find a root leash" }
+
+ // Create a new floating view in Launcher, positioned above the launching icon
+ val drawableArea = launchingIconView.iconDrawableArea
+ val appIcon = appInfo.newIcon(launchingIconView.context)
+ appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+
+ val floatingView =
+ FloatingAppPairView.getFloatingAppPairView(
+ launcher,
+ drawableArea,
+ appIcon,
+ null /*appIcon2*/,
+ 0 /*dividerPos*/
+ )
+ floatingView.bringToFront()
+ launchAnimation.play(
+ getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
+ rootCandidate))
+ launchAnimation.start()
+ }
+
+ private fun getIconLaunchValueAnimator(t: Transaction,
+ dp: com.android.launcher3.DeviceProfile,
+ finishCallback: Runnable,
+ launcher: QuickstepLauncher,
+ floatingView: FloatingAppPairView,
+ rootCandidate: Change) : ValueAnimator {
+ val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
+ val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
+ progressUpdater.setDuration(timings.getDuration().toLong())
+ progressUpdater.interpolator = Interpolators.LINEAR
+
+ // Shell animation: the apps are revealed toward end of the launch animation
+ progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
+ val progress =
+ Interpolators.clampToProgress(
+ Interpolators.LINEAR,
+ valueAnimator.animatedFraction,
+ timings.appRevealStartOffset,
+ timings.appRevealEndOffset
+ )
+
+ // Set the alpha of the shell layer (2 apps + divider)
+ t.setAlpha(rootCandidate.leash, progress)
+ t.apply()
+ }
+
+ progressUpdater.addUpdateListener(
+ object : MultiValueUpdateListener() {
+ var mDx =
+ FloatProp(
+ floatingView.startingPosition.left,
+ dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
+ Interpolators.clampToProgress(
+ timings.getStagedRectXInterpolator(),
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+ var mDy =
+ FloatProp(
+ floatingView.startingPosition.top,
+ dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
+ Interpolators.clampToProgress(
+ Interpolators.EMPHASIZED,
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+ var mScaleX =
+ FloatProp(
+ 1f /* start */,
+ dp.widthPx / floatingView.startingPosition.width(),
+ Interpolators.clampToProgress(
+ Interpolators.EMPHASIZED,
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+ var mScaleY =
+ FloatProp(
+ 1f /* start */,
+ dp.heightPx / floatingView.startingPosition.height(),
+ Interpolators.clampToProgress(
+ Interpolators.EMPHASIZED,
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+
+ override fun onUpdate(percent: Float, initOnly: Boolean) {
+ floatingView.progress = percent
+ floatingView.x = mDx.value
+ floatingView.y = mDy.value
+ floatingView.scaleX = mScaleX.value
+ floatingView.scaleY = mScaleY.value
+ floatingView.invalidate()
+ }
+ }
+ )
+ progressUpdater.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ safeRemoveViewFromDragLayer(launcher, floatingView)
+ finishCallback.run()
+ }
+ }
+ )
+
+ return progressUpdater
+ }
+
+ /**
* This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when
* there is no visible associated tile to expand from.
+ * [windowingMode] helps determine whether we are looking for a split or a single fullscreen
+ * [Change]
*/
@VisibleForTesting
fun composeScaleUpLaunchAnimation(
transitionInfo: TransitionInfo,
t: Transaction,
- finishCallback: Runnable
+ finishCallback: Runnable,
+ windowingMode: Int
) {
val launchAnimation = AnimatorSet()
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
@@ -833,9 +962,8 @@
// TODO (b/316490565): Replace this logic when SplitBounds is available to
// startAnimation() and we can know the precise taskIds of launching tasks.
- // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
if (
- taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
+ taskInfo.windowingMode == windowingMode &&
(change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
) {
// Found one!
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index c257be6..df1879e 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -90,7 +90,6 @@
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.RecentsAnimationTargets;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SplitSelectionListener;
@@ -646,8 +645,13 @@
}
}
- public void initSplitFromDesktopController(QuickstepLauncher launcher) {
- initSplitFromDesktopController(new SplitFromDesktopController(launcher));
+ /**
+ * Init {@code SplitFromDesktopController}
+ */
+ public void initSplitFromDesktopController(QuickstepLauncher launcher,
+ OverviewComponentObserver overviewComponentObserver) {
+ initSplitFromDesktopController(
+ new SplitFromDesktopController(launcher, overviewComponentObserver));
}
@VisibleForTesting
@@ -956,12 +960,10 @@
private ISplitSelectListener mSplitSelectListener;
private Drawable mAppIcon;
- public SplitFromDesktopController(QuickstepLauncher launcher) {
+ public SplitFromDesktopController(QuickstepLauncher launcher,
+ OverviewComponentObserver overviewComponentObserver) {
mLauncher = launcher;
- RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
- launcher.getApplicationContext());
- mOverviewComponentObserver =
- new OverviewComponentObserver(launcher.getApplicationContext(), deviceState);
+ mOverviewComponentObserver = overviewComponentObserver;
mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
R.dimen.split_placeholder_size);
mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 555bf21..85d4f4b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -62,12 +62,13 @@
private final int mSplitPlaceholderInset;
public SplitWithKeyboardShortcutController(QuickstepLauncher launcher,
- SplitSelectStateController controller) {
+ SplitSelectStateController controller,
+ OverviewComponentObserver overviewComponentObserver,
+ RecentsAnimationDeviceState deviceState) {
mLauncher = launcher;
mController = controller;
- mDeviceState = new RecentsAnimationDeviceState(launcher.getApplicationContext());
- mOverviewComponentObserver = new OverviewComponentObserver(launcher.getApplicationContext(),
- mDeviceState);
+ mDeviceState = deviceState;
+ mOverviewComponentObserver = overviewComponentObserver;
mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
R.dimen.split_placeholder_size);
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index b8afd9d..c3efc3c 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -34,7 +34,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.R;
-import com.android.launcher3.statemanager.StatefulActivity;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.BorderAnimator;
@@ -136,6 +135,10 @@
* 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= */
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index a0ec525..e797819 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -204,18 +204,14 @@
}
private void updateTaskIdContainer() {
- // TODO(b/249371338): TaskView expects the array to have at least 2 elements.
- // At least 2 elements in the array
- mTaskIdContainer = new int[Math.max(mTasks.size(), 2)];
+ mTaskIdContainer = new int[mTasks.size()];
for (int i = 0; i < mTasks.size(); i++) {
mTaskIdContainer[i] = mTasks.get(i).key.id;
}
}
private void updateTaskIdAttributeContainer() {
- // TODO(b/249371338): TaskView expects the array to have at least 2 elements.
- // At least 2 elements in the array
- mTaskIdAttributeContainer = new TaskIdAttributeContainer[Math.max(mTasks.size(), 2)];
+ mTaskIdAttributeContainer = new TaskIdAttributeContainer[mTasks.size()];
for (int i = 0; i < mTasks.size(); i++) {
Task task = mTasks.get(i);
TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.get(task.key.id);
@@ -248,12 +244,6 @@
}
@Override
- public boolean containsTaskId(int taskId) {
- // Thumbnail map contains taskId -> thumbnail map. Use the keys for contains
- return mSnapshotViewMap.contains(taskId);
- }
-
- @Override
public void onTaskListVisibilityChanged(boolean visible, int changes) {
cancelPendingLoadTasks();
if (visible) {
@@ -311,9 +301,13 @@
DesktopRecentsTransitionController recentsController =
recentsView.getDesktopRecentsController();
if (recentsController != null) {
- recentsController.launchDesktopFromRecents(this, success -> {
- endCallback.executeAllAndDestroy();
- });
+ recentsController.launchDesktopFromRecents(this,
+ success -> endCallback.executeAllAndDestroy());
+ Log.d(TAG, "launchTaskAnimated - launchDesktopFromRecents: " + Arrays.toString(
+ getTaskIds()));
+ } else {
+ Log.d(TAG, "launchTaskAnimated - recentsController is null: " + Arrays.toString(
+ getTaskIds()));
}
// Callbacks get run from recentsView for case when recents animation already running
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index 0d49309..e024995 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -36,17 +36,18 @@
* animation. Consists of a rectangular background that splits into two, and two app icons that
* increase in size during the animation.
*/
-class FloatingAppPairBackground(
- context: Context,
- private val floatingView: FloatingAppPairView, // the view that we will draw this background on
- private val appIcon1: Drawable,
- private val appIcon2: Drawable,
- dividerPos: Int
+open class FloatingAppPairBackground(
+ context: Context,
+ // the view that we will draw this background on
+ protected val floatingView: FloatingAppPairView,
+ private val appIcon1: Drawable,
+ private val appIcon2: Drawable?,
+ dividerPos: Int
) : Drawable() {
companion object {
// Design specs -- app icons start small and expand during the animation
- private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
- private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
+ internal val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
+ internal val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
// Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other
// API for drawing rectangles with 4 different corner radii.
@@ -58,13 +59,13 @@
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
// Animation interpolators
- private val expandXInterpolator: Interpolator
- private val expandYInterpolator: Interpolator
+ protected val expandXInterpolator: Interpolator
+ protected val expandYInterpolator: Interpolator
private val cellSplitInterpolator: Interpolator
- private val iconFadeInterpolator: Interpolator
+ protected val iconFadeInterpolator: Interpolator
// Device-specific measurements
- private val deviceCornerRadius: Float
+ protected val deviceCornerRadius: Float
private val deviceHalfDividerSize: Float
private val desiredSplitRatio: Float
@@ -214,7 +215,7 @@
canvas.save()
canvas.translate(changingIcon2Left, changingIconTop)
canvas.scale(changingIconScaleX, changingIconScaleY)
- appIcon2.alpha = changingIconAlpha
+ appIcon2!!.alpha = changingIconAlpha
appIcon2.draw(canvas)
canvas.restore()
}
@@ -312,7 +313,7 @@
canvas.save()
canvas.translate(changingIconLeft, changingIcon2Top)
canvas.scale(changingIconScaleX, changingIconScaleY)
- appIcon2.alpha = changingIconAlpha
+ appIcon2!!.alpha = changingIconAlpha
appIcon2.draw(canvas)
canvas.restore()
}
@@ -325,7 +326,7 @@
* @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top
* right y, bottom right x, and so on.
*/
- private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
+ protected fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Canvas.drawDoubleRoundRect is supported from Q onward
c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint)
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
index e90aa13..e8d1cc1 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
@@ -40,8 +40,8 @@
fun getFloatingAppPairView(
launcher: StatefulActivity<*>,
originalView: View,
- appIcon1: Drawable,
- appIcon2: Drawable,
+ appIcon1: Drawable?,
+ appIcon2: Drawable?,
dividerPos: Int
): FloatingAppPairView {
val dragLayer: ViewGroup = launcher.getDragLayer()
@@ -64,8 +64,8 @@
fun init(
launcher: StatefulActivity<*>,
originalView: View,
- appIcon1: Drawable,
- appIcon2: Drawable,
+ appIcon1: Drawable?,
+ appIcon2: Drawable?,
dividerPos: Int
) {
val viewBounds = Rect(0, 0, originalView.width, originalView.height)
@@ -92,7 +92,14 @@
layoutParams = lp
// Prepare to draw app pair icon background
- background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+ background = if (appIcon1 == null || appIcon2 == null) {
+ val iconToAnimate = appIcon1 ?: appIcon2
+ checkNotNull(iconToAnimate)
+ FloatingFullscreenAppPairBackground(context, this, iconToAnimate,
+ dividerPos)
+ } else {
+ FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+ }
background.setBounds(0, 0, lp.width, lp.height)
}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt
new file mode 100644
index 0000000..8cd997f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.RectF
+import android.graphics.drawable.Drawable
+
+class FloatingFullscreenAppPairBackground(
+ context: Context,
+ floatingView: FloatingAppPairView,
+ private val iconToLaunch: Drawable,
+ dividerPos: Int) :
+ FloatingAppPairBackground(
+ context,
+ floatingView,
+ iconToLaunch,
+ null /*appIcon2*/,
+ dividerPos
+) {
+
+ /** Animates the background as if launching a fullscreen task. */
+ override fun draw(canvas: Canvas) {
+ val progress = floatingView.progress
+
+ // Since the entire floating app pair surface is scaling up during this animation, we
+ // scale down most of these drawn elements so that they appear the proper size on-screen.
+ val scaleFactorX = floatingView.scaleX
+ val scaleFactorY = floatingView.scaleY
+
+ // Get the bounds where we will draw the background image
+ val width = bounds.width().toFloat()
+ val height = bounds.height().toFloat()
+
+ // Get device-specific measurements
+ val cornerRadiusX = deviceCornerRadius / scaleFactorX
+ val cornerRadiusY = deviceCornerRadius / scaleFactorY
+
+ // Draw background
+ drawCustomRoundedRect(
+ canvas,
+ RectF(0f, 0f, width, height),
+ floatArrayOf(
+ cornerRadiusX,
+ cornerRadiusY,
+ cornerRadiusX,
+ cornerRadiusY,
+ cornerRadiusX,
+ cornerRadiusY,
+ cornerRadiusX,
+ cornerRadiusY,
+ )
+ )
+
+ // Calculate changing measurements for icon.
+ val changingIconSizeX =
+ (STARTING_ICON_SIZE_PX +
+ ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+ expandXInterpolator.getInterpolation(progress))) / scaleFactorX
+ val changingIconSizeY =
+ (STARTING_ICON_SIZE_PX +
+ ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+ expandYInterpolator.getInterpolation(progress))) / scaleFactorY
+
+ val changingIcon1Left = (width / 2f) - (changingIconSizeX / 2f)
+ val changingIconTop = (height / 2f) - (changingIconSizeY / 2f)
+ val changingIconScaleX = changingIconSizeX / iconToLaunch.bounds.width()
+ val changingIconScaleY = changingIconSizeY / iconToLaunch.bounds.height()
+ val changingIconAlpha =
+ (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt()
+
+ // Draw icon
+ canvas.save()
+ canvas.translate(changingIcon1Left, changingIconTop)
+ canvas.scale(changingIconScaleX, changingIconScaleY)
+ iconToLaunch.alpha = changingIconAlpha
+ iconToLaunch.draw(canvas)
+ canvas.restore()
+ }
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index a593712..c7a4203 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -6,6 +6,7 @@
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell;
+import android.app.ActivityTaskManager;
import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
@@ -46,6 +47,7 @@
import java.util.Arrays;
import java.util.HashMap;
+import java.util.Optional;
import java.util.function.Consumer;
/**
@@ -60,7 +62,7 @@
*/
public class GroupedTaskView extends TaskView {
- private static final String TAG = TaskView.class.getSimpleName();
+ private static final String TAG = GroupedTaskView.class.getSimpleName();
@Nullable
private Task mSecondaryTask;
// TODO(b/336612373): Support new TTV for GroupedTaskView
@@ -129,9 +131,11 @@
@Nullable SplitBounds splitBoundsConfig) {
super.bind(primary, orientedState);
mSecondaryTask = secondary;
- mTaskIdContainer[1] = secondary.key.id;
- mTaskIdAttributeContainer[1] = new TaskIdAttributeContainer(secondary, mSnapshotView2,
- mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT);
+ mTaskIdContainer = new int[]{mTaskIdContainer[0], secondary.key.id};
+ mTaskIdAttributeContainer = new TaskIdAttributeContainer[]{
+ mTaskIdAttributeContainer[0],
+ new TaskIdAttributeContainer(secondary, mSnapshotView2,
+ mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT)};
mTaskIdAttributeContainer[0].setStagePosition(
SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT);
mSnapshotView2.bind(secondary);
@@ -154,6 +158,9 @@
public void setUpShowAllInstancesListener() {
// sets up the listener for the left/top task
super.setUpShowAllInstancesListener();
+ if (mTaskIdAttributeContainer.length < 2) {
+ return;
+ }
// right/bottom task's base package name
String taskPackageName = mTaskIdAttributeContainer[1].getTask().key.getPackageName();
@@ -308,16 +315,20 @@
}
@Override
- public boolean containsTaskId(int taskId) {
- return (mTask != null && mTask.key.id == taskId)
- || (mSecondaryTask != null && mSecondaryTask.key.id == taskId);
- }
-
- @Override
public TaskThumbnailViewDeprecated[] getThumbnails() {
return new TaskThumbnailViewDeprecated[]{mTaskThumbnailViewDeprecated, mSnapshotView2};
}
+ /**
+ * Returns taskId that split selection was initiated with,
+ * {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of
+ * split selection
+ */
+ protected int getThisTaskCurrentlyInSplitSelection() {
+ int initialTaskId = getRecentsView().getSplitSelectController().getInitialTaskId();
+ return containsTaskId(initialTaskId) ? initialTaskId : INVALID_TASK_ID;
+ }
+
@Override
protected int getLastSelectedChildTaskIndex() {
SplitSelectStateController splitSelectController =
@@ -382,13 +393,15 @@
} else {
// Currently being split with this taskView, let the non-split selected thumbnail
// take up full thumbnail area
- TaskIdAttributeContainer container =
- mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0];
- container.getThumbnailView().measure(widthMeasureSpec,
- View.MeasureSpec.makeMeasureSpec(
- heightSize -
- mContainer.getDeviceProfile().overviewTaskThumbnailTopMarginPx,
- MeasureSpec.EXACTLY));
+ Optional<TaskIdAttributeContainer> nonSplitContainer = Arrays.stream(
+ mTaskIdAttributeContainer).filter(
+ container -> container.getTask().key.id != initSplitTaskId).findAny();
+ nonSplitContainer.ifPresent(
+ taskIdAttributeContainer -> taskIdAttributeContainer.getThumbnailView().measure(
+ widthMeasureSpec, MeasureSpec.makeMeasureSpec(
+ heightSize - mContainer.getDeviceProfile()
+ .overviewTaskThumbnailTopMarginPx,
+ MeasureSpec.EXACTLY)));
}
if (!enableOverviewIconMenu()) {
updateIconPlacement();
@@ -529,7 +542,7 @@
mDigitalWellBeingToast.setBannerVisibility(visibility);
mSnapshotView2.setVisibility(visibility);
mDigitalWellBeingToast2.setBannerVisibility(visibility);
- } else if (taskId == getTaskIds()[0]) {
+ } else if (mTaskIdContainer.length > 0 && mTaskIdContainer[0] == taskId) {
mTaskThumbnailViewDeprecated.setVisibility(visibility);
mDigitalWellBeingToast.setBannerVisibility(visibility);
} else {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5daafcf..62fa6c8 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -626,7 +626,6 @@
*/
protected int mRunningTaskViewId = -1;
private int mTaskViewIdCount;
- private final int[] INVALID_TASK_IDS = new int[]{-1, -1};
protected boolean mRunningTaskTileHidden;
@Nullable
private Task[] mTmpRunningTasks;
@@ -1413,7 +1412,7 @@
*/
@Nullable
public TaskView getTaskViewByTaskIds(int[] taskIds) {
- if (!hasAnyValidTaskIds(taskIds)) {
+ if (!hasAllValidTaskIds(taskIds)) {
return null;
}
@@ -1432,9 +1431,11 @@
return null;
}
- /** Returns false if {@code taskIds} is null or contains invalid values, true otherwise */
- private boolean hasAnyValidTaskIds(int[] taskIds) {
- return taskIds != null && !Arrays.equals(taskIds, INVALID_TASK_IDS);
+ /** Returns false if {@code taskIds} is null or contains any invalid values, true otherwise */
+ private boolean hasAllValidTaskIds(int[] taskIds) {
+ return taskIds != null
+ && taskIds.length > 0
+ && Arrays.stream(taskIds).noneMatch(taskId -> taskId == INVALID_TASK_ID);
}
public void setOverviewStateEnabled(boolean enabled) {
@@ -1707,6 +1708,12 @@
return;
}
+ if (taskGroups == null) {
+ Log.d(TAG, "applyLoadPlan - taskGroups is null");
+ } else {
+ Log.d(TAG, "applyLoadPlan - taskGroups: " + taskGroups.stream().map(
+ GroupTask::toString).toList());
+ }
mLoadPlanEverApplied = true;
if (taskGroups == null || taskGroups.isEmpty()) {
removeTasksViewsAndClearAllButton();
@@ -1720,10 +1727,12 @@
return;
}
- int[] currentTaskId = INVALID_TASK_IDS;
+ int[] currentTaskIds;
TaskView currentTaskView = getTaskViewAt(mCurrentPage);
if (currentTaskView != null && currentTaskView.getTask() != null) {
- currentTaskId = currentTaskView.getTaskIds();
+ currentTaskIds = currentTaskView.getTaskIds();
+ } else {
+ currentTaskIds = new int[0];
}
// Unload existing visible task data
@@ -1735,8 +1744,8 @@
// 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[] runningTaskId = getTaskIdsForTaskViewId(mRunningTaskViewId);
- int[] focusedTaskId = getTaskIdsForTaskViewId(mFocusedTaskViewId);
+ int[] runningTaskIds = getTaskIdsForTaskViewId(mRunningTaskViewId);
+ 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.
@@ -1819,7 +1828,7 @@
}
// Keep same previous focused task
- TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskId);
+ TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
// If the list changed, maybe the focused task doesn't exist anymore
if (newFocusedTaskView == null && getTaskViewCount() > 0) {
newFocusedTaskView = getTaskViewAt(0);
@@ -1830,10 +1839,10 @@
updateChildTaskOrientations();
TaskView newRunningTaskView = null;
- if (hasAnyValidTaskIds(runningTaskId)) {
+ if (hasAllValidTaskIds(runningTaskIds)) {
// Update mRunningTaskViewId to be the new TaskView that was assigned by binding
// the full list of tasks to taskViews
- newRunningTaskView = getTaskViewByTaskIds(runningTaskId);
+ newRunningTaskView = getTaskViewByTaskIds(runningTaskIds);
if (newRunningTaskView != null) {
setRunningTaskViewId(newRunningTaskView.getTaskViewId());
} else {
@@ -1853,8 +1862,8 @@
if (mNextPage != INVALID_PAGE) {
// Restore mCurrentPage but don't call setCurrentPage() as that clobbers the scroll.
mCurrentPage = previousCurrentPage;
- if (hasAnyValidTaskIds(currentTaskId)) {
- currentTaskView = getTaskViewByTaskIds(currentTaskId);
+ if (hasAllValidTaskIds(currentTaskIds)) {
+ currentTaskView = getTaskViewByTaskIds(currentTaskIds);
if (currentTaskView != null) {
targetPage = indexOfChild(currentTaskView);
}
@@ -1863,7 +1872,7 @@
targetPage = previousFocusedPage;
} else {
// Set the current page to the running task, but not if settling on new task.
- if (hasAnyValidTaskIds(runningTaskId)) {
+ if (hasAllValidTaskIds(runningTaskIds)) {
targetPage = indexOfChild(newRunningTaskView);
} else if (getTaskViewCount() > 0) {
targetPage = indexOfChild(requireTaskViewAt(0));
@@ -1963,7 +1972,8 @@
public void resetTaskVisuals() {
for (int i = getTaskViewCount() - 1; i >= 0; i--) {
TaskView taskView = requireTaskViewAt(i);
- if (mIgnoreResetTaskId != taskView.getTaskIds()[0]) {
+ if (Arrays.stream(taskView.getTaskIds()).noneMatch(
+ taskId -> taskId == mIgnoreResetTaskId)) {
taskView.resetViewTransforms();
taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
taskView.setStableAlpha(mContentAlpha);
@@ -2343,7 +2353,7 @@
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView taskView = requireTaskViewAt(i);
TaskIdAttributeContainer[] containers = taskView.getTaskIdAttributeContainers();
- if (containers[0] == null && containers[1] == null) {
+ if (containers.length == 0) {
continue;
}
int index = indexOfChild(taskView);
@@ -2498,7 +2508,7 @@
// For now 2 distinct task IDs is max for split screen
TaskView runningTaskView = getTaskViewFromTaskViewId(taskViewId);
if (runningTaskView == null) {
- return INVALID_TASK_IDS;
+ return new int[0];
}
return runningTaskView.getTaskIds();
@@ -2587,7 +2597,7 @@
*/
public void onGestureAnimationStart(
Task[] runningTasks, RotationTouchHelper rotationTouchHelper) {
- Log.d(TAG, "onGestureAnimationStart");
+ Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
mActiveGestureRunningTasks = runningTasks;
// This needs to be called before the other states are set since it can create the task view
if (mOrientationState.setGestureActive(true)) {
@@ -2736,22 +2746,19 @@
* Returns true if we should add a stub taskView for the running task id
*/
protected boolean shouldAddStubTaskView(Task[] runningTasks) {
- TaskView taskView = getTaskViewByTaskId(runningTasks[0].key.id);
- if (taskView == null) {
- // No TaskView found, add a stub task.
- return true;
- }
-
- if (runningTasks.length > 1) {
- // Ensure all taskIds matches the TaskView, otherwise add a stub task.
- return Arrays.stream(runningTasks).anyMatch(
- runningTask -> !taskView.containsTaskId(runningTask.key.id));
+ int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray();
+ TaskView matchingTaskView = null;
+ if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) {
+ // TODO(b/249371338): 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) {
+ matchingTaskView = taskview;
+ }
} else {
- // Ensure the TaskView only contains a single taskId, or is a DesktopTask,
- // otherwise add a stub task.
- // TODO(b/249371338): Figure out why DesktopTask only have a single runningTask.
- return taskView.containsMultipleTasks() && !taskView.isDesktopTask();
+ matchingTaskView = getTaskViewByTaskIds(runningTaskIds);
}
+ return matchingTaskView == null;
}
/**
@@ -2761,6 +2768,7 @@
* 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) {
return;
}
@@ -4273,13 +4281,10 @@
alpha = Utilities.boundToRange(alpha, 0, 1);
mContentAlpha = alpha;
- int runningTaskId = getTaskIdsForRunningTaskView()[0];
+ TaskView runningTaskView = getRunningTaskView();
for (int i = getTaskViewCount() - 1; i >= 0; i--) {
TaskView child = requireTaskViewAt(i);
- int[] childTaskIds = child.getTaskIds();
- if (runningTaskId != INVALID_TASK_ID
- && mRunningTaskTileHidden
- && (childTaskIds[0] == runningTaskId || childTaskIds[1] == runningTaskId)) {
+ if (runningTaskView != null && mRunningTaskTileHidden && child == runningTaskView) {
continue;
}
child.setStableAlpha(alpha);
@@ -4753,7 +4758,7 @@
// Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
- && mSplitHiddenTaskView.containsMultipleTasks());
+ && mSplitHiddenTaskView instanceof GroupedTaskView);
mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
@@ -4774,14 +4779,14 @@
mSplitSelectStateController.isAnimateCurrentTaskDismissal();
boolean isInitiatingTaskViewSplitPair =
mSplitSelectStateController.isDismissingFromSplitPair();
- if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair) {
+ if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair
+ && mSplitHiddenTaskView instanceof GroupedTaskView) {
// Splitting from Overview for split pair task
createInitialSplitSelectAnimation(builder);
// Animate pair thumbnail into full thumbnail
- boolean primaryTaskSelected =
- mSplitHiddenTaskView.getTaskIdAttributeContainers()[0].getTask().key.id ==
- mSplitSelectStateController.getInitialTaskId();
+ boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIds()[0]
+ == mSplitSelectStateController.getInitialTaskId();
TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView
.getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0];
TaskThumbnailViewDeprecated thumbnail = taskIdAttributeContainer.getThumbnailView();
@@ -5232,7 +5237,8 @@
mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
mPendingAnimation.addEndListener(isSuccess -> {
if (isSuccess) {
- if (tv.getTaskIds()[1] != -1 && mRemoteTargetHandles != null) {
+ if (tv instanceof GroupedTaskView && hasAllValidTaskIds(tv.getTaskIds())
+ && mRemoteTargetHandles != null) {
// TODO(b/194414938): make this part of the animations instead.
TaskViewUtils.createSplitAuxiliarySurfacesAnimator(
mRemoteTargetHandles[0].getTransformParams().getTargetSet().nonApps,
@@ -5457,8 +5463,9 @@
* Called when a running recents animation has finished or canceled.
*/
public void onRecentsAnimationComplete() {
- Log.d(TAG, "onRecentsAnimationComplete - mRecentsAnimationController: "
- + mRecentsAnimationController);
+ Log.d(TAG, "onRecentsAnimationComplete "
+ + "- mRecentsAnimationController: " + mRecentsAnimationController
+ + ", mSideTaskLaunchCallback: " + mSideTaskLaunchCallback);
// At this point, the recents animation is not running and if the animation was canceled
// by a display rotation then reset this state to show the screenshot
setRunningTaskViewShowScreenshot(true);
@@ -5879,8 +5886,7 @@
}
taskView.setShowScreenshot(true);
- for (TaskIdAttributeContainer container :
- taskView.getTaskIdAttributeContainers()) {
+ for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) {
if (container == null) {
continue;
}
@@ -6012,7 +6018,8 @@
}
public void cleanupRemoteTargets() {
- Log.d(TAG, "cleanupRemoteTargets");
+ Log.d(TAG, "cleanupRemoteTargets - mRemoteTargetHandles: " + Arrays.toString(
+ mRemoteTargetHandles));
mRemoteTargetHandles = null;
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 8fd99de..1e2a259 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -16,7 +16,6 @@
package com.android.quickstep.views;
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.widget.Toast.LENGTH_SHORT;
@@ -48,7 +47,6 @@
import android.animation.ObjectAnimator;
import android.annotation.IdRes;
import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
@@ -127,6 +125,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -150,7 +149,8 @@
*/
@Retention(SOURCE)
@IntDef({FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS})
- public @interface TaskDataChanges {}
+ public @interface TaskDataChanges {
+ }
/**
* Type of task view
@@ -371,12 +371,9 @@
private float mStableAlpha = 1;
private int mTaskViewId = -1;
- /**
- * Index 0 will contain taskID of left/top task, index 1 will contain taskId of bottom/right
- */
- protected int[] mTaskIdContainer = new int[]{-1, -1};
+ protected int[] mTaskIdContainer = new int[0];
protected TaskIdAttributeContainer[] mTaskIdAttributeContainer =
- new TaskIdAttributeContainer[2];
+ new TaskIdAttributeContainer[0];
private boolean mShowScreenshot;
private boolean mBorderEnabled;
@@ -395,9 +392,11 @@
private boolean mIsClickableAsLiveTile = true;
- @Nullable private final BorderAnimator mFocusBorderAnimator;
+ @Nullable
+ private final BorderAnimator mFocusBorderAnimator;
- @Nullable private final BorderAnimator mHoverBorderAnimator;
+ @Nullable
+ private final BorderAnimator mHoverBorderAnimator;
public TaskView(Context context) {
this(context, null);
@@ -583,6 +582,10 @@
* Enable or disable showing border on hover and focus change
*/
public void setBorderEnabled(boolean enabled) {
+ if (mBorderEnabled == enabled) {
+ return;
+ }
+
mBorderEnabled = enabled;
// Set the animation correctly in case it misses the hover/focus event during state
// transition
@@ -674,10 +677,10 @@
public void bind(Task task, RecentsOrientedState orientedState) {
cancelPendingLoadTasks();
mTask = task;
- mTaskIdContainer[0] = mTask.key.id;
- mTaskIdAttributeContainer[0] = new TaskIdAttributeContainer(task,
- mTaskThumbnailViewDeprecated, mIconView,
- STAGE_POSITION_UNDEFINED);
+ mTaskIdContainer = new int[]{mTask.key.id};
+ mTaskIdAttributeContainer = new TaskIdAttributeContainer[]{
+ new TaskIdAttributeContainer(task, mTaskThumbnailViewDeprecated, mIconView,
+ STAGE_POSITION_UNDEFINED)};
if (enableRefactorTaskThumbnail()) {
bindTaskThumbnailView();
} else {
@@ -696,6 +699,9 @@
* Sets up an on-click listener and the visibility for show_windows icon on top of the task.
*/
public void setUpShowAllInstancesListener() {
+ if (mTaskIdAttributeContainer.length == 0) {
+ return;
+ }
String taskPackageName = mTaskIdAttributeContainer[0].mTask.key.getPackageName();
// icon of the top/left task
@@ -751,19 +757,18 @@
* Check if given {@code taskId} is tracked in this view
*/
public boolean containsTaskId(int taskId) {
- return mTask != null && mTask.key.id == taskId;
+ return Arrays.stream(mTaskIdContainer).anyMatch(myTaskId -> myTaskId == taskId);
}
/**
- * @return integer array of two elements to be size consistent with max number of tasks possible
- * index 0 will contain the taskId, index 1 will be -1 indicating a null taskID value
+ * Returns a copy of integer array containing taskIds of all tasks in the TaskView.
*/
public int[] getTaskIds() {
return Arrays.copyOf(mTaskIdContainer, mTaskIdContainer.length);
}
public boolean containsMultipleTasks() {
- return mTaskIdContainer[1] != -1;
+ return mTaskIdContainer.length > 1;
}
/**
@@ -833,25 +838,6 @@
return super.dispatchTouchEvent(ev);
}
- /**
- * @return taskId that split selection was initiated with,
- * {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of
- * split selection
- */
- protected int getThisTaskCurrentlyInSplitSelection() {
- SplitSelectStateController splitSelectController =
- getRecentsView().getSplitSelectController();
- int initSplitTaskId = INVALID_TASK_ID;
- for (TaskIdAttributeContainer container : getTaskIdAttributeContainers()) {
- int taskId = container.getTask().key.id;
- if (taskId == splitSelectController.getInitialTaskId()) {
- initSplitTaskId = taskId;
- break;
- }
- }
- return initSplitTaskId;
- }
-
private void onClick(View view) {
if (getTask() == null) {
Log.d("b/310064698", "onClick - task is null");
@@ -864,7 +850,8 @@
RunnableList callbackList = launchTasks();
Log.d("b/310064698", mTask + " - onClick - callbackList: " + callbackList);
if (callbackList != null) {
- callbackList.add(() -> Log.d("b/310064698", mTask + " - onClick - launchCompleted"));
+ callbackList.add(() -> Log.d("b/310064698", Arrays.toString(
+ getTaskIds()) + " - onClick - launchCompleted"));
}
mContainer.getStatsLogManager().logger().withItemInfo(getItemInfo())
.log(LAUNCHER_TASK_LAUNCH_TAP);
@@ -872,10 +859,13 @@
/**
* @return {@code true} if user is already in split select mode and this tap was to choose the
- * second app. {@code false} otherwise
+ * second app. {@code false} otherwise
*/
protected boolean confirmSecondSplitSelectApp() {
int index = getLastSelectedChildTaskIndex();
+ if (index >= mTaskIdAttributeContainer.length) {
+ return false;
+ }
TaskIdAttributeContainer container = mTaskIdAttributeContainer[index];
if (container != null) {
return getRecentsView().confirmSplitSelect(this, container.getTask(),
@@ -897,6 +887,7 @@
/**
* Starts the task associated with this view and animates the startup.
+ *
* @return CompletionStage to indicate the animation completion or null if the launch failed.
*/
@Nullable
@@ -904,7 +895,7 @@
if (mTask != null) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
- ActivityOptionsWrapper opts = mContainer.getActivityLaunchOptions(this, null);
+ ActivityOptionsWrapper opts = mContainer.getActivityLaunchOptions(this, null);
opts.options.setLaunchDisplayId(
getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
if (ActivityManagerWrapper.getInstance()
@@ -936,7 +927,7 @@
return null;
}
} else {
- Log.d(TAG, "launchTaskAnimated - mTask is null");
+ Log.d(TAG, "launchTaskAnimated - mTask is null" + Arrays.toString(getTaskIds()));
return null;
}
}
@@ -1006,9 +997,12 @@
callback.accept(false);
});
}
+ Log.d(TAG,
+ "launchTask - startActivityFromRecents: " + Arrays.toString(getTaskIds()));
});
} else {
callback.accept(false);
+ Log.d(TAG, "launchTask - mTask is null" + Arrays.toString(getTaskIds()));
}
}
@@ -1092,6 +1086,7 @@
/**
* See {@link TaskDataChanges}
+ *
* @param visible If this task view will be visible to the user in overview or hidden
*/
public void onTaskListVisibilityChanged(boolean visible) {
@@ -1100,6 +1095,7 @@
/**
* See {@link TaskDataChanges}
+ *
* @param visible If this task view will be visible to the user in overview or hidden
*/
public void onTaskListVisibilityChanged(boolean visible, @TaskDataChanges int changes) {
@@ -1188,29 +1184,34 @@
}
protected boolean showTaskMenuWithContainer(TaskViewIcon iconView) {
- TaskIdAttributeContainer menuContainer =
- mTaskIdAttributeContainer[iconView == mIconView ? 0 : 1];
+ Optional<TaskIdAttributeContainer> menuContainer = Arrays.stream(
+ mTaskIdAttributeContainer).filter(
+ container -> container.getIconView() == iconView).findAny();
+ if (menuContainer.isEmpty()) {
+ return false;
+ }
DeviceProfile dp = mContainer.getDeviceProfile();
if (enableOverviewIconMenu() && iconView instanceof IconAppChipView) {
((IconAppChipView) iconView).revealAnim(/* isRevealing= */ true);
- return TaskMenuView.showForTask(menuContainer,
+ return TaskMenuView.showForTask(menuContainer.get(),
() -> ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false));
} else if (dp.isTablet) {
int alignedOptionIndex = 0;
- if (getRecentsView().isOnGridBottomRow(menuContainer.getTaskView()) && dp.isLandscape) {
+ if (getRecentsView().isOnGridBottomRow(menuContainer.get().getTaskView())
+ && dp.isLandscape) {
if (Flags.enableGridOnlyOverview()) {
// With no focused task, there is less available space below the tasks, so align
// the arrow to the third option in the menu.
alignedOptionIndex = 2;
- } else {
+ } else {
// Bottom row of landscape grid aligns arrow to second option to avoid clipping
alignedOptionIndex = 1;
}
}
- return TaskMenuViewWithArrow.Companion.showForTask(menuContainer,
+ return TaskMenuViewWithArrow.Companion.showForTask(menuContainer.get(),
alignedOptionIndex);
} else {
- return TaskMenuView.showForTask(menuContainer);
+ return TaskMenuView.showForTask(menuContainer.get());
}
}
@@ -1664,9 +1665,6 @@
final Context context = getContext();
for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
- if (taskContainer == null) {
- continue;
- }
for (SystemShortcut s : TraceHelper.allowIpcs(
"TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) {
info.addAction(s.createAccessibilityAction(context));
@@ -1702,9 +1700,6 @@
}
for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
- if (taskContainer == null) {
- continue;
- }
for (SystemShortcut s : getEnabledShortcuts(this,
taskContainer)) {
if (s.hasHandlerForAction(action)) {
@@ -1903,13 +1898,13 @@
private int getRootViewDisplayId() {
- Display display = getRootView().getDisplay();
+ Display display = getRootView().getDisplay();
return display != null ? display.getDisplayId() : DEFAULT_DISPLAY;
}
/**
- * Sets visibility for the thumbnail and associated elements (DWB banners and action chips).
- * IconView is unaffected.
+ * Sets visibility for the thumbnail and associated elements (DWB banners and action chips).
+ * IconView is unaffected.
*
* @param taskId is only used when setting visibility to a non-{@link View#VISIBLE} value
*/
@@ -1966,7 +1961,8 @@
}
@Override
- public void close() { }
+ public void close() {
+ }
}
public class TaskIdAttributeContainer {
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
index daed861..4fafde8 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
@@ -54,7 +54,9 @@
super.setup()
userHandle = Process.myUserHandle()
taskbarRunningAppsController =
- DesktopTaskbarRunningAppsController(mockRecentsModel, mockDesktopVisibilityController)
+ DesktopTaskbarRunningAppsController(mockRecentsModel) {
+ mockDesktopVisibilityController
+ }
taskbarRunningAppsController.init(taskbarControllers)
taskbarRunningAppsController.setApps(
ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
index 8eec903..512557b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
@@ -17,9 +17,11 @@
package com.android.quickstep;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -34,7 +36,6 @@
import androidx.test.filters.SmallTest;
-import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.quickstep.util.BorderAnimator;
import com.android.quickstep.views.TaskView;
@@ -74,6 +75,7 @@
@Test
public void notShowBorderOnBorderDisabled() {
+ presetBorderStatus(/* enabled= */ true);
mTaskView.setBorderEnabled(/* enabled= */ false);
MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
mTaskView.onHoverEvent(MotionEvent.obtain(event));
@@ -86,7 +88,7 @@
}
@Test
- public void showBorderOnBorderEnabled() {
+ public void showBorderOnHoverEvent() {
mTaskView.setBorderEnabled(/* enabled= */ true);
MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
mTaskView.onHoverEvent(MotionEvent.obtain(event));
@@ -98,7 +100,18 @@
}
@Test
+ public void showBorderOnBorderEnabled() {
+ presetBorderStatus(/* enabled= */ false);
+ mTaskView.setBorderEnabled(/* enabled= */ true);
+ verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
+ true);
+ verify(mFocusAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
+ true);
+ }
+
+ @Test
public void hideBorderOnBorderDisabled() {
+ presetBorderStatus(/* enabled= */ true);
mTaskView.setBorderEnabled(/* enabled= */ false);
verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ false, /* animated= */
true);
@@ -107,13 +120,35 @@
}
@Test
+ public void notTriggerAnimatorWhenEnableStatusUnchanged() {
+ presetBorderStatus(/* enabled= */ false);
+ // Border is disabled by default, no animator is triggered after it is disabled again
+ mTaskView.setBorderEnabled(/* enabled= */ false);
+ verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */
+ anyBoolean(), /* animated= */ anyBoolean());
+ verify(mFocusAnimator, never()).setBorderVisibility(/* visible= */
+ anyBoolean(), /* animated= */ anyBoolean());
+ }
+
+ private void presetBorderStatus(boolean enabled) {
+ // Make the task view focused and hovered
+ MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
+ mTaskView.onHoverEvent(MotionEvent.obtain(event));
+ mTaskView.requestFocus();
+ mTaskView.setBorderEnabled(/* enabled= */ enabled);
+ // Reset invocation count after presetting status
+ reset(mHoverAnimator);
+ reset(mFocusAnimator);
+ }
+
+ @Test
public void notShowBorderByDefault() {
MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
mTaskView.onHoverEvent(MotionEvent.obtain(event));
- verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */ false, /* animated= */
- true);
+ verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */
+ anyBoolean(), /* animated= */ anyBoolean());
mTaskView.onFocusChanged(true, 0, new Rect());
- verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */ false, /* animated= */
- true);
+ verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */
+ anyBoolean(), /* animated= */ anyBoolean());
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 4ffb6bd..de98703 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -17,6 +17,8 @@
package com.android.quickstep.util
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.ContextThemeWrapper
@@ -39,8 +41,10 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
@@ -273,6 +277,9 @@
doNothing()
.whenever(spySplitAnimationController)
.composeIconSplitLaunchAnimator(any(), any(), any(), any())
+ doReturn(-1)
+ .whenever(spySplitAnimationController)
+ .hasChangesForBothAppPairs(any(), any())
spySplitAnimationController.playSplitLaunchAnimation(
null /* launchingTaskView */,
@@ -294,13 +301,45 @@
}
@Test
- fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarContextCorrectly() {
+ fun playsAppropriateSplitLaunchAnimation_playsIconFullscreenLaunchCorrectly() {
+ val spySplitAnimationController = spy(splitAnimationController)
+ whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
+ doNothing()
+ .whenever(spySplitAnimationController)
+ .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any())
+ doReturn(0)
+ .whenever(spySplitAnimationController)
+ .hasChangesForBothAppPairs(any(), any())
+
+ spySplitAnimationController.playSplitLaunchAnimation(
+ null /* launchingTaskView */,
+ mockAppPairIcon,
+ taskId,
+ taskId2,
+ null /* apps */,
+ null /* wallpapers */,
+ null /* nonApps */,
+ stateManager,
+ depthController,
+ transitionInfo,
+ transaction,
+ {} /* finishCallback */
+ )
+
+ verify(spySplitAnimationController)
+ .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0))
+ }
+
+ @Test
+ fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() {
val spySplitAnimationController = spy(splitAnimationController)
whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
doNothing()
.whenever(spySplitAnimationController)
- .composeScaleUpLaunchAnimation(any(), any(), any())
-
+ .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+ doReturn(-1)
+ .whenever(spySplitAnimationController)
+ .hasChangesForBothAppPairs(any(), any())
spySplitAnimationController.playSplitLaunchAnimation(
null /* launchingTaskView */,
mockAppPairIcon,
@@ -316,7 +355,37 @@
{} /* finishCallback */
)
- verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any())
+ verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
+ eq(WINDOWING_MODE_MULTI_WINDOW))
+ }
+
+ @Test
+ fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarFullscreen() {
+ val spySplitAnimationController = spy(splitAnimationController)
+ whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
+ doNothing()
+ .whenever(spySplitAnimationController)
+ .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+ doReturn(0)
+ .whenever(spySplitAnimationController)
+ .hasChangesForBothAppPairs(any(), any())
+ spySplitAnimationController.playSplitLaunchAnimation(
+ null /* launchingTaskView */,
+ mockAppPairIcon,
+ taskId,
+ taskId2,
+ null /* apps */,
+ null /* wallpapers */,
+ null /* nonApps */,
+ stateManager,
+ depthController,
+ transitionInfo,
+ transaction,
+ {} /* finishCallback */
+ )
+
+ verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
+ eq(WINDOWING_MODE_FULLSCREEN))
}
@Test
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7d1f43f..009d709 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1708,7 +1708,7 @@
AbstractFloatingView.closeAllOpenViews(this);
getStateManager().goToState(ALL_APPS, alreadyOnHome);
if (mAppsView.isSearching()) {
- mAppsView.reset(alreadyOnHome);
+ mAppsView.getSearchUiManager().resetSearch();
}
if (mAppsView.getCurrentPage() != tab) {
mAppsView.switchToTab(tab);
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 8026d4a..2f623e2 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -499,18 +499,15 @@
* Exits search and returns to A-Z apps list. Scroll to the private space header.
*/
public void resetAndScrollToPrivateSpaceHeader() {
- if (mTouchHandler != null) {
- mTouchHandler.endFastScrolling();
- }
-
- // Reset the base recycler view after transitioning home.
- updateHeaderScroll(0);
-
// Animate to A-Z with 0 time to reset the animation with proper state management.
+ // We can't rely on `animateToSearchState` with delay inside `resetSearch` because that will
+ // conflict with following scrolling to bottom, so we need it with 0 time here.
animateToSearchState(false, 0);
MAIN_EXECUTOR.getHandler().post(() -> {
// Reset the search bar after transitioning home.
+ // When `resetSearch` is called after `animateToSearchState` is finished, the inside
+ // `animateToSearchState` with delay is a just no-op and return early.
mSearchUiManager.resetSearch();
// Switch to the main tab
switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN);
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 51bc339..eea1a7d 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -51,7 +51,7 @@
public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>> {
public static final String TAG = "StateManager";
- // b/279059025
+ // b/279059025, b/325463989
private static final boolean DEBUG = true;
private final AnimationState mConfig = new AnimationState();
@@ -240,16 +240,8 @@
private void goToState(
STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
if (DEBUG) {
- String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
- String truncatedTrace =
- Arrays.stream(stackTrace.split("\\n"))
- .limit(5)
- .skip(1) // Removes the line "java.lang.Exception: tracing state
- // transition"
- .filter(traceLine -> !traceLine.contains("StateManager.goToState"))
- .collect(Collectors.joining("\n"));
Log.d(TAG, "goToState - fromState: " + mState + ", toState: " + state
- + ", partial trace:\n" + truncatedTrace);
+ + ", partial trace:\n" + getTrimmedStackTrace("StateManager.goToState"));
}
animated &= areAnimatorsEnabled();
@@ -336,17 +328,9 @@
public AnimatorSet createAtomicAnimation(
STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
if (DEBUG) {
- String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
- String truncatedTrace =
- Arrays.stream(stackTrace.split("\\n"))
- .limit(5)
- .skip(1) // Removes the line "java.lang.Exception: tracing state
- // transition"
- .filter(traceLine -> !traceLine.contains(
- "StateManager.createAtomicAnimation"))
- .collect(Collectors.joining("\n"));
Log.d(TAG, "createAtomicAnimation - fromState: " + fromState + ", toState: " + toState
- + ", partial trace:\n" + truncatedTrace);
+ + ", partial trace:\n" + getTrimmedStackTrace(
+ "StateManager.createAtomicAnimation"));
}
PendingAnimation builder = new PendingAnimation(config.duration);
@@ -481,7 +465,8 @@
*/
public void cancelAnimation() {
if (DEBUG && mConfig.currentAnimation != null) {
- Log.d(TAG, "cancelAnimation - with ongoing animation");
+ Log.d(TAG, "cancelAnimation - with ongoing animation"
+ + ", partial trace:\n" + getTrimmedStackTrace("StateManager.cancelAnimation"));
}
mConfig.reset();
// It could happen that a new animation is set as a result of an endListener on the
@@ -579,6 +564,15 @@
mConfig.playbackController = null;
}
+ private String getTrimmedStackTrace(String callingMethodName) {
+ String stackTrace = Log.getStackTraceString(new Exception());
+ return Arrays.stream(stackTrace.split("\\n"))
+ .skip(2) // Removes the line "java.lang.Exception" and "getTrimmedStackTrace".
+ .filter(traceLine -> !traceLine.contains(callingMethodName))
+ .limit(3)
+ .collect(Collectors.joining("\n"));
+ }
+
private class StartAnimRunnable implements Runnable {
private final AnimatorSet mAnim;