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