SwipeDismissLayout.java revision d7a313fb118fb6f5e5eaae6acff253038a85da4b
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.Animator; 20import android.animation.TimeInterpolator; 21import android.animation.ValueAnimator; 22import android.animation.ValueAnimator.AnimatorUpdateListener; 23import android.app.Activity; 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.ContextWrapper; 27import android.content.Intent; 28import android.content.IntentFilter; 29import android.content.res.TypedArray; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.MotionEvent; 33import android.view.VelocityTracker; 34import android.view.View; 35import android.view.ViewConfiguration; 36import android.view.ViewGroup; 37import android.view.animation.DecelerateInterpolator; 38import android.widget.FrameLayout; 39 40/** 41 * Special layout that finishes its activity when swiped away. 42 */ 43public class SwipeDismissLayout extends FrameLayout { 44 private static final String TAG = "SwipeDismissLayout"; 45 46 private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f; 47 48 public interface OnDismissedListener { 49 void onDismissed(SwipeDismissLayout layout); 50 } 51 52 public interface OnSwipeProgressChangedListener { 53 /** 54 * Called when the layout has been swiped and the position of the window should change. 55 * 56 * @param alpha A number in [0, 1] representing what the alpha transparency of the window 57 * should be. 58 * @param translate A number in [0, w], where w is the width of the 59 * layout. This is equivalent to progress * layout.getWidth(). 60 */ 61 void onSwipeProgressChanged(SwipeDismissLayout layout, float alpha, float translate); 62 63 void onSwipeCancelled(SwipeDismissLayout layout); 64 } 65 66 private boolean mIsWindowNativelyTranslucent; 67 68 // Cached ViewConfiguration and system-wide constant values 69 private int mSlop; 70 private int mMinFlingVelocity; 71 72 // Transient properties 73 private int mActiveTouchId; 74 private float mDownX; 75 private float mDownY; 76 private boolean mSwiping; 77 private boolean mDismissed; 78 private boolean mDiscardIntercept; 79 private VelocityTracker mVelocityTracker; 80 private float mTranslationX; 81 private boolean mBlockGesture = false; 82 83 private final DismissAnimator mDismissAnimator = new DismissAnimator(); 84 85 private OnDismissedListener mDismissedListener; 86 private OnSwipeProgressChangedListener mProgressListener; 87 private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { 88 private Runnable mRunnable = new Runnable() { 89 @Override 90 public void run() { 91 if (mDismissed) { 92 dismiss(); 93 } else { 94 cancel(); 95 } 96 resetMembers(); 97 } 98 }; 99 100 @Override 101 public void onReceive(Context context, Intent intent) { 102 post(mRunnable); 103 } 104 }; 105 private IntentFilter mScreenOffFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF); 106 107 private float mLastX; 108 109 private boolean mDismissable = true; 110 111 public SwipeDismissLayout(Context context) { 112 super(context); 113 init(context); 114 } 115 116 public SwipeDismissLayout(Context context, AttributeSet attrs) { 117 super(context, attrs); 118 init(context); 119 } 120 121 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) { 122 super(context, attrs, defStyle); 123 init(context); 124 } 125 126 private void init(Context context) { 127 ViewConfiguration vc = ViewConfiguration.get(context); 128 mSlop = vc.getScaledTouchSlop(); 129 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); 130 TypedArray a = context.getTheme().obtainStyledAttributes( 131 com.android.internal.R.styleable.Theme); 132 mIsWindowNativelyTranslucent = a.getBoolean( 133 com.android.internal.R.styleable.Window_windowIsTranslucent, false); 134 a.recycle(); 135 } 136 137 public void setOnDismissedListener(OnDismissedListener listener) { 138 mDismissedListener = listener; 139 } 140 141 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) { 142 mProgressListener = listener; 143 } 144 145 @Override 146 protected void onAttachedToWindow() { 147 super.onAttachedToWindow(); 148 getContext().registerReceiver(mScreenOffReceiver, mScreenOffFilter); 149 } 150 151 @Override 152 protected void onDetachedFromWindow() { 153 getContext().unregisterReceiver(mScreenOffReceiver); 154 super.onDetachedFromWindow(); 155 } 156 157 @Override 158 public boolean onInterceptTouchEvent(MotionEvent ev) { 159 checkGesture((ev)); 160 if (mBlockGesture) { 161 return true; 162 } 163 if (!mDismissable) { 164 return super.onInterceptTouchEvent(ev); 165 } 166 167 // offset because the view is translated during swipe 168 ev.offsetLocation(mTranslationX, 0); 169 170 switch (ev.getActionMasked()) { 171 case MotionEvent.ACTION_DOWN: 172 resetMembers(); 173 mDownX = ev.getRawX(); 174 mDownY = ev.getRawY(); 175 mActiveTouchId = ev.getPointerId(0); 176 mVelocityTracker = VelocityTracker.obtain(); 177 mVelocityTracker.addMovement(ev); 178 break; 179 180 case MotionEvent.ACTION_POINTER_DOWN: 181 int actionIndex = ev.getActionIndex(); 182 mActiveTouchId = ev.getPointerId(actionIndex); 183 break; 184 case MotionEvent.ACTION_POINTER_UP: 185 actionIndex = ev.getActionIndex(); 186 int pointerId = ev.getPointerId(actionIndex); 187 if (pointerId == mActiveTouchId) { 188 // This was our active pointer going up. Choose a new active pointer. 189 int newActionIndex = actionIndex == 0 ? 1 : 0; 190 mActiveTouchId = ev.getPointerId(newActionIndex); 191 } 192 break; 193 194 case MotionEvent.ACTION_CANCEL: 195 case MotionEvent.ACTION_UP: 196 resetMembers(); 197 break; 198 199 case MotionEvent.ACTION_MOVE: 200 if (mVelocityTracker == null || mDiscardIntercept) { 201 break; 202 } 203 204 int pointerIndex = ev.findPointerIndex(mActiveTouchId); 205 if (pointerIndex == -1) { 206 Log.e(TAG, "Invalid pointer index: ignoring."); 207 mDiscardIntercept = true; 208 break; 209 } 210 float dx = ev.getRawX() - mDownX; 211 float x = ev.getX(pointerIndex); 212 float y = ev.getY(pointerIndex); 213 if (dx != 0 && canScroll(this, false, dx, x, y)) { 214 mDiscardIntercept = true; 215 break; 216 } 217 updateSwiping(ev); 218 break; 219 } 220 221 return !mDiscardIntercept && mSwiping; 222 } 223 224 @Override 225 public boolean onTouchEvent(MotionEvent ev) { 226 checkGesture((ev)); 227 if (mBlockGesture) { 228 return true; 229 } 230 if (mVelocityTracker == null || !mDismissable) { 231 return super.onTouchEvent(ev); 232 } 233 // offset because the view is translated during swipe 234 ev.offsetLocation(mTranslationX, 0); 235 switch (ev.getActionMasked()) { 236 case MotionEvent.ACTION_UP: 237 updateDismiss(ev); 238 if (mDismissed) { 239 mDismissAnimator.animateDismissal(ev.getRawX() - mDownX); 240 } else if (mSwiping) { 241 mDismissAnimator.animateRecovery(ev.getRawX() - mDownX); 242 } 243 resetMembers(); 244 break; 245 246 case MotionEvent.ACTION_CANCEL: 247 cancel(); 248 resetMembers(); 249 break; 250 251 case MotionEvent.ACTION_MOVE: 252 mVelocityTracker.addMovement(ev); 253 mLastX = ev.getRawX(); 254 updateSwiping(ev); 255 if (mSwiping) { 256 setProgress(ev.getRawX() - mDownX); 257 break; 258 } 259 } 260 return true; 261 } 262 263 private void setProgress(float deltaX) { 264 mTranslationX = deltaX; 265 if (mProgressListener != null && deltaX >= 0) { 266 mProgressListener.onSwipeProgressChanged( 267 this, progressToAlpha(deltaX / getWidth()), deltaX); 268 } 269 } 270 271 private void dismiss() { 272 if (mDismissedListener != null) { 273 mDismissedListener.onDismissed(this); 274 } 275 } 276 277 protected void cancel() { 278 if (!mIsWindowNativelyTranslucent) { 279 Activity activity = findActivity(); 280 if (activity != null) { 281 activity.convertFromTranslucent(); 282 } 283 } 284 if (mProgressListener != null) { 285 mProgressListener.onSwipeCancelled(this); 286 } 287 } 288 289 /** 290 * Resets internal members when canceling. 291 */ 292 private void resetMembers() { 293 if (mVelocityTracker != null) { 294 mVelocityTracker.recycle(); 295 } 296 mVelocityTracker = null; 297 mTranslationX = 0; 298 mDownX = 0; 299 mDownY = 0; 300 mSwiping = false; 301 mDismissed = false; 302 mDiscardIntercept = false; 303 } 304 305 private void updateSwiping(MotionEvent ev) { 306 boolean oldSwiping = mSwiping; 307 if (!mSwiping) { 308 float deltaX = ev.getRawX() - mDownX; 309 float deltaY = ev.getRawY() - mDownY; 310 if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) { 311 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < Math.abs(deltaX); 312 } else { 313 mSwiping = false; 314 } 315 } 316 317 if (mSwiping && !oldSwiping) { 318 // Swiping has started 319 if (!mIsWindowNativelyTranslucent) { 320 Activity activity = findActivity(); 321 if (activity != null) { 322 activity.convertToTranslucent(null, null); 323 } 324 } 325 } 326 } 327 328 private void updateDismiss(MotionEvent ev) { 329 float deltaX = ev.getRawX() - mDownX; 330 mVelocityTracker.addMovement(ev); 331 mVelocityTracker.computeCurrentVelocity(1000); 332 if (!mDismissed) { 333 334 if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) && 335 ev.getRawX() >= mLastX) { 336 mDismissed = true; 337 } 338 } 339 // Check if the user tried to undo this. 340 if (mDismissed && mSwiping) { 341 // Check if the user's finger is actually back 342 if (deltaX < (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) || 343 // or user is flinging back left 344 mVelocityTracker.getXVelocity() < -mMinFlingVelocity) { 345 mDismissed = false; 346 } 347 } 348 } 349 350 /** 351 * Tests scrollability within child views of v in the direction of dx. 352 * 353 * @param v View to test for horizontal scrollability 354 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 355 * or just its children (false). 356 * @param dx Delta scrolled in pixels. Only the sign of this is used. 357 * @param x X coordinate of the active touch point 358 * @param y Y coordinate of the active touch point 359 * @return true if child views of v can be scrolled by delta of dx. 360 */ 361 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) { 362 if (v instanceof ViewGroup) { 363 final ViewGroup group = (ViewGroup) v; 364 final int scrollX = v.getScrollX(); 365 final int scrollY = v.getScrollY(); 366 final int count = group.getChildCount(); 367 for (int i = count - 1; i >= 0; i--) { 368 final View child = group.getChildAt(i); 369 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 370 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 371 canScroll(child, true, dx, x + scrollX - child.getLeft(), 372 y + scrollY - child.getTop())) { 373 return true; 374 } 375 } 376 } 377 378 return checkV && v.canScrollHorizontally((int) -dx); 379 } 380 381 public void setDismissable(boolean dismissable) { 382 if (!dismissable && mDismissable) { 383 cancel(); 384 resetMembers(); 385 } 386 387 mDismissable = dismissable; 388 } 389 390 private void checkGesture(MotionEvent ev) { 391 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 392 mBlockGesture = mDismissAnimator.isAnimating(); 393 } 394 } 395 396 private float progressToAlpha(float progress) { 397 return 1 - progress * progress * progress; 398 } 399 400 private Activity findActivity() { 401 Context context = getContext(); 402 while (context instanceof ContextWrapper) { 403 if (context instanceof Activity) { 404 return (Activity) context; 405 } 406 context = ((ContextWrapper) context).getBaseContext(); 407 } 408 return null; 409 } 410 411 private class DismissAnimator implements AnimatorUpdateListener, Animator.AnimatorListener { 412 private final TimeInterpolator DISMISS_INTERPOLATOR = new DecelerateInterpolator(1.5f); 413 private final long DISMISS_DURATION = 250; 414 415 private final ValueAnimator mDismissAnimator = new ValueAnimator(); 416 private boolean mWasCanceled = false; 417 private boolean mDismissOnComplete = false; 418 419 /* package */ DismissAnimator() { 420 mDismissAnimator.addUpdateListener(this); 421 mDismissAnimator.addListener(this); 422 } 423 424 /* package */ void animateDismissal(float currentTranslation) { 425 animate( 426 currentTranslation / getWidth(), 427 1, 428 DISMISS_DURATION, 429 DISMISS_INTERPOLATOR, 430 true /* dismiss */); 431 } 432 433 /* package */ void animateRecovery(float currentTranslation) { 434 animate( 435 currentTranslation / getWidth(), 436 0, 437 DISMISS_DURATION, 438 DISMISS_INTERPOLATOR, 439 false /* don't dismiss */); 440 } 441 442 /* package */ boolean isAnimating() { 443 return mDismissAnimator.isStarted(); 444 } 445 446 private void animate(float from, float to, long duration, TimeInterpolator interpolator, 447 boolean dismissOnComplete) { 448 mDismissAnimator.cancel(); 449 mDismissOnComplete = dismissOnComplete; 450 mDismissAnimator.setFloatValues(from, to); 451 mDismissAnimator.setDuration(duration); 452 mDismissAnimator.setInterpolator(interpolator); 453 mDismissAnimator.start(); 454 } 455 456 @Override 457 public void onAnimationUpdate(ValueAnimator animation) { 458 float value = (Float) animation.getAnimatedValue(); 459 setProgress(value * getWidth()); 460 } 461 462 @Override 463 public void onAnimationStart(Animator animation) { 464 mWasCanceled = false; 465 } 466 467 @Override 468 public void onAnimationCancel(Animator animation) { 469 mWasCanceled = true; 470 } 471 472 @Override 473 public void onAnimationEnd(Animator animation) { 474 if (!mWasCanceled) { 475 if (mDismissOnComplete) { 476 dismiss(); 477 } else { 478 cancel(); 479 } 480 } 481 } 482 483 @Override 484 public void onAnimationRepeat(Animator animation) { 485 } 486 } 487} 488