• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 android.support.design.widget;
18 
19 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.support.annotation.IntDef;
22 import android.support.annotation.NonNull;
23 import android.support.annotation.RestrictTo;
24 import android.support.v4.view.ViewCompat;
25 import android.support.v4.widget.ViewDragHelper;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.ViewParent;
30 
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.RetentionPolicy;
33 
34 /**
35  * An interaction behavior plugin for child views of {@link CoordinatorLayout} to provide support
36  * for the 'swipe-to-dismiss' gesture.
37  */
38 public class SwipeDismissBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
39 
40     /**
41      * A view is not currently being dragged or animating as a result of a fling/snap.
42      */
43     public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
44 
45     /**
46      * A view is currently being dragged. The position is currently changing as a result
47      * of user input or simulated user input.
48      */
49     public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
50 
51     /**
52      * A view is currently settling into place as a result of a fling or
53      * predefined non-interactive motion.
54      */
55     public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
56 
57     /** @hide */
58     @RestrictTo(LIBRARY_GROUP)
59     @IntDef({SWIPE_DIRECTION_START_TO_END, SWIPE_DIRECTION_END_TO_START, SWIPE_DIRECTION_ANY})
60     @Retention(RetentionPolicy.SOURCE)
61     private @interface SwipeDirection {}
62 
63     /**
64      * Swipe direction that only allows swiping in the direction of start-to-end. That is
65      * left-to-right in LTR, or right-to-left in RTL.
66      */
67     public static final int SWIPE_DIRECTION_START_TO_END = 0;
68 
69     /**
70      * Swipe direction that only allows swiping in the direction of end-to-start. That is
71      * right-to-left in LTR or left-to-right in RTL.
72      */
73     public static final int SWIPE_DIRECTION_END_TO_START = 1;
74 
75     /**
76      * Swipe direction which allows swiping in either direction.
77      */
78     public static final int SWIPE_DIRECTION_ANY = 2;
79 
80     private static final float DEFAULT_DRAG_DISMISS_THRESHOLD = 0.5f;
81     private static final float DEFAULT_ALPHA_START_DISTANCE = 0f;
82     private static final float DEFAULT_ALPHA_END_DISTANCE = DEFAULT_DRAG_DISMISS_THRESHOLD;
83 
84     ViewDragHelper mViewDragHelper;
85     OnDismissListener mListener;
86     private boolean mInterceptingEvents;
87 
88     private float mSensitivity = 0f;
89     private boolean mSensitivitySet;
90 
91     int mSwipeDirection = SWIPE_DIRECTION_ANY;
92     float mDragDismissThreshold = DEFAULT_DRAG_DISMISS_THRESHOLD;
93     float mAlphaStartSwipeDistance = DEFAULT_ALPHA_START_DISTANCE;
94     float mAlphaEndSwipeDistance = DEFAULT_ALPHA_END_DISTANCE;
95 
96     /**
97      * Callback interface used to notify the application that the view has been dismissed.
98      */
99     public interface OnDismissListener {
100         /**
101          * Called when {@code view} has been dismissed via swiping.
102          */
onDismiss(View view)103         public void onDismiss(View view);
104 
105         /**
106          * Called when the drag state has changed.
107          *
108          * @param state the new state. One of
109          * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
110          */
onDragStateChanged(int state)111         public void onDragStateChanged(int state);
112     }
113 
114     /**
115      * Set the listener to be used when a dismiss event occurs.
116      *
117      * @param listener the listener to use.
118      */
setListener(OnDismissListener listener)119     public void setListener(OnDismissListener listener) {
120         mListener = listener;
121     }
122 
123     /**
124      * Sets the swipe direction for this behavior.
125      *
126      * @param direction one of the {@link #SWIPE_DIRECTION_START_TO_END},
127      *                  {@link #SWIPE_DIRECTION_END_TO_START} or {@link #SWIPE_DIRECTION_ANY}
128      */
setSwipeDirection(@wipeDirection int direction)129     public void setSwipeDirection(@SwipeDirection int direction) {
130         mSwipeDirection = direction;
131     }
132 
133     /**
134      * Set the threshold for telling if a view has been dragged enough to be dismissed.
135      *
136      * @param distance a ratio of a view's width, values are clamped to 0 >= x <= 1f;
137      */
setDragDismissDistance(float distance)138     public void setDragDismissDistance(float distance) {
139         mDragDismissThreshold = clamp(0f, distance, 1f);
140     }
141 
142     /**
143      * The minimum swipe distance before the view's alpha is modified.
144      *
145      * @param fraction the distance as a fraction of the view's width.
146      */
setStartAlphaSwipeDistance(float fraction)147     public void setStartAlphaSwipeDistance(float fraction) {
148         mAlphaStartSwipeDistance = clamp(0f, fraction, 1f);
149     }
150 
151     /**
152      * The maximum swipe distance for the view's alpha is modified.
153      *
154      * @param fraction the distance as a fraction of the view's width.
155      */
setEndAlphaSwipeDistance(float fraction)156     public void setEndAlphaSwipeDistance(float fraction) {
157         mAlphaEndSwipeDistance = clamp(0f, fraction, 1f);
158     }
159 
160     /**
161      * Set the sensitivity used for detecting the start of a swipe. This only takes effect if
162      * no touch handling has occured yet.
163      *
164      * @param sensitivity Multiplier for how sensitive we should be about detecting
165      *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
166      */
setSensitivity(float sensitivity)167     public void setSensitivity(float sensitivity) {
168         mSensitivity = sensitivity;
169         mSensitivitySet = true;
170     }
171 
172     @Override
onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event)173     public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
174         boolean dispatchEventToHelper = mInterceptingEvents;
175 
176         switch (event.getActionMasked()) {
177             case MotionEvent.ACTION_DOWN:
178                 mInterceptingEvents = parent.isPointInChildBounds(child,
179                         (int) event.getX(), (int) event.getY());
180                 dispatchEventToHelper = mInterceptingEvents;
181                 break;
182             case MotionEvent.ACTION_UP:
183             case MotionEvent.ACTION_CANCEL:
184                 // Reset the ignore flag for next time
185                 mInterceptingEvents = false;
186                 break;
187         }
188 
189         if (dispatchEventToHelper) {
190             ensureViewDragHelper(parent);
191             return mViewDragHelper.shouldInterceptTouchEvent(event);
192         }
193         return false;
194     }
195 
196     @Override
onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event)197     public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
198         if (mViewDragHelper != null) {
199             mViewDragHelper.processTouchEvent(event);
200             return true;
201         }
202         return false;
203     }
204 
205     /**
206      * Called when the user's input indicates that they want to swipe the given view.
207      *
208      * @param view View the user is attempting to swipe
209      * @return true if the view can be dismissed via swiping, false otherwise
210      */
canSwipeDismissView(@onNull View view)211     public boolean canSwipeDismissView(@NonNull View view) {
212         return true;
213     }
214 
215     private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
216         private static final int INVALID_POINTER_ID = -1;
217 
218         private int mOriginalCapturedViewLeft;
219         private int mActivePointerId = INVALID_POINTER_ID;
220 
221         @Override
222         public boolean tryCaptureView(View child, int pointerId) {
223             // Only capture if we don't already have an active pointer id
224             return mActivePointerId == INVALID_POINTER_ID && canSwipeDismissView(child);
225         }
226 
227         @Override
228         public void onViewCaptured(View capturedChild, int activePointerId) {
229             mActivePointerId = activePointerId;
230             mOriginalCapturedViewLeft = capturedChild.getLeft();
231 
232             // The view has been captured, and thus a drag is about to start so stop any parents
233             // intercepting
234             final ViewParent parent = capturedChild.getParent();
235             if (parent != null) {
236                 parent.requestDisallowInterceptTouchEvent(true);
237             }
238         }
239 
240         @Override
241         public void onViewDragStateChanged(int state) {
242             if (mListener != null) {
243                 mListener.onDragStateChanged(state);
244             }
245         }
246 
247         @Override
248         public void onViewReleased(View child, float xvel, float yvel) {
249             // Reset the active pointer ID
250             mActivePointerId = INVALID_POINTER_ID;
251 
252             final int childWidth = child.getWidth();
253             int targetLeft;
254             boolean dismiss = false;
255 
256             if (shouldDismiss(child, xvel)) {
257                 targetLeft = child.getLeft() < mOriginalCapturedViewLeft
258                         ? mOriginalCapturedViewLeft - childWidth
259                         : mOriginalCapturedViewLeft + childWidth;
260                 dismiss = true;
261             } else {
262                 // Else, reset back to the original left
263                 targetLeft = mOriginalCapturedViewLeft;
264             }
265 
266             if (mViewDragHelper.settleCapturedViewAt(targetLeft, child.getTop())) {
267                 ViewCompat.postOnAnimation(child, new SettleRunnable(child, dismiss));
268             } else if (dismiss && mListener != null) {
269                 mListener.onDismiss(child);
270             }
271         }
272 
273         private boolean shouldDismiss(View child, float xvel) {
274             if (xvel != 0f) {
275                 final boolean isRtl = ViewCompat.getLayoutDirection(child)
276                         == ViewCompat.LAYOUT_DIRECTION_RTL;
277 
278                 if (mSwipeDirection == SWIPE_DIRECTION_ANY) {
279                     // We don't care about the direction so return true
280                     return true;
281                 } else if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
282                     // We only allow start-to-end swiping, so the fling needs to be in the
283                     // correct direction
284                     return isRtl ? xvel < 0f : xvel > 0f;
285                 } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
286                     // We only allow end-to-start swiping, so the fling needs to be in the
287                     // correct direction
288                     return isRtl ? xvel > 0f : xvel < 0f;
289                 }
290             } else {
291                 final int distance = child.getLeft() - mOriginalCapturedViewLeft;
292                 final int thresholdDistance = Math.round(child.getWidth() * mDragDismissThreshold);
293                 return Math.abs(distance) >= thresholdDistance;
294             }
295 
296             return false;
297         }
298 
299         @Override
300         public int getViewHorizontalDragRange(View child) {
301             return child.getWidth();
302         }
303 
304         @Override
305         public int clampViewPositionHorizontal(View child, int left, int dx) {
306             final boolean isRtl = ViewCompat.getLayoutDirection(child)
307                     == ViewCompat.LAYOUT_DIRECTION_RTL;
308             int min, max;
309 
310             if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
311                 if (isRtl) {
312                     min = mOriginalCapturedViewLeft - child.getWidth();
313                     max = mOriginalCapturedViewLeft;
314                 } else {
315                     min = mOriginalCapturedViewLeft;
316                     max = mOriginalCapturedViewLeft + child.getWidth();
317                 }
318             } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
319                 if (isRtl) {
320                     min = mOriginalCapturedViewLeft;
321                     max = mOriginalCapturedViewLeft + child.getWidth();
322                 } else {
323                     min = mOriginalCapturedViewLeft - child.getWidth();
324                     max = mOriginalCapturedViewLeft;
325                 }
326             } else {
327                 min = mOriginalCapturedViewLeft - child.getWidth();
328                 max = mOriginalCapturedViewLeft + child.getWidth();
329             }
330 
331             return clamp(min, left, max);
332         }
333 
334         @Override
335         public int clampViewPositionVertical(View child, int top, int dy) {
336             return child.getTop();
337         }
338 
339         @Override
340         public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
341             final float startAlphaDistance = mOriginalCapturedViewLeft
342                     + child.getWidth() * mAlphaStartSwipeDistance;
343             final float endAlphaDistance = mOriginalCapturedViewLeft
344                     + child.getWidth() * mAlphaEndSwipeDistance;
345 
346             if (left <= startAlphaDistance) {
347                 child.setAlpha(1f);
348             } else if (left >= endAlphaDistance) {
349                 child.setAlpha(0f);
350             } else {
351                 // We're between the start and end distances
352                 final float distance = fraction(startAlphaDistance, endAlphaDistance, left);
353                 child.setAlpha(clamp(0f, 1f - distance, 1f));
354             }
355         }
356     };
357 
ensureViewDragHelper(ViewGroup parent)358     private void ensureViewDragHelper(ViewGroup parent) {
359         if (mViewDragHelper == null) {
360             mViewDragHelper = mSensitivitySet
361                     ? ViewDragHelper.create(parent, mSensitivity, mDragCallback)
362                     : ViewDragHelper.create(parent, mDragCallback);
363         }
364     }
365 
366     private class SettleRunnable implements Runnable {
367         private final View mView;
368         private final boolean mDismiss;
369 
SettleRunnable(View view, boolean dismiss)370         SettleRunnable(View view, boolean dismiss) {
371             mView = view;
372             mDismiss = dismiss;
373         }
374 
375         @Override
run()376         public void run() {
377             if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
378                 ViewCompat.postOnAnimation(mView, this);
379             } else {
380                 if (mDismiss && mListener != null) {
381                     mListener.onDismiss(mView);
382                 }
383             }
384         }
385     }
386 
clamp(float min, float value, float max)387     static float clamp(float min, float value, float max) {
388         return Math.min(Math.max(min, value), max);
389     }
390 
clamp(int min, int value, int max)391     static int clamp(int min, int value, int max) {
392         return Math.min(Math.max(min, value), max);
393     }
394 
395     /**
396      * Retrieve the current drag state of this behavior. This will return one of
397      * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
398      *
399      * @return The current drag state
400      */
getDragState()401     public int getDragState() {
402         return mViewDragHelper != null ? mViewDragHelper.getViewDragState() : STATE_IDLE;
403     }
404 
405     /**
406      * The fraction that {@code value} is between {@code startValue} and {@code endValue}.
407      */
fraction(float startValue, float endValue, float value)408     static float fraction(float startValue, float endValue, float value) {
409         return (value - startValue) / (endValue - startValue);
410     }
411 }