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