/*
|
* Copyright (C) 2012 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.systemui.bubbles;
|
|
import android.content.Context;
|
import android.graphics.PointF;
|
import android.os.Handler;
|
import android.view.MotionEvent;
|
import android.view.VelocityTracker;
|
import android.view.View;
|
import android.view.ViewConfiguration;
|
|
import com.android.systemui.Dependency;
|
|
/**
|
* Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
|
* dismissing, and flings.
|
*/
|
class BubbleTouchHandler implements View.OnTouchListener {
|
/** Velocity required to dismiss the stack without dragging it into the dismiss target. */
|
private static final float STACK_DISMISS_MIN_VELOCITY = 4000f;
|
|
/**
|
* Velocity required to dismiss an individual bubble without dragging it into the dismiss
|
* target.
|
*
|
* This is higher than the stack dismiss velocity since unlike the stack, a downward fling could
|
* also be an attempted gesture to return the bubble to the row of expanded bubbles, which would
|
* usually be below the dragged bubble. By increasing the required velocity, it's less likely
|
* that the user is trying to drop it back into the row vs. fling it away.
|
*/
|
private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f;
|
|
private static final String TAG = "BubbleTouchHandler";
|
/**
|
* When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung
|
* towards the center of the screen (where the dismiss target is). This value is the width of
|
* the target area to be considered 'towards the target'. For example 50% means that the stack
|
* needs to be flung towards the middle 50%, and the 25% on the left and right sides won't
|
* count.
|
*/
|
private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f;
|
|
private final PointF mTouchDown = new PointF();
|
private final PointF mViewPositionOnTouchDown = new PointF();
|
private final BubbleStackView mStack;
|
private final BubbleData mBubbleData;
|
|
private BubbleController mController = Dependency.get(BubbleController.class);
|
|
private boolean mMovedEnough;
|
private int mTouchSlopSquared;
|
private VelocityTracker mVelocityTracker;
|
|
private boolean mInDismissTarget;
|
private Handler mHandler = new Handler();
|
|
/** View that was initially touched, when we received the first ACTION_DOWN event. */
|
private View mTouchedView;
|
|
BubbleTouchHandler(BubbleStackView stackView,
|
BubbleData bubbleData, Context context) {
|
final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
mTouchSlopSquared = touchSlop * touchSlop;
|
mBubbleData = bubbleData;
|
mStack = stackView;
|
}
|
|
@Override
|
public boolean onTouch(View v, MotionEvent event) {
|
final int action = event.getActionMasked();
|
|
// If we aren't currently in the process of touching a view, figure out what we're touching.
|
// It'll be the stack, an individual bubble, or nothing.
|
if (mTouchedView == null) {
|
mTouchedView = mStack.getTargetView(event);
|
}
|
|
// If this is an ACTION_OUTSIDE event, or the stack reported that we aren't touching
|
// anything, collapse the stack.
|
if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
|
mBubbleData.setExpanded(false);
|
resetForNextGesture();
|
return false;
|
}
|
|
final boolean isStack = mStack.equals(mTouchedView);
|
final boolean isFlyout = mStack.getFlyoutView().equals(mTouchedView);
|
final float rawX = event.getRawX();
|
final float rawY = event.getRawY();
|
|
// The coordinates of the touch event, in terms of the touched view's position.
|
final float viewX = mViewPositionOnTouchDown.x + rawX - mTouchDown.x;
|
final float viewY = mViewPositionOnTouchDown.y + rawY - mTouchDown.y;
|
switch (action) {
|
case MotionEvent.ACTION_DOWN:
|
trackMovement(event);
|
|
mTouchDown.set(rawX, rawY);
|
mStack.onGestureStart();
|
|
if (isStack) {
|
mViewPositionOnTouchDown.set(mStack.getStackPosition());
|
mStack.onDragStart();
|
} else if (isFlyout) {
|
mStack.onFlyoutDragStart();
|
} else {
|
mViewPositionOnTouchDown.set(
|
mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
|
mStack.onBubbleDragStart(mTouchedView);
|
}
|
|
break;
|
case MotionEvent.ACTION_MOVE:
|
trackMovement(event);
|
final float deltaX = rawX - mTouchDown.x;
|
final float deltaY = rawY - mTouchDown.y;
|
|
if ((deltaX * deltaX) + (deltaY * deltaY) > mTouchSlopSquared && !mMovedEnough) {
|
mMovedEnough = true;
|
}
|
|
if (mMovedEnough) {
|
if (isStack) {
|
mStack.onDragged(viewX, viewY);
|
} else if (isFlyout) {
|
mStack.onFlyoutDragged(deltaX);
|
} else {
|
mStack.onBubbleDragged(mTouchedView, viewX, viewY);
|
}
|
}
|
|
final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
|
if (currentlyInDismissTarget != mInDismissTarget) {
|
mInDismissTarget = currentlyInDismissTarget;
|
|
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
|
final float velX = mVelocityTracker.getXVelocity();
|
final float velY = mVelocityTracker.getYVelocity();
|
|
// If the touch event is within the dismiss target, magnet the stack to it.
|
if (!isFlyout) {
|
mStack.animateMagnetToDismissTarget(
|
mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
|
}
|
}
|
break;
|
|
case MotionEvent.ACTION_CANCEL:
|
resetForNextGesture();
|
break;
|
|
case MotionEvent.ACTION_UP:
|
trackMovement(event);
|
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
|
final float velX = mVelocityTracker.getXVelocity();
|
final float velY = mVelocityTracker.getYVelocity();
|
|
final boolean shouldDismiss =
|
isStack
|
? mInDismissTarget
|
|| isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY)
|
: mInDismissTarget
|
|| velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
|
|
if (isFlyout && mMovedEnough) {
|
mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
|
} else if (shouldDismiss) {
|
final String individualBubbleKey =
|
isStack ? null : ((BubbleView) mTouchedView).getKey();
|
mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
|
() -> {
|
if (isStack) {
|
mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
|
} else {
|
mController.removeBubble(
|
individualBubbleKey,
|
BubbleController.DISMISS_USER_GESTURE);
|
}
|
});
|
} else if (isFlyout) {
|
// TODO(b/129768381): Expand if tapped, dismiss if swiped away.
|
if (!mBubbleData.isExpanded() && !mMovedEnough) {
|
mBubbleData.setExpanded(true);
|
}
|
} else if (mMovedEnough) {
|
if (isStack) {
|
mStack.onDragFinish(viewX, viewY, velX, velY);
|
} else {
|
mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
|
}
|
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
|
mBubbleData.setExpanded(false);
|
} else if (isStack || isFlyout) {
|
// Toggle expansion
|
mBubbleData.setExpanded(!mBubbleData.isExpanded());
|
} else {
|
final String key = ((BubbleView) mTouchedView).getKey();
|
mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
|
}
|
|
resetForNextGesture();
|
break;
|
}
|
|
return true;
|
}
|
|
/**
|
* Whether the given touch data represents a powerful fling towards the bottom-center of the
|
* screen (the dismiss target).
|
*/
|
private boolean isFastFlingTowardsDismissTarget(
|
float rawX, float rawY, float velX, float velY) {
|
// Not a fling downward towards the target if velocity is zero or negative.
|
if (velY <= 0) {
|
return false;
|
}
|
|
float bottomOfScreenInterceptX = rawX;
|
|
// Only do math if the X velocity is non-zero, otherwise X won't change.
|
if (velX != 0) {
|
// Rise over run...
|
final float slope = velY / velX;
|
// ...y = mx + b, b = y / mx...
|
final float yIntercept = rawY - slope * rawX;
|
// ...calculate the x value when y = bottom of the screen.
|
bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope;
|
}
|
|
final float dismissTargetWidth =
|
mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT;
|
return velY > STACK_DISMISS_MIN_VELOCITY
|
&& bottomOfScreenInterceptX > dismissTargetWidth / 2f
|
&& bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f;
|
}
|
|
/** Clears all touch-related state. */
|
private void resetForNextGesture() {
|
if (mVelocityTracker != null) {
|
mVelocityTracker.recycle();
|
mVelocityTracker = null;
|
}
|
|
mTouchedView = null;
|
mMovedEnough = false;
|
mInDismissTarget = false;
|
|
mStack.onGestureFinished();
|
}
|
|
private void trackMovement(MotionEvent event) {
|
if (mVelocityTracker == null) {
|
mVelocityTracker = VelocityTracker.obtain();
|
}
|
mVelocityTracker.addMovement(event);
|
}
|
}
|