Home | History | Annotate | Download | only in bubbles
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.systemui.bubbles;
     18 
     19 import android.content.Context;
     20 import android.graphics.PointF;
     21 import android.os.Handler;
     22 import android.view.MotionEvent;
     23 import android.view.VelocityTracker;
     24 import android.view.View;
     25 import android.view.ViewConfiguration;
     26 
     27 import com.android.systemui.Dependency;
     28 
     29 /**
     30  * Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
     31  * dismissing, and flings.
     32  */
     33 class BubbleTouchHandler implements View.OnTouchListener {
     34     /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
     35     private static final float STACK_DISMISS_MIN_VELOCITY = 4000f;
     36 
     37     /**
     38      * Velocity required to dismiss an individual bubble without dragging it into the dismiss
     39      * target.
     40      *
     41      * This is higher than the stack dismiss velocity since unlike the stack, a downward fling could
     42      * also be an attempted gesture to return the bubble to the row of expanded bubbles, which would
     43      * usually be below the dragged bubble. By increasing the required velocity, it's less likely
     44      * that the user is trying to drop it back into the row vs. fling it away.
     45      */
     46     private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f;
     47 
     48     private static final String TAG = "BubbleTouchHandler";
     49     /**
     50      * When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung
     51      * towards the center of the screen (where the dismiss target is). This value is the width of
     52      * the target area to be considered 'towards the target'. For example 50% means that the stack
     53      * needs to be flung towards the middle 50%, and the 25% on the left and right sides won't
     54      * count.
     55      */
     56     private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f;
     57 
     58     private final PointF mTouchDown = new PointF();
     59     private final PointF mViewPositionOnTouchDown = new PointF();
     60     private final BubbleStackView mStack;
     61     private final BubbleData mBubbleData;
     62 
     63     private BubbleController mController = Dependency.get(BubbleController.class);
     64 
     65     private boolean mMovedEnough;
     66     private int mTouchSlopSquared;
     67     private VelocityTracker mVelocityTracker;
     68 
     69     private boolean mInDismissTarget;
     70     private Handler mHandler = new Handler();
     71 
     72     /** View that was initially touched, when we received the first ACTION_DOWN event. */
     73     private View mTouchedView;
     74 
     75     BubbleTouchHandler(BubbleStackView stackView,
     76             BubbleData bubbleData, Context context) {
     77         final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
     78         mTouchSlopSquared = touchSlop * touchSlop;
     79         mBubbleData = bubbleData;
     80         mStack = stackView;
     81     }
     82 
     83     @Override
     84     public boolean onTouch(View v, MotionEvent event) {
     85         final int action = event.getActionMasked();
     86 
     87         // If we aren't currently in the process of touching a view, figure out what we're touching.
     88         // It'll be the stack, an individual bubble, or nothing.
     89         if (mTouchedView == null) {
     90             mTouchedView = mStack.getTargetView(event);
     91         }
     92 
     93         // If this is an ACTION_OUTSIDE event, or the stack reported that we aren't touching
     94         // anything, collapse the stack.
     95         if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
     96             mBubbleData.setExpanded(false);
     97             resetForNextGesture();
     98             return false;
     99         }
    100 
    101         final boolean isStack = mStack.equals(mTouchedView);
    102         final boolean isFlyout = mStack.getFlyoutView().equals(mTouchedView);
    103         final float rawX = event.getRawX();
    104         final float rawY = event.getRawY();
    105 
    106         // The coordinates of the touch event, in terms of the touched view's position.
    107         final float viewX = mViewPositionOnTouchDown.x + rawX - mTouchDown.x;
    108         final float viewY = mViewPositionOnTouchDown.y + rawY - mTouchDown.y;
    109         switch (action) {
    110             case MotionEvent.ACTION_DOWN:
    111                 trackMovement(event);
    112 
    113                 mTouchDown.set(rawX, rawY);
    114                 mStack.onGestureStart();
    115 
    116                 if (isStack) {
    117                     mViewPositionOnTouchDown.set(mStack.getStackPosition());
    118                     mStack.onDragStart();
    119                 } else if (isFlyout) {
    120                     mStack.onFlyoutDragStart();
    121                 } else {
    122                     mViewPositionOnTouchDown.set(
    123                             mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
    124                     mStack.onBubbleDragStart(mTouchedView);
    125                 }
    126 
    127                 break;
    128             case MotionEvent.ACTION_MOVE:
    129                 trackMovement(event);
    130                 final float deltaX = rawX - mTouchDown.x;
    131                 final float deltaY = rawY - mTouchDown.y;
    132 
    133                 if ((deltaX * deltaX) + (deltaY * deltaY) > mTouchSlopSquared && !mMovedEnough) {
    134                     mMovedEnough = true;
    135                 }
    136 
    137                 if (mMovedEnough) {
    138                     if (isStack) {
    139                         mStack.onDragged(viewX, viewY);
    140                     } else if (isFlyout) {
    141                         mStack.onFlyoutDragged(deltaX);
    142                     } else {
    143                         mStack.onBubbleDragged(mTouchedView, viewX, viewY);
    144                     }
    145                 }
    146 
    147                 final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
    148                 if (currentlyInDismissTarget != mInDismissTarget) {
    149                     mInDismissTarget = currentlyInDismissTarget;
    150 
    151                     mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
    152                     final float velX = mVelocityTracker.getXVelocity();
    153                     final float velY = mVelocityTracker.getYVelocity();
    154 
    155                     // If the touch event is within the dismiss target, magnet the stack to it.
    156                     if (!isFlyout) {
    157                         mStack.animateMagnetToDismissTarget(
    158                                 mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
    159                     }
    160                 }
    161                 break;
    162 
    163             case MotionEvent.ACTION_CANCEL:
    164                 resetForNextGesture();
    165                 break;
    166 
    167             case MotionEvent.ACTION_UP:
    168                 trackMovement(event);
    169                 mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
    170                 final float velX = mVelocityTracker.getXVelocity();
    171                 final float velY = mVelocityTracker.getYVelocity();
    172 
    173                 final boolean shouldDismiss =
    174                         isStack
    175                                 ? mInDismissTarget
    176                                     || isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY)
    177                                 : mInDismissTarget
    178                                         || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
    179 
    180                 if (isFlyout && mMovedEnough) {
    181                     mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
    182                 } else if (shouldDismiss) {
    183                     final String individualBubbleKey =
    184                             isStack ? null : ((BubbleView) mTouchedView).getKey();
    185                     mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
    186                             () -> {
    187                                 if (isStack) {
    188                                     mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
    189                                 } else {
    190                                     mController.removeBubble(
    191                                             individualBubbleKey,
    192                                             BubbleController.DISMISS_USER_GESTURE);
    193                                 }
    194                             });
    195                 } else if (isFlyout) {
    196                     // TODO(b/129768381): Expand if tapped, dismiss if swiped away.
    197                     if (!mBubbleData.isExpanded() && !mMovedEnough) {
    198                         mBubbleData.setExpanded(true);
    199                     }
    200                 } else if (mMovedEnough) {
    201                     if (isStack) {
    202                         mStack.onDragFinish(viewX, viewY, velX, velY);
    203                     } else {
    204                         mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
    205                     }
    206                 } else if (mTouchedView == mStack.getExpandedBubbleView()) {
    207                     mBubbleData.setExpanded(false);
    208                 } else if (isStack || isFlyout) {
    209                     // Toggle expansion
    210                     mBubbleData.setExpanded(!mBubbleData.isExpanded());
    211                 } else {
    212                     final String key = ((BubbleView) mTouchedView).getKey();
    213                     mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
    214                 }
    215 
    216                 resetForNextGesture();
    217                 break;
    218         }
    219 
    220         return true;
    221     }
    222 
    223     /**
    224      * Whether the given touch data represents a powerful fling towards the bottom-center of the
    225      * screen (the dismiss target).
    226      */
    227     private boolean isFastFlingTowardsDismissTarget(
    228             float rawX, float rawY, float velX, float velY) {
    229         // Not a fling downward towards the target if velocity is zero or negative.
    230         if (velY <= 0) {
    231             return false;
    232         }
    233 
    234         float bottomOfScreenInterceptX = rawX;
    235 
    236         // Only do math if the X velocity is non-zero, otherwise X won't change.
    237         if (velX != 0) {
    238             // Rise over run...
    239             final float slope = velY / velX;
    240             // ...y = mx + b, b = y / mx...
    241             final float yIntercept = rawY - slope * rawX;
    242             // ...calculate the x value when y = bottom of the screen.
    243             bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope;
    244         }
    245 
    246         final float dismissTargetWidth =
    247                 mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT;
    248         return velY > STACK_DISMISS_MIN_VELOCITY
    249                 && bottomOfScreenInterceptX > dismissTargetWidth / 2f
    250                 && bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f;
    251     }
    252 
    253     /** Clears all touch-related state. */
    254     private void resetForNextGesture() {
    255         if (mVelocityTracker != null) {
    256             mVelocityTracker.recycle();
    257             mVelocityTracker = null;
    258         }
    259 
    260         mTouchedView = null;
    261         mMovedEnough = false;
    262         mInDismissTarget = false;
    263 
    264         mStack.onGestureFinished();
    265     }
    266 
    267     private void trackMovement(MotionEvent event) {
    268         if (mVelocityTracker == null) {
    269             mVelocityTracker = VelocityTracker.obtain();
    270         }
    271         mVelocityTracker.addMovement(event);
    272     }
    273 }
    274