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 {
onDismissed(SwipeDismissLayout layout)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          */
onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate)57         void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate);
58 
onSwipeCancelled(SwipeDismissLayout layout)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 
SwipeDismissLayout(Context context)113     public SwipeDismissLayout(Context context) {
114         super(context);
115         init(context);
116     }
117 
SwipeDismissLayout(Context context, AttributeSet attrs)118     public SwipeDismissLayout(Context context, AttributeSet attrs) {
119         super(context, attrs);
120         init(context);
121     }
122 
SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle)123     public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
124         super(context, attrs, defStyle);
125         init(context);
126     }
127 
init(Context context)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 
setOnDismissedListener(OnDismissedListener listener)139     public void setOnDismissedListener(OnDismissedListener listener) {
140         mDismissedListener = listener;
141     }
142 
setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener)143     public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
144         mProgressListener = listener;
145     }
146 
147     @Override
onAttachedToWindow()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
onDetachedFromWindow()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
onInterceptTouchEvent(MotionEvent ev)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
onTouchEvent(MotionEvent ev)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 
setProgress(float deltaX)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 
dismiss()271     private void dismiss() {
272         if (mDismissedListener != null) {
273             mDismissedListener.onDismissed(this);
274         }
275     }
276 
cancel()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      */
resetMembers()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 
updateSwiping(MotionEvent ev)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