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 SwipeHelper(int swipeDirection, Callback callback, Context context)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 setLongPressListener(LongPressListener listener)102 public void setLongPressListener(LongPressListener listener) { 103 mLongPressListener = listener; 104 } 105 setDensityScale(float densityScale)106 public void setDensityScale(float densityScale) { 107 mDensityScale = densityScale; 108 } 109 setPagingTouchSlop(float pagingTouchSlop)110 public void setPagingTouchSlop(float pagingTouchSlop) { 111 mPagingTouchSlop = pagingTouchSlop; 112 } 113 getPos(MotionEvent ev)114 private float getPos(MotionEvent ev) { 115 return mSwipeDirection == X ? ev.getX() : ev.getY(); 116 } 117 getTranslation(View v)118 private float getTranslation(View v) { 119 return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); 120 } 121 getVelocity(VelocityTracker vt)122 private float getVelocity(VelocityTracker vt) { 123 return mSwipeDirection == X ? vt.getXVelocity() : 124 vt.getYVelocity(); 125 } 126 createTranslationAnimation(View v, float newPos)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 getPerpendicularVelocity(VelocityTracker vt)133 private float getPerpendicularVelocity(VelocityTracker vt) { 134 return mSwipeDirection == X ? vt.getYVelocity() : 135 vt.getXVelocity(); 136 } 137 setTranslation(View v, float translate)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 getSize(View v)146 private float getSize(View v) { 147 return mSwipeDirection == X ? v.getMeasuredWidth() : 148 v.getMeasuredHeight(); 149 } 150 setMinSwipeProgress(float minSwipeProgress)151 public void setMinSwipeProgress(float minSwipeProgress) { 152 mMinSwipeProgress = minSwipeProgress; 153 } 154 setMaxSwipeProgress(float maxSwipeProgress)155 public void setMaxSwipeProgress(float maxSwipeProgress) { 156 mMaxSwipeProgress = maxSwipeProgress; 157 } 158 getSwipeProgressForOffset(View view)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 updateSwipeProgressFromOffset(View animView, boolean dismissable)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 invalidateGlobalRegion(View view)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 invalidateGlobalRegion(View view, RectF childBounds)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 removeLongPressCallback()217 public void removeLongPressCallback() { 218 if (mWatchLongPress != null) { 219 mHandler.removeCallbacks(mWatchLongPress); 220 mWatchLongPress = null; 221 } 222 } 223 onInterceptTouchEvent(final MotionEvent ev)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 */ dismissChild(final View view, float velocity)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 */ dismissChild(final View view, float velocity, final Runnable endAction, long delay, boolean useAccelerateInterpolator, long fixedDuration)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 snapChild(final View view, float velocity)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 onTouchEvent(MotionEvent ev)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 getFalsingThreshold()473 private int getFalsingThreshold() { 474 float factor = mCallback.getFalsingThresholdFactor(); 475 return (int) (mFalsingThreshold * factor); 476 } 477 478 public interface Callback { getChildAtPosition(MotionEvent ev)479 View getChildAtPosition(MotionEvent ev); 480 getChildContentView(View v)481 View getChildContentView(View v); 482 canChildBeDismissed(View v)483 boolean canChildBeDismissed(View v); 484 isAntiFalsingNeeded()485 boolean isAntiFalsingNeeded(); 486 onBeginDrag(View v)487 void onBeginDrag(View v); 488 onChildDismissed(View v)489 void onChildDismissed(View v); 490 onDragCancelled(View v)491 void onDragCancelled(View v); 492 onChildSnappedBack(View animView)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 */ updateSwipeProgress(View animView, boolean dismissable, float swipeProgress)500 boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress); 501 502 /** 503 * @return The factor the falsing threshold should be multiplied with 504 */ getFalsingThresholdFactor()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 */ onLongPress(View v, int x, int y)516 boolean onLongPress(View v, int x, int y); 517 } 518 } 519