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