Merge "Bubble bar dismiss interaction" into udc-qpr-dev

This commit is contained in:
Ivan Tkachenko
2023-07-31 17:54:43 +00:00
committed by Android (Google) Code Review
9 changed files with 898 additions and 7 deletions
@@ -49,6 +49,7 @@
android:visibility="gone"
android:gravity="center"
android:clipChildren="false"
android:elevation="@dimen/bubblebar_elevation"
/>
<FrameLayout
+1
View File
@@ -363,6 +363,7 @@
<dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
<dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
<dimen name="bubblebar_pointer_size">8dp</dimen>
<dimen name="bubblebar_elevation">1dp</dimen>
<dimen name="bubblebar_icon_size">50dp</dimen>
<dimen name="bubblebar_badge_size">24dp</dimen>
@@ -90,6 +90,8 @@ import com.android.launcher3.taskbar.bubbles.BubbleBarController;
import com.android.launcher3.taskbar.bubbles.BubbleBarView;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
import com.android.launcher3.taskbar.bubbles.BubbleDragController;
import com.android.launcher3.taskbar.bubbles.BubbleStashController;
import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
@@ -216,7 +218,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
new BubbleBarController(this, bubbleBarView),
new BubbleBarViewController(this, bubbleBarView),
new BubbleStashController(this),
new BubbleStashedHandleViewController(this, bubbleHandleView)));
new BubbleStashedHandleViewController(this, bubbleHandleView),
new BubbleDragController(this),
new BubbleDismissController(this, mDragLayer)));
}
// Construct controllers.
@@ -95,6 +95,8 @@ public class BubbleBarView extends FrameLayout {
private View.OnClickListener mOnClickListener;
private final Rect mTempRect = new Rect();
private float mRelativePivotX = 1f;
private float mRelativePivotY = 1f;
// An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
// collapsed state and 1 to the fully expanded state.
@@ -109,6 +111,9 @@ public class BubbleBarView extends FrameLayout {
@Nullable
private Consumer<String> mUpdateSelectedBubbleAfterCollapse;
@Nullable
private BubbleView mDraggedBubbleView;
public BubbleBarView(Context context) {
this(context, null);
}
@@ -181,9 +186,10 @@ public class BubbleBarView extends FrameLayout {
mBubbleBarBounds.right = right;
mBubbleBarBounds.bottom = bottom;
// The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
setPivotX(getWidth());
setPivotY(getHeight());
// The bubble bar handle is aligned according to the relative pivot,
// by default it's aligned to the bottom edge of the screen so scale towards that
setPivotX(mRelativePivotX * getWidth());
setPivotY(mRelativePivotY * getHeight());
// Position the views
updateChildrenRenderNodeProperties();
@@ -198,6 +204,32 @@ public class BubbleBarView extends FrameLayout {
return mBubbleBarBounds;
}
/**
* Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
* respectively. If the value is not in range of 0 to 1 it will be normalized.
* @param x relative X pivot value in range 0..1
* @param y relative Y pivot value in range 0..1
*/
public void setRelativePivot(float x, float y) {
mRelativePivotX = Float.max(Float.min(x, 1), 0);
mRelativePivotY = Float.max(Float.min(y, 1), 0);
requestLayout();
}
/**
* Get current relative pivot for X axis
*/
public float getRelativePivotX() {
return mRelativePivotX;
}
/**
* Get current relative pivot for Y axis
*/
public float getRelativePivotY() {
return mRelativePivotY;
}
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -254,9 +286,9 @@ public class BubbleBarView extends FrameLayout {
// where the bubble will end up when the animation ends
final float targetX = currentWidth - expandedWidth + expandedX;
bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
// if we're fully expanded, set the z level to 0
// if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
if (widthState == 1f) {
bv.setZ(0);
bv.setZ(bv == mDraggedBubbleView ? mBubbleElevation : 0);
}
// When we're expanded, we're not stacked so we're not behind the stack
bv.setBehindStack(false, animate);
@@ -331,6 +363,14 @@ public class BubbleBarView extends FrameLayout {
updateArrowForSelected(/* shouldAnimate= */ true);
}
/**
* Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top
*/
public void setDraggedBubble(@Nullable BubbleView view) {
mDraggedBubbleView = view;
requestLayout();
}
/**
* Update the arrow position to match the selected bubble.
*
@@ -24,6 +24,8 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -54,6 +56,7 @@ public class BubbleBarViewController {
// Initialized in init.
private BubbleStashController mBubbleStashController;
private BubbleBarController mBubbleBarController;
private BubbleDragController mBubbleDragController;
private TaskbarStashController mTaskbarStashController;
private TaskbarInsetsController mTaskbarInsetsController;
private View.OnClickListener mBubbleClickListener;
@@ -85,6 +88,7 @@ public class BubbleBarViewController {
public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleBarController = bubbleControllers.bubbleBarController;
mBubbleDragController = bubbleControllers.bubbleDragController;
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
@@ -95,6 +99,7 @@ public class BubbleBarViewController {
mBubbleBarScale.updateValue(1f);
mBubbleClickListener = v -> onBubbleClicked(v);
mBubbleBarClickListener = v -> setExpanded(true);
mBubbleDragController.setupBubbleBarView(mBarView);
mBarView.setOnClickListener(mBubbleBarClickListener);
mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -268,6 +273,7 @@ public class BubbleBarViewController {
if (b != null) {
mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
b.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(b.getView());
} else {
Log.w(TAG, "addBubble, bubble was null!");
}
@@ -319,4 +325,46 @@ public class BubbleBarViewController {
mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
}
}
/**
* Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
* that a bubble is being dragged to dismiss.
* @param bubbleView dragged bubble view
*/
public void onDragStart(@NonNull BubbleView bubbleView) {
if (bubbleView.getBubble() == null) return;
mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ true);
mBarView.setDraggedBubble(bubbleView);
}
/**
* Notifies SystemUI to expand the selected bubble when the bubble is released.
* @param bubbleView dragged bubble view
*/
public void onDragRelease(@NonNull BubbleView bubbleView) {
if (bubbleView.getBubble() == null) return;
mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ false);
}
/**
* Removes the dragged bubble view in the bubble bar view
*/
public void onDragEnd() {
mBarView.setDraggedBubble(null);
}
/**
* Called when bubble was dragged into the dismiss target. Notifies System
* @param bubble dismissed bubble item
*/
public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
mSystemUiProxy.removeBubble(bubble.getKey());
}
/**
* Called when bubble stack was dragged into the dismiss target
*/
public void onDismissAllBubblesWhileDragging() {
mSystemUiProxy.removeAllBubbles();
}
}
@@ -27,6 +27,8 @@ public class BubbleControllers {
public final BubbleBarViewController bubbleBarViewController;
public final BubbleStashController bubbleStashController;
public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
public final BubbleDragController bubbleDragController;
public final BubbleDismissController bubbleDismissController;
private final RunnableList mPostInitRunnables = new RunnableList();
@@ -39,11 +41,15 @@ public class BubbleControllers {
BubbleBarController bubbleBarController,
BubbleBarViewController bubbleBarViewController,
BubbleStashController bubbleStashController,
BubbleStashedHandleViewController bubbleStashedHandleViewController) {
BubbleStashedHandleViewController bubbleStashedHandleViewController,
BubbleDragController bubbleDragController,
BubbleDismissController bubbleDismissController) {
this.bubbleBarController = bubbleBarController;
this.bubbleBarViewController = bubbleBarViewController;
this.bubbleStashController = bubbleStashController;
this.bubbleStashedHandleViewController = bubbleStashedHandleViewController;
this.bubbleDragController = bubbleDragController;
this.bubbleDismissController = bubbleDismissController;
}
/**
@@ -56,6 +62,8 @@ public class BubbleControllers {
bubbleBarViewController.init(taskbarControllers, this);
bubbleStashedHandleViewController.init(taskbarControllers, this);
bubbleStashController.init(taskbarControllers, this);
bubbleDragController.init(/* bubbleControllers = */ this);
bubbleDismissController.init(/* bubbleControllers = */ this);
mPostInitRunnables.executeAllAndDestroy();
}
@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.taskbar.bubbles;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import android.os.SystemProperties;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import com.android.launcher3.R;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarDragLayer;
import com.android.wm.shell.common.bubbles.DismissView;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
/**
* Controls dismiss view presentation for the bubble bar dismiss functionality.
* Provides the dragged view snapping to the target dismiss area and animates it.
* When the dragged bubble/bubble stack is released inside of the target area, it gets dismissed.
*
* @see BubbleDragController
*/
public class BubbleDismissController {
private static final String TAG = BubbleDismissController.class.getSimpleName();
private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
// LINT.IfChange
private static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
// LINT.ThenChange(com/android/wm/shell/bubbles/BubbleStackView.java)
private final TaskbarActivityContext mActivity;
private final TaskbarDragLayer mDragLayer;
@Nullable
private BubbleBarViewController mBubbleBarViewController;
// Dismiss view that's attached to drag layer. It consists of the scrim view and the circular
// dismiss view used as a dismiss target.
@Nullable
private DismissView mDismissView;
// The currently magnetized object, which is being dragged and will be attracted to the magnetic
// dismiss target. This is either the stack itself, or an individual bubble.
@Nullable
private MagnetizedObject<View> mMagnetizedObject;
// The MagneticTarget instance for our circular dismiss view. This is added to the
// MagnetizedObject instances for the stack and any dragged-out bubbles.
@Nullable
private MagnetizedObject.MagneticTarget mMagneticTarget;
// The bubble drag animator that synchronizes bubble drag and dismiss view animations
// A new instance is provided when the dismiss view is setup
@Nullable
private BubbleDragAnimator mAnimator;
public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
mActivity = activity;
mDragLayer = dragLayer;
}
/**
* Initializes dependencies when bubble controllers are created.
* Should be careful to only access things that were created in constructors for now, as some
* controllers may still be waiting for init().
*/
public void init(@NonNull BubbleControllers bubbleControllers) {
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
}
/**
* Setup the dismiss view and magnetized object that will be attracted to magnetic target.
* Should be called before handling events or showing/hiding dismiss view.
*
* @param magnetizedView the view to be pulled into target dismiss area
* @param animator the bubble animator to be used for the magnetized view, it syncs bubble
* dragging and dismiss animations with the dismiss view provided.
*/
public void setupDismissView(@NonNull View magnetizedView,
@NonNull BubbleDragAnimator animator) {
setupDismissView();
setupMagnetizedObject(magnetizedView);
if (mDismissView != null) {
animator.setDismissView(mDismissView);
mAnimator = animator;
}
}
/**
* Handle the touch event and pass it to the magnetized object.
* It should be called after {@code setupDismissView}
*/
public boolean handleTouchEvent(@NonNull MotionEvent event) {
return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
}
/**
* Show dismiss view with animation
* It should be called after {@code setupDismissView}
*/
public void showDismissView() {
if (mDismissView == null) return;
mDismissView.show();
}
/**
* Hide dismiss view with animation
* It should be called after {@code setupDismissView}
*/
public void hideDismissView() {
if (mDismissView == null) return;
mDismissView.hide();
}
/**
* Dismiss magnetized object when it's released in the dismiss target area
*/
private void dismissMagnetizedObject() {
if (mMagnetizedObject == null || mBubbleBarViewController == null) return;
if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
if (bubbleView.getBubble() != null) {
mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
}
} else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
mBubbleBarViewController.onDismissAllBubblesWhileDragging();
}
}
private void setupDismissView() {
if (mDismissView != null) return;
mDismissView = new DismissView(mActivity.getApplicationContext());
BubbleDismissViewUtils.setup(mDismissView);
mDragLayer.addView(mDismissView, /* index = */ 0,
new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mDismissView.setElevation(mDismissView.getResources().getDimensionPixelSize(
R.dimen.bubblebar_elevation));
setupMagneticTarget(mDismissView.getCircle());
}
private void setupMagneticTarget(@NonNull View view) {
int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize(
R.dimen.bubblebar_dismiss_target_size);
mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius);
}
private void setupMagnetizedObject(@NonNull View magnetizedView) {
mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
@Override
public float getWidth(@NonNull View underlyingObject) {
return underlyingObject.getWidth() * underlyingObject.getScaleX();
}
@Override
public float getHeight(@NonNull View underlyingObject) {
return underlyingObject.getHeight() * underlyingObject.getScaleY();
}
@Override
public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
underlyingObject.getLocationOnScreen(loc);
}
};
mMagnetizedObject.setHapticsEnabled(true);
mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
if (mMagneticTarget != null) {
mMagnetizedObject.addTarget(mMagneticTarget);
} else {
Log.e(TAG,"Requires MagneticTarget to add target to MagnetizedObject!");
}
mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() {
@Override
public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
if (mAnimator == null) return;
mAnimator.animateDismissCaptured();
}
@Override
public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
float velX, float velY, boolean wasFlungOut) {
if (mAnimator == null) return;
mAnimator.animateDismissReleased();
}
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
dismissMagnetizedObject();
}
});
}
}
@@ -0,0 +1,222 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.taskbar.bubbles;
import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY;
import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
import android.content.res.Resources;
import android.graphics.PointF;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import com.android.launcher3.R;
import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.common.bubbles.DismissCircleView;
import com.android.wm.shell.common.bubbles.DismissView;
/**
* The animator performs the bubble animations while dragging and coordinates bubble and dismiss
* view animations when it gets magnetized, released or dismissed.
*/
public class BubbleDragAnimator {
private static final float SCALE_BUBBLE_FOCUSED = 1.2f;
private static final float SCALE_BUBBLE_CAPTURED = 0.9f;
private static final float SCALE_BUBBLE_BAR_FOCUSED = 1.1f;
private final PhysicsAnimator.SpringConfig mDefaultConfig =
new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY);
private final PhysicsAnimator.SpringConfig mTranslationConfig =
new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_LOW_BOUNCY);
@NonNull
private final View mView;
@NonNull
private final PhysicsAnimator<View> mBubbleAnimator;
@Nullable
private DismissView mDismissView;
@Nullable
private PhysicsAnimator<DismissCircleView> mDismissAnimator;
private final float mBubbleFocusedScale;
private final float mBubbleCapturedScale;
private final float mDismissCapturedScale;
/**
* Should be initialised for each dragged view
*
* @param view the dragged view to animate
*/
public BubbleDragAnimator(@NonNull View view) {
mView = view;
mBubbleAnimator = PhysicsAnimator.getInstance(view);
mBubbleAnimator.setDefaultSpringConfig(mDefaultConfig);
Resources resources = view.getResources();
final int collapsedSize = resources.getDimensionPixelSize(
R.dimen.bubblebar_dismiss_target_small_size);
final int expandedSize = resources.getDimensionPixelSize(
R.dimen.bubblebar_dismiss_target_size);
mDismissCapturedScale = (float) collapsedSize / expandedSize;
if (view instanceof BubbleBarView) {
mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED;
mBubbleCapturedScale = mDismissCapturedScale;
} else {
mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED;
mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED;
}
}
/**
* Sets dismiss view to be animated alongside the dragged bubble
*/
public void setDismissView(@NonNull DismissView dismissView) {
mDismissView = dismissView;
mDismissAnimator = PhysicsAnimator.getInstance(dismissView.getCircle());
mDismissAnimator.setDefaultSpringConfig(mDefaultConfig);
}
/**
* Animates the focused state of the bubble when the dragging starts
*/
public void animateFocused() {
mBubbleAnimator.cancel();
mBubbleAnimator
.spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
.spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
.start();
}
/**
* Animates the dragged bubble movement back to the initial position.
*
* @param initialPosition 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,
@Nullable Runnable endActions) {
mBubbleAnimator.cancel();
mBubbleAnimator
.spring(DynamicAnimation.SCALE_X, 1f)
.spring(DynamicAnimation.SCALE_Y, 1f)
.spring(DynamicAnimation.TRANSLATION_X, initialPosition.x, velocity.x,
mTranslationConfig)
.spring(DynamicAnimation.TRANSLATION_Y, initialPosition.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);
if (endActions != null) {
endActions.run();
}
}
})
.start();
}
/**
* Animates the dragged view alongside the dismiss view when it gets captured in the dismiss
* target area.
*/
public void animateDismissCaptured() {
mBubbleAnimator.cancel();
mBubbleAnimator
.spring(DynamicAnimation.SCALE_X, mBubbleCapturedScale)
.spring(DynamicAnimation.SCALE_Y, mBubbleCapturedScale)
.spring(DynamicAnimation.ALPHA, mDismissCapturedScale)
.start();
if (mDismissAnimator != null) {
mDismissAnimator.cancel();
mDismissAnimator
.spring(DynamicAnimation.SCALE_X, mDismissCapturedScale)
.spring(DynamicAnimation.SCALE_Y, mDismissCapturedScale)
.start();
}
}
/**
* Animates the dragged view alongside the dismiss view when it gets released from the dismiss
* target area.
*/
public void animateDismissReleased() {
mBubbleAnimator.cancel();
mBubbleAnimator
.spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
.spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
.spring(DynamicAnimation.ALPHA, 1f)
.start();
if (mDismissAnimator != null) {
mDismissAnimator.cancel();
mDismissAnimator
.spring(DynamicAnimation.SCALE_X, 1f)
.spring(DynamicAnimation.SCALE_Y, 1f)
.start();
}
}
/**
* Animates the dragged bubble dismiss when it's released in the dismiss target area.
*
* @param initialPosition the initial position to move the bubble too after animation finishes
* @param endActions gets called when the animation completes or gets cancelled
*/
public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) {
float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f;
float translationY = mView.getTranslationY() + dismissHeight;
mBubbleAnimator
.spring(DynamicAnimation.TRANSLATION_Y, translationY)
.spring(DynamicAnimation.SCALE_X, 0f)
.spring(DynamicAnimation.SCALE_Y, 0f)
.spring(DynamicAnimation.ALPHA, 0f)
.addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
boolean allRelevantPropertyAnimationsEnded) -> {
if (canceled || allRelevantPropertyAnimationsEnded) {
resetAnimatedViews(initialPosition);
if (endActions != null) endActions.run();
}
})
.start();
}
/**
* Reset the animated views to the initial state
*
* @param initialPosition position of the bubble
*/
private void resetAnimatedViews(@NonNull PointF initialPosition) {
mView.setScaleX(1f);
mView.setScaleY(1f);
mView.setAlpha(1f);
mView.setTranslationX(initialPosition.x);
mView.setTranslationY(initialPosition.y);
if (mDismissView != null) {
mDismissView.getCircle().setScaleX(1f);
mDismissView.getCircle().setScaleY(1f);
}
}
}
@@ -0,0 +1,355 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.taskbar.bubbles;
import android.annotation.SuppressLint;
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.taskbar.TaskbarActivityContext;
/**
* Controls bubble bar drag to dismiss interaction.
* Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}.
* Supported interactions:
* - Drag a single bubble view into dismiss target to remove it.
* - Drag the bubble stack into dismiss target to remove all.
* Restores initial position of dragged view if released outside of the dismiss target.
*/
public class BubbleDragController {
private final TaskbarActivityContext mActivity;
private BubbleBarViewController mBubbleBarViewController;
private BubbleDismissController mBubbleDismissController;
public BubbleDragController(TaskbarActivityContext activity) {
mActivity = activity;
}
/**
* Initializes dependencies when bubble controllers are created.
* Should be careful to only access things that were created in constructors for now, as some
* controllers may still be waiting for init().
*/
public void init(@NonNull BubbleControllers bubbleControllers) {
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
mBubbleDismissController = bubbleControllers.bubbleDismissController;
}
/**
* Setup the bubble view for dragging and attach touch listener to it
*/
@SuppressLint("ClickableViewAccessibility")
public void setupBubbleView(@NonNull BubbleView bubbleView) {
if (!(bubbleView.getBubble() instanceof BubbleBarBubble)) {
// Don't setup dragging for overflow bubble view
return;
}
bubbleView.setOnTouchListener(new BubbleTouchListener() {
@Override
void onDragStart() {
mBubbleBarViewController.onDragStart(bubbleView);
}
@Override
void onDragEnd() {
mBubbleBarViewController.onDragEnd();
}
@Override
protected void onDragRelease() {
mBubbleBarViewController.onDragRelease(bubbleView);
}
});
}
/**
* Setup the bubble bar view for dragging and attach touch listener to it
*/
@SuppressLint("ClickableViewAccessibility")
public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) {
PointF initialRelativePivot = new PointF();
bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
@Override
protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
if (bubbleBarView.isExpanded()) return false;
return super.onTouchDown(view, event);
}
@Override
void onDragStart() {
initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
bubbleBarView.getRelativePivotY());
// By default the bubble bar view pivot is in bottom right corner, while dragging
// it should be centered in order to align it with the dismiss target view
bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
}
@Override
void onDragEnd() {
// Restoring the initial pivot for the bubble bar view
bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
}
});
}
/**
* Bubble touch listener for handling a single bubble view or bubble bar view while dragging.
* The dragging starts after "shorter" long click (the long click duration might change):
* - When the touch gesture moves out of the {@code ACTION_DOWN} location the dragging
* interaction is cancelled.
* - When {@code ACTION_UP} happens before long click is registered and there was no significant
* movement the view will perform click.
* - When the listener registers long click it starts dragging interaction, all the subsequent
* {@code ACTION_MOVE} events will drag the view, and the interaction finishes when
* {@code ACTION_UP} or {@code ACTION_CANCEL} are received.
* Lifecycle methods can be overridden do add extra setup/clean up steps.
*/
private abstract class BubbleTouchListener implements View.OnTouchListener {
/**
* The internal state of the touch listener
*/
private enum State {
// Idle and ready for the touch events.
// Changes to:
// - TOUCHED, when the {@code ACTION_DOWN} is handled
IDLE,
// Touch down was handled and the lister is recognising the gestures.
// Changes to:
// - IDLE, when performs the click
// - DRAGGING, when registers the long click and starts dragging interaction
// - CANCELLED, when the touch events move out of the initial location before the long
// click is recognised
TOUCHED,
// The long click was registered and the view is being dragged.
// Changes to:
// - IDLE, when the gesture ends with the {@code ACTION_UP} or {@code ACTION_CANCEL}
DRAGGING,
// The dragging was cancelled.
// Changes to:
// - IDLE, when the current gesture completes
CANCELLED
}
private final PointF mTouchDownLocation = new PointF();
private final PointF mViewInitialPosition = new PointF();
private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
private State mState = State.IDLE;
private int mTouchSlop = -1;
private BubbleDragAnimator mAnimator;
@Nullable
private Runnable mLongClickRunnable;
/**
* Called when the dragging interaction has started
*/
abstract void onDragStart();
/**
* Called when the dragging interaction has ended and all the animations have completed
*/
abstract void onDragEnd();
/**
* Called when the dragged bubble is released outside of the dismiss target area and will
* move back to its initial position
*/
protected void onDragRelease() {
}
/**
* Called when the dragged bubble is released inside of the dismiss target area and will get
* dismissed with animation
*/
protected void onDragDismiss() {
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
updateVelocity(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
return onTouchDown(view, event);
case MotionEvent.ACTION_MOVE:
onTouchMove(view, event);
break;
case MotionEvent.ACTION_UP:
onTouchUp(view, event);
break;
case MotionEvent.ACTION_CANCEL:
onTouchCancel(view, event);
break;
}
return true;
}
/**
* The touch down starts the interaction and schedules the long click handler.
*
* @param view the view that received the event
* @param event the motion event
* @return true if the gesture should be intercepted and handled, false otherwise. Note if
* the false is returned subsequent events in the gesture won't get reported.
*/
protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
mState = State.TOUCHED;
mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
mTouchDownLocation.set(event.getRawX(), event.getRawY());
mViewInitialPosition.set(view.getTranslationX(), view.getTranslationY());
setupLongClickHandler(view);
return true;
}
/**
* The move event drags the view or cancels the interaction if hasn't long clicked yet.
*
* @param view the view that received the event
* @param event the motion event
*/
protected void onTouchMove(@NonNull View view, @NonNull MotionEvent event) {
final float dx = event.getRawX() - mTouchDownLocation.x;
final float dy = event.getRawY() - mTouchDownLocation.y;
switch (mState) {
case TOUCHED:
final boolean movedOut = Math.hypot(dx, dy) > mTouchSlop;
if (movedOut) {
// Moved out of the initial location before the long click was registered
mState = State.CANCELLED;
cleanUpLongClickHandler(view);
}
break;
case DRAGGING:
drag(view, event, dx, dy);
break;
}
}
/**
* On touch up performs click or finishes the dragging depending on the state.
*
* @param view the view that received the event
* @param event the motion event
*/
protected void onTouchUp(@NonNull View view, @NonNull MotionEvent event) {
switch (mState) {
case TOUCHED:
view.performClick();
cleanUp(view);
break;
case DRAGGING:
stopDragging(view, event);
break;
default:
cleanUp(view);
break;
}
}
/**
* The gesture is cancelled and the interaction should clean up and complete.
*
* @param view the view that received the event
* @param event the motion event
*/
protected void onTouchCancel(@NonNull View view, @NonNull MotionEvent event) {
if (mState == State.DRAGGING) {
stopDragging(view, event);
} else {
cleanUp(view);
}
}
private void startDragging(@NonNull View view) {
onDragStart();
mActivity.setTaskbarWindowFullscreen(true);
mAnimator = new BubbleDragAnimator(view);
mAnimator.animateFocused();
mBubbleDismissController.setupDismissView(view, mAnimator);
mBubbleDismissController.showDismissView();
}
private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy) {
if (mBubbleDismissController.handleTouchEvent(event)) return;
view.setTranslationX(mViewInitialPosition.x + dx);
view.setTranslationY(mViewInitialPosition.y + dy);
}
private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
Runnable onComplete = () -> {
mActivity.setTaskbarWindowFullscreen(false);
cleanUp(view);
onDragEnd();
};
if (mBubbleDismissController.handleTouchEvent(event)) {
onDragDismiss();
mAnimator.animateDismiss(mViewInitialPosition, onComplete);
} else {
onDragRelease();
mAnimator.animateToInitialState(mViewInitialPosition, getCurrentVelocity(),
onComplete);
}
mBubbleDismissController.hideDismissView();
}
private void setupLongClickHandler(@NonNull View view) {
cleanUpLongClickHandler(view);
mLongClickRunnable = () -> {
// Register long click and start dragging interaction
mState = State.DRAGGING;
startDragging(view);
};
view.getHandler().postDelayed(mLongClickRunnable, mPressToDragTimeout);
}
private void cleanUpLongClickHandler(@NonNull View view) {
if (mLongClickRunnable == null || view.getHandler() == null) return;
view.getHandler().removeCallbacks(mLongClickRunnable);
mLongClickRunnable = null;
}
private void cleanUp(@NonNull View view) {
cleanUpLongClickHandler(view);
mVelocityTracker.clear();
mState = State.IDLE;
}
private void updateVelocity(MotionEvent event) {
final float deltaX = event.getRawX() - event.getX();
final float deltaY = event.getRawY() - event.getY();
event.offsetLocation(deltaX, deltaY);
mVelocityTracker.addMovement(event);
event.offsetLocation(-deltaX, -deltaY);
}
private PointF getCurrentVelocity() {
mVelocityTracker.computeCurrentVelocity(/* units = */ 1000);
return new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
}
}
}