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