Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2014 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.internal.widget;
     18 
     19 import android.animation.TimeInterpolator;
     20 import android.content.Context;
     21 import android.util.AttributeSet;
     22 import android.util.Log;
     23 import android.view.MotionEvent;
     24 import android.view.VelocityTracker;
     25 import android.view.View;
     26 import android.view.ViewConfiguration;
     27 import android.view.ViewGroup;
     28 import android.view.animation.AccelerateInterpolator;
     29 import android.view.animation.DecelerateInterpolator;
     30 import android.widget.FrameLayout;
     31 
     32 /**
     33  * Special layout that finishes its activity when swiped away.
     34  */
     35 public class SwipeDismissLayout extends FrameLayout {
     36     private static final String TAG = "SwipeDismissLayout";
     37 
     38     private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f;
     39 
     40     public interface OnDismissedListener {
     41         void onDismissed(SwipeDismissLayout layout);
     42     }
     43 
     44     public interface OnSwipeProgressChangedListener {
     45         /**
     46          * Called when the layout has been swiped and the position of the window should change.
     47          *
     48          * @param progress A number in [0, 1] representing how far to the
     49          * right the window has been swiped
     50          * @param translate A number in [0, w], where w is the width of the
     51          * layout. This is equivalent to progress * layout.getWidth().
     52          */
     53         void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate);
     54 
     55         void onSwipeCancelled(SwipeDismissLayout layout);
     56     }
     57 
     58     // Cached ViewConfiguration and system-wide constant values
     59     private int mSlop;
     60     private int mMinFlingVelocity;
     61     private int mMaxFlingVelocity;
     62     private long mAnimationTime;
     63     private TimeInterpolator mCancelInterpolator;
     64     private TimeInterpolator mDismissInterpolator;
     65 
     66     // Transient properties
     67     private int mActiveTouchId;
     68     private float mDownX;
     69     private float mDownY;
     70     private boolean mSwiping;
     71     private boolean mDismissed;
     72     private boolean mDiscardIntercept;
     73     private VelocityTracker mVelocityTracker;
     74     private float mTranslationX;
     75 
     76     private OnDismissedListener mDismissedListener;
     77     private OnSwipeProgressChangedListener mProgressListener;
     78 
     79     private float mLastX;
     80 
     81     public SwipeDismissLayout(Context context) {
     82         super(context);
     83         init(context);
     84     }
     85 
     86     public SwipeDismissLayout(Context context, AttributeSet attrs) {
     87         super(context, attrs);
     88         init(context);
     89     }
     90 
     91     public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
     92         super(context, attrs, defStyle);
     93         init(context);
     94     }
     95 
     96     private void init(Context context) {
     97         ViewConfiguration vc = ViewConfiguration.get(getContext());
     98         mSlop = vc.getScaledTouchSlop();
     99         mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
    100         mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
    101         mAnimationTime = getContext().getResources().getInteger(
    102                 android.R.integer.config_shortAnimTime);
    103         mCancelInterpolator = new DecelerateInterpolator(1.5f);
    104         mDismissInterpolator = new AccelerateInterpolator(1.5f);
    105     }
    106 
    107     public void setOnDismissedListener(OnDismissedListener listener) {
    108         mDismissedListener = listener;
    109     }
    110 
    111     public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
    112         mProgressListener = listener;
    113     }
    114 
    115     @Override
    116     public boolean onInterceptTouchEvent(MotionEvent ev) {
    117         // offset because the view is translated during swipe
    118         ev.offsetLocation(mTranslationX, 0);
    119 
    120         switch (ev.getActionMasked()) {
    121             case MotionEvent.ACTION_DOWN:
    122                 resetMembers();
    123                 mDownX = ev.getRawX();
    124                 mDownY = ev.getRawY();
    125                 mActiveTouchId = ev.getPointerId(0);
    126                 mVelocityTracker = VelocityTracker.obtain();
    127                 mVelocityTracker.addMovement(ev);
    128                 break;
    129 
    130             case MotionEvent.ACTION_POINTER_DOWN:
    131                 int actionIndex = ev.getActionIndex();
    132                 mActiveTouchId = ev.getPointerId(actionIndex);
    133                 break;
    134             case MotionEvent.ACTION_POINTER_UP:
    135                 actionIndex = ev.getActionIndex();
    136                 int pointerId = ev.getPointerId(actionIndex);
    137                 if (pointerId == mActiveTouchId) {
    138                     // This was our active pointer going up. Choose a new active pointer.
    139                     int newActionIndex = actionIndex == 0 ? 1 : 0;
    140                     mActiveTouchId = ev.getPointerId(newActionIndex);
    141                 }
    142                 break;
    143 
    144             case MotionEvent.ACTION_CANCEL:
    145             case MotionEvent.ACTION_UP:
    146                 resetMembers();
    147                 break;
    148 
    149             case MotionEvent.ACTION_MOVE:
    150                 if (mVelocityTracker == null || mDiscardIntercept) {
    151                     break;
    152                 }
    153 
    154                 int pointerIndex = ev.findPointerIndex(mActiveTouchId);
    155                 if (pointerIndex == -1) {
    156                     Log.e(TAG, "Invalid pointer index: ignoring.");
    157                     mDiscardIntercept = true;
    158                     break;
    159                 }
    160                 float dx = ev.getRawX() - mDownX;
    161                 float x = ev.getX(pointerIndex);
    162                 float y = ev.getY(pointerIndex);
    163                 if (dx != 0 && canScroll(this, false, dx, x, y)) {
    164                     mDiscardIntercept = true;
    165                     break;
    166                 }
    167                 updateSwiping(ev);
    168                 break;
    169         }
    170 
    171         return !mDiscardIntercept && mSwiping;
    172     }
    173 
    174     @Override
    175     public boolean onTouchEvent(MotionEvent ev) {
    176         if (mVelocityTracker == null) {
    177             return super.onTouchEvent(ev);
    178         }
    179         switch (ev.getActionMasked()) {
    180             case MotionEvent.ACTION_UP:
    181                 updateDismiss(ev);
    182                 if (mDismissed) {
    183                     dismiss();
    184                 } else if (mSwiping) {
    185                     cancel();
    186                 }
    187                 resetMembers();
    188                 break;
    189 
    190             case MotionEvent.ACTION_CANCEL:
    191                 cancel();
    192                 resetMembers();
    193                 break;
    194 
    195             case MotionEvent.ACTION_MOVE:
    196                 mVelocityTracker.addMovement(ev);
    197                 mLastX = ev.getRawX();
    198                 updateSwiping(ev);
    199                 if (mSwiping) {
    200                     setProgress(ev.getRawX() - mDownX);
    201                     break;
    202                 }
    203         }
    204         return true;
    205     }
    206 
    207     private void setProgress(float deltaX) {
    208         mTranslationX = deltaX;
    209         if (mProgressListener != null && deltaX >= 0)  {
    210             mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX);
    211         }
    212     }
    213 
    214     private void dismiss() {
    215         if (mDismissedListener != null) {
    216             mDismissedListener.onDismissed(this);
    217         }
    218     }
    219 
    220     protected void cancel() {
    221         if (mProgressListener != null) {
    222             mProgressListener.onSwipeCancelled(this);
    223         }
    224     }
    225 
    226     /**
    227      * Resets internal members when canceling.
    228      */
    229     private void resetMembers() {
    230         if (mVelocityTracker != null) {
    231             mVelocityTracker.recycle();
    232         }
    233         mVelocityTracker = null;
    234         mTranslationX = 0;
    235         mDownX = 0;
    236         mDownY = 0;
    237         mSwiping = false;
    238         mDismissed = false;
    239         mDiscardIntercept = false;
    240     }
    241 
    242     private void updateSwiping(MotionEvent ev) {
    243         if (!mSwiping) {
    244             float deltaX = ev.getRawX() - mDownX;
    245             float deltaY = ev.getRawY() - mDownY;
    246             if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) {
    247                 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2;
    248             } else {
    249                 mSwiping = false;
    250             }
    251         }
    252     }
    253 
    254     private void updateDismiss(MotionEvent ev) {
    255         float deltaX = ev.getRawX() - mDownX;
    256         if (!mDismissed) {
    257             mVelocityTracker.addMovement(ev);
    258             mVelocityTracker.computeCurrentVelocity(1000);
    259 
    260             if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
    261                     ev.getRawX() >= mLastX) {
    262                 mDismissed = true;
    263             }
    264         }
    265         // Check if the user tried to undo this.
    266         if (mDismissed && mSwiping) {
    267             // Check if the user's finger is actually back
    268             if (deltaX < (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO)) {
    269                 mDismissed = false;
    270             }
    271         }
    272     }
    273 
    274     /**
    275      * Tests scrollability within child views of v in the direction of dx.
    276      *
    277      * @param v View to test for horizontal scrollability
    278      * @param checkV Whether the view v passed should itself be checked for scrollability (true),
    279      *               or just its children (false).
    280      * @param dx Delta scrolled in pixels. Only the sign of this is used.
    281      * @param x X coordinate of the active touch point
    282      * @param y Y coordinate of the active touch point
    283      * @return true if child views of v can be scrolled by delta of dx.
    284      */
    285     protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) {
    286         if (v instanceof ViewGroup) {
    287             final ViewGroup group = (ViewGroup) v;
    288             final int scrollX = v.getScrollX();
    289             final int scrollY = v.getScrollY();
    290             final int count = group.getChildCount();
    291             for (int i = count - 1; i >= 0; i--) {
    292                 final View child = group.getChildAt(i);
    293                 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
    294                         y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
    295                         canScroll(child, true, dx, x + scrollX - child.getLeft(),
    296                                 y + scrollY - child.getTop())) {
    297                     return true;
    298                 }
    299             }
    300         }
    301 
    302         return checkV && v.canScrollHorizontally((int) -dx);
    303     }
    304 }
    305