Files
Lawnchair/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
T
Ats Jenk b34f8c8831 Only send bubble bar top coordinate to shell
Stop sending entire bubble bar bounds to shell.
Keeping the bounds in sync was error prone as bubble bar can expand and
collapse. Bubbles can be added/removed.
In each of these cases we had to make sure that shell gets updated.
Shell only needed the full bounds for collapse/expand animation. But
after updating the animation, shell only needs the top coordinate of the
bubble bar.

Bug: 330585402
Flag: com.android.wm.shell.enable_bubble_bar
Test: bubble bar drag
  - drag bar from right to left
  - expand the bar
  - check that expanded view scales in from left edge
  - collapse and drag bar back, check the animation
Test: selected bubble drag
  - drag expanded bubble from right to left
  - check that expanded view scales in from the left edge
  - drag the bubble back to right, check animation
Test: other bubble drag
  - drag a unselected bubble from right to left
  - check that the selected bubble expands in from left edge
  - drag the bubble back to right, check animation again
Test: drag bubble from right to left, observe that expanded view
  expand animation originates from the bubble bar

Change-Id: Ib66cef8d3c04bce54a69e30e99edd408a31f018f
2024-05-17 10:53:33 -07:00

472 lines
19 KiB
Java

/*
* 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;
import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
/**
* Controls bubble bar drag interactions.
* 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 BubbleBarController mBubbleBarController;
private BubbleBarViewController mBubbleBarViewController;
private BubbleDismissController mBubbleDismissController;
private BubbleBarPinController mBubbleBarPinController;
private BubblePinController mBubblePinController;
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) {
mBubbleBarController = bubbleControllers.bubbleBarController;
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
mBubbleDismissController = bubbleControllers.bubbleDismissController;
mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
mBubblePinController = bubbleControllers.bubblePinController;
mBubbleDismissController.setListener(
stuck -> {
mBubbleBarPinController.setDropTargetHidden(stuck);
mBubblePinController.setDropTargetHidden(stuck);
});
}
/**
* 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() {
private BubbleBarLocation mReleasedLocation = BubbleBarLocation.DEFAULT;
private final LocationChangeListener mLocationChangeListener =
new LocationChangeListener() {
@Override
public void onChange(@NonNull BubbleBarLocation location) {
mBubbleBarController.animateBubbleBarLocation(location);
}
@Override
public void onRelease(@NonNull BubbleBarLocation location) {
mReleasedLocation = location;
}
};
@Override
void onDragStart() {
mBubblePinController.setListener(mLocationChangeListener);
mBubbleBarViewController.onBubbleDragStart(bubbleView);
mBubblePinController.onDragStart(
mBubbleBarViewController.getBubbleBarLocation().isOnLeft(
bubbleView.isLayoutRtl()));
}
@Override
protected void onDragUpdate(float x, float y, float newTx, float newTy) {
bubbleView.setDragTranslationX(newTx);
bubbleView.setTranslationY(newTy);
mBubblePinController.onDragUpdate(x, y);
}
@Override
protected void onDragRelease() {
mBubblePinController.onDragEnd();
mBubbleBarViewController.onBubbleDragRelease(mReleasedLocation);
}
@Override
protected void onDragDismiss() {
mBubblePinController.onDragEnd();
}
@Override
void onDragEnd() {
mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
mBubbleBarViewController.onBubbleDragEnd();
mBubblePinController.setListener(null);
}
@Override
protected PointF getRestingPosition() {
return mBubbleBarViewController.getDraggedBubbleReleaseTranslation(
getInitialPosition(), mReleasedLocation);
}
});
}
/**
* 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() {
private BubbleBarLocation mReleasedLocation = BubbleBarLocation.DEFAULT;
private final LocationChangeListener mLocationChangeListener =
location -> mReleasedLocation = location;
@Override
protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
if (bubbleBarView.isExpanded()) return false;
return super.onTouchDown(view, event);
}
@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
// it should be centered in order to align it with the dismiss target view
bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
bubbleBarView.setIsDragging(true);
mBubbleBarPinController.onDragStart(
bubbleBarView.getBubbleBarLocation().isOnLeft(bubbleBarView.isLayoutRtl()));
}
@Override
protected void onDragUpdate(float x, float y, float newTx, float newTy) {
bubbleBarView.setTranslationX(newTx);
bubbleBarView.setTranslationY(newTy);
mBubbleBarPinController.onDragUpdate(x, y);
}
@Override
protected void onDragRelease() {
mBubbleBarPinController.onDragEnd();
}
@Override
protected void onDragDismiss() {
mBubbleBarPinController.onDragEnd();
}
@Override
void onDragEnd() {
// Make sure to update location as the first thing. Pivot update causes a relayout
mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
bubbleBarView.setIsDragging(false);
// Restoring the initial pivot for the bubble bar view
bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
mBubbleBarViewController.onBubbleBarDragEnd();
mBubbleBarPinController.setListener(null);
}
@Override
protected PointF getRestingPosition() {
return mBubbleBarViewController.getBubbleBarDragReleaseTranslation(
getInitialPosition(), mReleasedLocation);
}
});
}
/**
* 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 bubble is dragged to new coordinates.
* Not called while bubble is stuck to the dismiss target.
*/
protected abstract void onDragUpdate(float x, float y, float newTx, float newTy);
/**
* 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() {
}
/**
* 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() {
return mViewInitialPosition;
}
@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) {
float rawX = event.getRawX();
float rawY = event.getRawY();
final float dx = rawX - mTouchDownLocation.x;
final float dy = rawY - 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, rawX, rawY);
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,
float x, float y) {
if (mBubbleDismissController.handleTouchEvent(event)) return;
final float newTx = mViewInitialPosition.x + dx;
final float newTy = mViewInitialPosition.y + dy;
onDragUpdate(x, y, newTx, newTy);
}
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.animateToRestingState(getRestingPosition(), 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());
}
}
}