blob: 0d8cb6372c76ce3cf322dfcc885e1b55677d00e0 [file] [log] [blame]
/*
* 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 static com.android.systemui.pip.phone.PipDismissViewController.SHOW_TARGET_DELAY;
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;
import com.android.systemui.pip.phone.PipDismissViewController;
/**
* Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
* dismissing, and flings.
*/
class BubbleTouchHandler implements View.OnTouchListener {
/** Velocity required to dismiss a bubble without dragging it into the dismiss target. */
private static final float DISMISS_MIN_VELOCITY = 4000f;
private final PointF mTouchDown = new PointF();
private final PointF mViewPositionOnTouchDown = new PointF();
private final BubbleStackView mStack;
private BubbleController mController = Dependency.get(BubbleController.class);
private PipDismissViewController mDismissViewController;
private boolean mMovedEnough;
private int mTouchSlopSquared;
private VelocityTracker mVelocityTracker;
private boolean mInDismissTarget;
private Handler mHandler = new Handler();
private Runnable mShowDismissAffordance = new Runnable() {
@Override
public void run() {
mDismissViewController.showDismissTarget();
}
};
/** View that was initially touched, when we received the first ACTION_DOWN event. */
private View mTouchedView;
BubbleTouchHandler(Context context, BubbleStackView stackView) {
final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mTouchSlopSquared = touchSlop * touchSlop;
mDismissViewController = new PipDismissViewController(context);
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) {
mStack.collapseStack();
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);
mDismissViewController.createDismissTarget();
mHandler.postDelayed(mShowDismissAffordance, SHOW_TARGET_DELAY);
mTouchDown.set(rawX, rawY);
if (isStack) {
mViewPositionOnTouchDown.set(mStack.getStackPosition());
mStack.onDragStart();
} else if (isFlyout) {
// TODO(b/129768381): Make the flyout dismissable with a gesture.
} 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) {
// TODO(b/129768381): Make the flyout dismissable with a gesture.
} else {
mStack.onBubbleDragged(mTouchedView, viewX, viewY);
}
}
// TODO - when we're in the target stick to it / animate in some way?
mInDismissTarget = mDismissViewController.updateTarget(
isStack ? mStack.getBubbleAt(0) : mTouchedView);
break;
case MotionEvent.ACTION_CANCEL:
resetForNextGesture();
break;
case MotionEvent.ACTION_UP:
trackMovement(event);
if (mInDismissTarget && isStack) {
mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
} else if (isFlyout) {
// TODO(b/129768381): Expand if tapped, dismiss if swiped away.
if (!mStack.isExpanded()) {
mStack.expandStack();
}
} else if (mMovedEnough) {
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
final float velX = mVelocityTracker.getXVelocity();
final float velY = mVelocityTracker.getYVelocity();
if (isStack) {
mStack.onDragFinish(viewX, viewY, velX, velY);
} else {
final boolean dismissed = mInDismissTarget || velY > DISMISS_MIN_VELOCITY;
mStack.onBubbleDragFinish(
mTouchedView, viewX, viewY, velX, velY, /* dismissed */ dismissed);
if (dismissed) {
mController.removeBubble(((BubbleView) mTouchedView).getKey(),
BubbleController.DISMISS_USER_GESTURE);
}
}
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
mStack.collapseStack();
} else if (isStack) {
if (mStack.isExpanded()) {
mStack.collapseStack();
} else {
mStack.expandStack();
}
} else {
mStack.setExpandedBubble(((BubbleView) mTouchedView).getKey());
}
resetForNextGesture();
break;
}
return true;
}
/** Clears all touch-related state. */
private void resetForNextGesture() {
cleanUpDismissTarget();
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mTouchedView = null;
mMovedEnough = false;
mInDismissTarget = false;
}
/**
* Removes the dismiss target and cancels any pending callbacks to show it.
*/
private void cleanUpDismissTarget() {
mHandler.removeCallbacks(mShowDismissAffordance);
mDismissViewController.destroyDismissTarget();
}
private void trackMovement(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
}