1 /* 2 * Copyright (C) 2011 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 17 package com.android.systemui; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.animation.ValueAnimator.AnimatorUpdateListener; 24 import android.content.Context; 25 import android.graphics.RectF; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.view.MotionEvent; 29 import android.view.VelocityTracker; 30 import android.view.View; 31 import android.view.ViewConfiguration; 32 import android.view.accessibility.AccessibilityEvent; 33 import android.view.animation.AnimationUtils; 34 import android.view.animation.Interpolator; 35 import android.view.animation.LinearInterpolator; 36 37 public class SwipeHelper implements Gefingerpoken { 38 static final String TAG = "com.android.systemui.SwipeHelper"; 39 private static final boolean DEBUG = false; 40 private static final boolean DEBUG_INVALIDATE = false; 41 private static final boolean SLOW_ANIMATIONS = false; // DEBUG; 42 private static final boolean CONSTRAIN_SWIPE = true; 43 private static final boolean FADE_OUT_DURING_SWIPE = true; 44 private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; 45 46 public static final int X = 0; 47 public static final int Y = 1; 48 49 private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); 50 private final Interpolator mFastOutLinearInInterpolator; 51 52 private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec 53 private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms 54 private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms 55 private int MAX_DISMISS_VELOCITY = 2000; // dp/sec 56 private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms 57 58 public static float SWIPE_PROGRESS_FADE_START = 0f; // fraction of thumbnail width 59 // where fade starts 60 static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width 61 // beyond which swipe progress->0 62 private float mMinSwipeProgress = 0f; 63 private float mMaxSwipeProgress = 1f; 64 65 private float mPagingTouchSlop; 66 private Callback mCallback; 67 private Handler mHandler; 68 private int mSwipeDirection; 69 private VelocityTracker mVelocityTracker; 70 71 private float mInitialTouchPos; 72 private boolean mDragging; 73 private View mCurrView; 74 private View mCurrAnimView; 75 private boolean mCanCurrViewBeDimissed; 76 private float mDensityScale; 77 78 private boolean mLongPressSent; 79 private LongPressListener mLongPressListener; 80 private Runnable mWatchLongPress; 81 private long mLongPressTimeout; 82 83 final private int[] mTmpPos = new int[2]; 84 private int mFalsingThreshold; 85 private boolean mTouchAboveFalsingThreshold; 86 87 public SwipeHelper(int swipeDirection, Callback callback, Context context) { 88 mCallback = callback; 89 mHandler = new Handler(); 90 mSwipeDirection = swipeDirection; 91 mVelocityTracker = VelocityTracker.obtain(); 92 mDensityScale = context.getResources().getDisplayMetrics().density; 93 mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); 94 95 mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press! 96 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, 97 android.R.interpolator.fast_out_linear_in); 98 mFalsingThreshold = context.getResources().getDimensionPixelSize( 99 R.dimen.swipe_helper_falsing_threshold); 100 } 101 102 public void setLongPressListener(LongPressListener listener) { 103 mLongPressListener = listener; 104 } 105 106 public void setDensityScale(float densityScale) { 107 mDensityScale = densityScale; 108 } 109 110 public void setPagingTouchSlop(float pagingTouchSlop) { 111 mPagingTouchSlop = pagingTouchSlop; 112 } 113 114 private float getPos(MotionEvent ev) { 115 return mSwipeDirection == X ? ev.getX() : ev.getY(); 116 } 117 118 private float getTranslation(View v) { 119 return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); 120 } 121 122 private float getVelocity(VelocityTracker vt) { 123 return mSwipeDirection == X ? vt.getXVelocity() : 124 vt.getYVelocity(); 125 } 126 127 private ObjectAnimator createTranslationAnimation(View v, float newPos) { 128 ObjectAnimator anim = ObjectAnimator.ofFloat(v, 129 mSwipeDirection == X ? "translationX" : "translationY", newPos); 130 return anim; 131 } 132 133 private float getPerpendicularVelocity(VelocityTracker vt) { 134 return mSwipeDirection == X ? vt.getYVelocity() : 135 vt.getXVelocity(); 136 } 137 138 private void setTranslation(View v, float translate) { 139 if (mSwipeDirection == X) { 140 v.setTranslationX(translate); 141 } else { 142 v.setTranslationY(translate); 143 } 144 } 145 146 private float getSize(View v) { 147 return mSwipeDirection == X ? v.getMeasuredWidth() : 148 v.getMeasuredHeight(); 149 } 150 151 public void setMinSwipeProgress(float minSwipeProgress) { 152 mMinSwipeProgress = minSwipeProgress; 153 } 154 155 public void setMaxSwipeProgress(float maxSwipeProgress) { 156 mMaxSwipeProgress = maxSwipeProgress; 157 } 158 159 private float getSwipeProgressForOffset(View view) { 160 float viewSize = getSize(view); 161 final float fadeSize = SWIPE_PROGRESS_FADE_END * viewSize; 162 float result = 1.0f; 163 float pos = getTranslation(view); 164 if (pos >= viewSize * SWIPE_PROGRESS_FADE_START) { 165 result = 1.0f - (pos - viewSize * SWIPE_PROGRESS_FADE_START) / fadeSize; 166 } else if (pos < viewSize * (1.0f - SWIPE_PROGRESS_FADE_START)) { 167 result = 1.0f + (viewSize * SWIPE_PROGRESS_FADE_START + pos) / fadeSize; 168 } 169 return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress); 170 } 171 172 private void updateSwipeProgressFromOffset(View animView, boolean dismissable) { 173 float swipeProgress = getSwipeProgressForOffset(animView); 174 if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) { 175 if (FADE_OUT_DURING_SWIPE && dismissable) { 176 float alpha = swipeProgress; 177 if (alpha != 0f && alpha != 1f) { 178 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 179 } else { 180 animView.setLayerType(View.LAYER_TYPE_NONE, null); 181 } 182 animView.setAlpha(getSwipeProgressForOffset(animView)); 183 } 184 } 185 invalidateGlobalRegion(animView); 186 } 187 188 // invalidate the view's own bounds all the way up the view hierarchy 189 public static void invalidateGlobalRegion(View view) { 190 invalidateGlobalRegion( 191 view, 192 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); 193 } 194 195 // invalidate a rectangle relative to the view's coordinate system all the way up the view 196 // hierarchy 197 public static void invalidateGlobalRegion(View view, RectF childBounds) { 198 //childBounds.offset(view.getTranslationX(), view.getTranslationY()); 199 if (DEBUG_INVALIDATE) 200 Log.v(TAG, "-------------"); 201 while (view.getParent() != null && view.getParent() instanceof View) { 202 view = (View) view.getParent(); 203 view.getMatrix().mapRect(childBounds); 204 view.invalidate((int) Math.floor(childBounds.left), 205 (int) Math.floor(childBounds.top), 206 (int) Math.ceil(childBounds.right), 207 (int) Math.ceil(childBounds.bottom)); 208 if (DEBUG_INVALIDATE) { 209 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) 210 + "," + (int) Math.floor(childBounds.top) 211 + "," + (int) Math.ceil(childBounds.right) 212 + "," + (int) Math.ceil(childBounds.bottom)); 213 } 214 } 215 } 216 217 public void removeLongPressCallback() { 218 if (mWatchLongPress != null) { 219 mHandler.removeCallbacks(mWatchLongPress); 220 mWatchLongPress = null; 221 } 222 } 223 224 public boolean onInterceptTouchEvent(final MotionEvent ev) { 225 final int action = ev.getAction(); 226 227 switch (action) { 228 case MotionEvent.ACTION_DOWN: 229 mTouchAboveFalsingThreshold = false; 230 mDragging = false; 231 mLongPressSent = false; 232 mCurrView = mCallback.getChildAtPosition(ev); 233 mVelocityTracker.clear(); 234 if (mCurrView != null) { 235 mCurrAnimView = mCallback.getChildContentView(mCurrView); 236 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 237 mVelocityTracker.addMovement(ev); 238 mInitialTouchPos = getPos(ev); 239 240 if (mLongPressListener != null) { 241 if (mWatchLongPress == null) { 242 mWatchLongPress = new Runnable() { 243 @Override 244 public void run() { 245 if (mCurrView != null && !mLongPressSent) { 246 mLongPressSent = true; 247 mCurrView.sendAccessibilityEvent( 248 AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 249 mCurrView.getLocationOnScreen(mTmpPos); 250 final int x = (int) ev.getRawX() - mTmpPos[0]; 251 final int y = (int) ev.getRawY() - mTmpPos[1]; 252 mLongPressListener.onLongPress(mCurrView, x, y); 253 } 254 } 255 }; 256 } 257 mHandler.postDelayed(mWatchLongPress, mLongPressTimeout); 258 } 259 260 } 261 break; 262 263 case MotionEvent.ACTION_MOVE: 264 if (mCurrView != null && !mLongPressSent) { 265 mVelocityTracker.addMovement(ev); 266 float pos = getPos(ev); 267 float delta = pos - mInitialTouchPos; 268 if (Math.abs(delta) > mPagingTouchSlop) { 269 mCallback.onBeginDrag(mCurrView); 270 mDragging = true; 271 mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); 272 273 removeLongPressCallback(); 274 } 275 } 276 277 break; 278 279 case MotionEvent.ACTION_UP: 280 case MotionEvent.ACTION_CANCEL: 281 final boolean captured = (mDragging || mLongPressSent); 282 mDragging = false; 283 mCurrView = null; 284 mCurrAnimView = null; 285 mLongPressSent = false; 286 removeLongPressCallback(); 287 if (captured) return true; 288 break; 289 } 290 return mDragging || mLongPressSent; 291 } 292 293 /** 294 * @param view The view to be dismissed 295 * @param velocity The desired pixels/second speed at which the view should move 296 */ 297 public void dismissChild(final View view, float velocity) { 298 dismissChild(view, velocity, null, 0, false, 0); 299 } 300 301 /** 302 * @param view The view to be dismissed 303 * @param velocity The desired pixels/second speed at which the view should move 304 * @param endAction The action to perform at the end 305 * @param delay The delay after which we should start 306 * @param useAccelerateInterpolator Should an accelerating Interpolator be used 307 * @param fixedDuration If not 0, this exact duration will be taken 308 */ 309 public void dismissChild(final View view, float velocity, final Runnable endAction, 310 long delay, boolean useAccelerateInterpolator, long fixedDuration) { 311 final View animView = mCallback.getChildContentView(view); 312 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 313 float newPos; 314 boolean isLayoutRtl = view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 315 316 if (velocity < 0 317 || (velocity == 0 && getTranslation(animView) < 0) 318 // if we use the Menu to dismiss an item in landscape, animate up 319 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y) 320 // if the language is rtl we prefer swiping to the left 321 || (velocity == 0 && getTranslation(animView) == 0 && isLayoutRtl)) { 322 newPos = -getSize(animView); 323 } else { 324 newPos = getSize(animView); 325 } 326 long duration; 327 if (fixedDuration == 0) { 328 duration = MAX_ESCAPE_ANIMATION_DURATION; 329 if (velocity != 0) { 330 duration = Math.min(duration, 331 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math 332 .abs(velocity)) 333 ); 334 } else { 335 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 336 } 337 } else { 338 duration = fixedDuration; 339 } 340 341 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 342 ObjectAnimator anim = createTranslationAnimation(animView, newPos); 343 if (useAccelerateInterpolator) { 344 anim.setInterpolator(mFastOutLinearInInterpolator); 345 } else { 346 anim.setInterpolator(sLinearInterpolator); 347 } 348 anim.setDuration(duration); 349 if (delay > 0) { 350 anim.setStartDelay(delay); 351 } 352 anim.addListener(new AnimatorListenerAdapter() { 353 public void onAnimationEnd(Animator animation) { 354 mCallback.onChildDismissed(view); 355 if (endAction != null) { 356 endAction.run(); 357 } 358 animView.setLayerType(View.LAYER_TYPE_NONE, null); 359 } 360 }); 361 anim.addUpdateListener(new AnimatorUpdateListener() { 362 public void onAnimationUpdate(ValueAnimator animation) { 363 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); 364 } 365 }); 366 anim.start(); 367 } 368 369 public void snapChild(final View view, float velocity) { 370 final View animView = mCallback.getChildContentView(view); 371 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); 372 ObjectAnimator anim = createTranslationAnimation(animView, 0); 373 int duration = SNAP_ANIM_LEN; 374 anim.setDuration(duration); 375 anim.addUpdateListener(new AnimatorUpdateListener() { 376 public void onAnimationUpdate(ValueAnimator animation) { 377 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); 378 } 379 }); 380 anim.addListener(new AnimatorListenerAdapter() { 381 public void onAnimationEnd(Animator animator) { 382 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); 383 mCallback.onChildSnappedBack(animView); 384 } 385 }); 386 anim.start(); 387 } 388 389 public boolean onTouchEvent(MotionEvent ev) { 390 if (mLongPressSent) { 391 return true; 392 } 393 394 if (!mDragging) { 395 if (mCallback.getChildAtPosition(ev) != null) { 396 397 // We are dragging directly over a card, make sure that we also catch the gesture 398 // even if nobody else wants the touch event. 399 onInterceptTouchEvent(ev); 400 return true; 401 } else { 402 403 // We are not doing anything, make sure the long press callback 404 // is not still ticking like a bomb waiting to go off. 405 removeLongPressCallback(); 406 return false; 407 } 408 } 409 410 mVelocityTracker.addMovement(ev); 411 final int action = ev.getAction(); 412 switch (action) { 413 case MotionEvent.ACTION_OUTSIDE: 414 case MotionEvent.ACTION_MOVE: 415 if (mCurrView != null) { 416 float delta = getPos(ev) - mInitialTouchPos; 417 float absDelta = Math.abs(delta); 418 if (absDelta >= getFalsingThreshold()) { 419 mTouchAboveFalsingThreshold = true; 420 } 421 // don't let items that can't be dismissed be dragged more than 422 // maxScrollDistance 423 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 424 float size = getSize(mCurrAnimView); 425 float maxScrollDistance = 0.15f * size; 426 if (absDelta >= size) { 427 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 428 } else { 429 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); 430 } 431 } 432 setTranslation(mCurrAnimView, delta); 433 434 updateSwipeProgressFromOffset(mCurrAnimView, mCanCurrViewBeDimissed); 435 } 436 break; 437 case MotionEvent.ACTION_UP: 438 case MotionEvent.ACTION_CANCEL: 439 if (mCurrView != null) { 440 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 441 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 442 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 443 float velocity = getVelocity(mVelocityTracker); 444 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 445 446 // Decide whether to dismiss the current view 447 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 448 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); 449 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 450 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 451 (velocity > 0) == (getTranslation(mCurrAnimView) > 0); 452 boolean falsingDetected = mCallback.isAntiFalsingNeeded() 453 && !mTouchAboveFalsingThreshold; 454 455 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) 456 && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough) 457 && ev.getActionMasked() == MotionEvent.ACTION_UP; 458 459 if (dismissChild) { 460 // flingadingy 461 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 462 } else { 463 // snappity 464 mCallback.onDragCancelled(mCurrView); 465 snapChild(mCurrView, velocity); 466 } 467 } 468 break; 469 } 470 return true; 471 } 472 473 private int getFalsingThreshold() { 474 float factor = mCallback.getFalsingThresholdFactor(); 475 return (int) (mFalsingThreshold * factor); 476 } 477 478 public interface Callback { 479 View getChildAtPosition(MotionEvent ev); 480 481 View getChildContentView(View v); 482 483 boolean canChildBeDismissed(View v); 484 485 boolean isAntiFalsingNeeded(); 486 487 void onBeginDrag(View v); 488 489 void onChildDismissed(View v); 490 491 void onDragCancelled(View v); 492 493 void onChildSnappedBack(View animView); 494 495 /** 496 * Updates the swipe progress on a child. 497 * 498 * @return if true, prevents the default alpha fading. 499 */ 500 boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress); 501 502 /** 503 * @return The factor the falsing threshold should be multiplied with 504 */ 505 float getFalsingThresholdFactor(); 506 } 507 508 /** 509 * Equivalent to View.OnLongClickListener with coordinates 510 */ 511 public interface LongPressListener { 512 /** 513 * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates 514 * @return whether the longpress was handled 515 */ 516 boolean onLongPress(View v, int x, int y); 517 } 518 } 519