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