SwipeDismissLayout.java revision 625ec4849118f061a99558ad558b16020435a88d
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 if (context instanceof Activity) { 111 ((Activity) context).convertFromTranslucent(); 112 } 113 } 114 115 public void setOnDismissedListener(OnDismissedListener listener) { 116 mDismissedListener = listener; 117 } 118 119 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) { 120 mProgressListener = listener; 121 } 122 123 @Override 124 public boolean onInterceptTouchEvent(MotionEvent ev) { 125 // offset because the view is translated during swipe 126 ev.offsetLocation(mTranslationX, 0); 127 128 switch (ev.getActionMasked()) { 129 case MotionEvent.ACTION_DOWN: 130 resetMembers(); 131 mDownX = ev.getRawX(); 132 mDownY = ev.getRawY(); 133 mActiveTouchId = ev.getPointerId(0); 134 mVelocityTracker = VelocityTracker.obtain(); 135 mVelocityTracker.addMovement(ev); 136 break; 137 138 case MotionEvent.ACTION_POINTER_DOWN: 139 int actionIndex = ev.getActionIndex(); 140 mActiveTouchId = ev.getPointerId(actionIndex); 141 break; 142 case MotionEvent.ACTION_POINTER_UP: 143 actionIndex = ev.getActionIndex(); 144 int pointerId = ev.getPointerId(actionIndex); 145 if (pointerId == mActiveTouchId) { 146 // This was our active pointer going up. Choose a new active pointer. 147 int newActionIndex = actionIndex == 0 ? 1 : 0; 148 mActiveTouchId = ev.getPointerId(newActionIndex); 149 } 150 break; 151 152 case MotionEvent.ACTION_CANCEL: 153 case MotionEvent.ACTION_UP: 154 resetMembers(); 155 break; 156 157 case MotionEvent.ACTION_MOVE: 158 if (mVelocityTracker == null || mDiscardIntercept) { 159 break; 160 } 161 162 int pointerIndex = ev.findPointerIndex(mActiveTouchId); 163 if (pointerIndex == -1) { 164 Log.e(TAG, "Invalid pointer index: ignoring."); 165 mDiscardIntercept = true; 166 break; 167 } 168 float dx = ev.getRawX() - mDownX; 169 float x = ev.getX(pointerIndex); 170 float y = ev.getY(pointerIndex); 171 if (dx != 0 && canScroll(this, false, dx, x, y)) { 172 mDiscardIntercept = true; 173 break; 174 } 175 updateSwiping(ev); 176 break; 177 } 178 179 return !mDiscardIntercept && mSwiping; 180 } 181 182 @Override 183 public boolean onTouchEvent(MotionEvent ev) { 184 if (mVelocityTracker == null) { 185 return super.onTouchEvent(ev); 186 } 187 switch (ev.getActionMasked()) { 188 case MotionEvent.ACTION_UP: 189 updateDismiss(ev); 190 if (mDismissed) { 191 dismiss(); 192 } else if (mSwiping) { 193 cancel(); 194 } 195 resetMembers(); 196 break; 197 198 case MotionEvent.ACTION_CANCEL: 199 cancel(); 200 resetMembers(); 201 break; 202 203 case MotionEvent.ACTION_MOVE: 204 mVelocityTracker.addMovement(ev); 205 mLastX = ev.getRawX(); 206 updateSwiping(ev); 207 if (mSwiping) { 208 if (getContext() instanceof Activity) { 209 ((Activity) getContext()).convertToTranslucent(null, null); 210 } 211 setProgress(ev.getRawX() - mDownX); 212 break; 213 } 214 } 215 return true; 216 } 217 218 private void setProgress(float deltaX) { 219 mTranslationX = deltaX; 220 if (mProgressListener != null && deltaX >= 0) { 221 mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX); 222 } 223 } 224 225 private void dismiss() { 226 if (mDismissedListener != null) { 227 mDismissedListener.onDismissed(this); 228 } 229 } 230 231 protected void cancel() { 232 if (getContext() instanceof Activity) { 233 ((Activity) getContext()).convertFromTranslucent(); 234 } 235 if (mProgressListener != null) { 236 mProgressListener.onSwipeCancelled(this); 237 } 238 } 239 240 /** 241 * Resets internal members when canceling. 242 */ 243 private void resetMembers() { 244 if (mVelocityTracker != null) { 245 mVelocityTracker.recycle(); 246 } 247 mVelocityTracker = null; 248 mTranslationX = 0; 249 mDownX = 0; 250 mDownY = 0; 251 mSwiping = false; 252 mDismissed = false; 253 mDiscardIntercept = false; 254 } 255 256 private void updateSwiping(MotionEvent ev) { 257 if (!mSwiping) { 258 float deltaX = ev.getRawX() - mDownX; 259 float deltaY = ev.getRawY() - mDownY; 260 if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) { 261 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2; 262 } else { 263 mSwiping = false; 264 } 265 } 266 } 267 268 private void updateDismiss(MotionEvent ev) { 269 float deltaX = ev.getRawX() - mDownX; 270 if (!mDismissed) { 271 mVelocityTracker.addMovement(ev); 272 mVelocityTracker.computeCurrentVelocity(1000); 273 274 if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) && 275 ev.getRawX() >= mLastX) { 276 mDismissed = true; 277 } 278 } 279 // Check if the user tried to undo this. 280 if (mDismissed && mSwiping) { 281 // Check if the user's finger is actually back 282 if (deltaX < (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO)) { 283 mDismissed = false; 284 } 285 } 286 } 287 288 /** 289 * Tests scrollability within child views of v in the direction of dx. 290 * 291 * @param v View to test for horizontal scrollability 292 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 293 * or just its children (false). 294 * @param dx Delta scrolled in pixels. Only the sign of this is used. 295 * @param x X coordinate of the active touch point 296 * @param y Y coordinate of the active touch point 297 * @return true if child views of v can be scrolled by delta of dx. 298 */ 299 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) { 300 if (v instanceof ViewGroup) { 301 final ViewGroup group = (ViewGroup) v; 302 final int scrollX = v.getScrollX(); 303 final int scrollY = v.getScrollY(); 304 final int count = group.getChildCount(); 305 for (int i = count - 1; i >= 0; i--) { 306 final View child = group.getChildAt(i); 307 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 308 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 309 canScroll(child, true, dx, x + scrollX - child.getLeft(), 310 y + scrollY - child.getTop())) { 311 return true; 312 } 313 } 314 } 315 316 return checkV && v.canScrollHorizontally((int) -dx); 317 } 318} 319