2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.internal.widget;
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;
35 * Special layout that finishes its activity when swiped away.
37 public class SwipeDismissLayout extends FrameLayout {
38 private static final String TAG = "SwipeDismissLayout";
40 private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f;
42 public interface OnDismissedListener {
43 void onDismissed(SwipeDismissLayout layout);
46 public interface OnSwipeProgressChangedListener {
48 * Called when the layout has been swiped and the position of the window should change.
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().
55 void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate);
57 void onSwipeCancelled(SwipeDismissLayout layout);
60 // Cached ViewConfiguration and system-wide constant values
62 private int mMinFlingVelocity;
63 private int mMaxFlingVelocity;
64 private long mAnimationTime;
65 private TimeInterpolator mCancelInterpolator;
66 private TimeInterpolator mDismissInterpolator;
68 // Transient properties
69 private int mActiveTouchId;
72 private boolean mSwiping;
73 private boolean mDismissed;
74 private boolean mDiscardIntercept;
75 private VelocityTracker mVelocityTracker;
76 private float mTranslationX;
78 private OnDismissedListener mDismissedListener;
79 private OnSwipeProgressChangedListener mProgressListener;
80 private ViewTreeObserver.OnEnterAnimationCompleteListener mOnEnterAnimationCompleteListener =
81 new ViewTreeObserver.OnEnterAnimationCompleteListener() {
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
88 if (getContext() instanceof Activity) {
89 ((Activity) getContext()).convertFromTranslucent();
96 public SwipeDismissLayout(Context context) {
101 public SwipeDismissLayout(Context context, AttributeSet attrs) {
102 super(context, attrs);
106 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
107 super(context, attrs, defStyle);
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);
122 public void setOnDismissedListener(OnDismissedListener listener) {
123 mDismissedListener = listener;
126 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
127 mProgressListener = listener;
131 protected void onAttachedToWindow() {
132 super.onAttachedToWindow();
133 if (getContext() instanceof Activity) {
134 getViewTreeObserver().addOnEnterAnimationCompleteListener(
135 mOnEnterAnimationCompleteListener);
140 protected void onDetachedFromWindow() {
141 super.onDetachedFromWindow();
142 if (getContext() instanceof Activity) {
143 getViewTreeObserver().removeOnEnterAnimationCompleteListener(
144 mOnEnterAnimationCompleteListener);
149 public boolean onInterceptTouchEvent(MotionEvent ev) {
150 // offset because the view is translated during swipe
151 ev.offsetLocation(mTranslationX, 0);
153 switch (ev.getActionMasked()) {
154 case MotionEvent.ACTION_DOWN:
156 mDownX = ev.getRawX();
157 mDownY = ev.getRawY();
158 mActiveTouchId = ev.getPointerId(0);
159 mVelocityTracker = VelocityTracker.obtain();
160 mVelocityTracker.addMovement(ev);
163 case MotionEvent.ACTION_POINTER_DOWN:
164 int actionIndex = ev.getActionIndex();
165 mActiveTouchId = ev.getPointerId(actionIndex);
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);
177 case MotionEvent.ACTION_CANCEL:
178 case MotionEvent.ACTION_UP:
182 case MotionEvent.ACTION_MOVE:
183 if (mVelocityTracker == null || mDiscardIntercept) {
187 int pointerIndex = ev.findPointerIndex(mActiveTouchId);
188 if (pointerIndex == -1) {
189 Log.e(TAG, "Invalid pointer index: ignoring.");
190 mDiscardIntercept = true;
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;
204 return !mDiscardIntercept && mSwiping;
208 public boolean onTouchEvent(MotionEvent ev) {
209 if (mVelocityTracker == null) {
210 return super.onTouchEvent(ev);
212 switch (ev.getActionMasked()) {
213 case MotionEvent.ACTION_UP:
217 } else if (mSwiping) {
223 case MotionEvent.ACTION_CANCEL:
228 case MotionEvent.ACTION_MOVE:
229 mVelocityTracker.addMovement(ev);
230 mLastX = ev.getRawX();
233 if (getContext() instanceof Activity) {
234 ((Activity) getContext()).convertToTranslucent(null, null);
236 setProgress(ev.getRawX() - mDownX);
243 private void setProgress(float deltaX) {
244 mTranslationX = deltaX;
245 if (mProgressListener != null && deltaX >= 0) {
246 mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX);
250 private void dismiss() {
251 if (mDismissedListener != null) {
252 mDismissedListener.onDismissed(this);
256 protected void cancel() {
257 if (getContext() instanceof Activity) {
258 ((Activity) getContext()).convertFromTranslucent();
260 if (mProgressListener != null) {
261 mProgressListener.onSwipeCancelled(this);
266 * Resets internal members when canceling.
268 private void resetMembers() {
269 if (mVelocityTracker != null) {
270 mVelocityTracker.recycle();
272 mVelocityTracker = null;
278 mDiscardIntercept = false;
281 private void updateSwiping(MotionEvent ev) {
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;
293 private void updateDismiss(MotionEvent ev) {
294 float deltaX = ev.getRawX() - mDownX;
296 mVelocityTracker.addMovement(ev);
297 mVelocityTracker.computeCurrentVelocity(1000);
299 if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
300 ev.getRawX() >= mLastX) {
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)) {
314 * Tests scrollability within child views of v in the direction of dx.
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.
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())) {
341 return checkV && v.canScrollHorizontally((int) -dx);