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 315 if (velocity < 0 316 || (velocity == 0 && getTranslation(animView) < 0) 317 // if we use the Menu to dismiss an item in landscape, animate up 318 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) { 319 newPos = -getSize(animView); 320 } else { 321 newPos = getSize(animView); 322 } 323 long duration; 324 if (fixedDuration == 0) { 325 duration = MAX_ESCAPE_ANIMATION_DURATION; 326 if (velocity != 0) { 327 duration = Math.min(duration, 328 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math 329 .abs(velocity)) 330 ); 331 } else { 332 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 333 } 334 } else { 335 duration = fixedDuration; 336 } 337 338 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 339 ObjectAnimator anim = createTranslationAnimation(animView, newPos); 340 if (useAccelerateInterpolator) { 341 anim.setInterpolator(mFastOutLinearInInterpolator); 342 } else { 343 anim.setInterpolator(sLinearInterpolator); 344 } 345 anim.setDuration(duration); 346 if (delay > 0) { 347 anim.setStartDelay(delay); 348 } 349 anim.addListener(new AnimatorListenerAdapter() { 350 public void onAnimationEnd(Animator animation) { 351 mCallback.onChildDismissed(view); 352 if (endAction != null) { 353 endAction.run(); 354 } 355 animView.setLayerType(View.LAYER_TYPE_NONE, null); 356 } 357 }); 358 anim.addUpdateListener(new AnimatorUpdateListener() { 359 public void onAnimationUpdate(ValueAnimator animation) { 360 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); 361 } 362 }); 363 anim.start(); 364 } 365 366 public void snapChild(final View view, float velocity) { 367 final View animView = mCallback.getChildContentView(view); 368 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); 369 ObjectAnimator anim = createTranslationAnimation(animView, 0); 370 int duration = SNAP_ANIM_LEN; 371 anim.setDuration(duration); 372 anim.addUpdateListener(new AnimatorUpdateListener() { 373 public void onAnimationUpdate(ValueAnimator animation) { 374 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); 375 } 376 }); 377 anim.addListener(new AnimatorListenerAdapter() { 378 public void onAnimationEnd(Animator animator) { 379 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed); 380 mCallback.onChildSnappedBack(animView); 381 } 382 }); 383 anim.start(); 384 } 385 386 public boolean onTouchEvent(MotionEvent ev) { 387 if (mLongPressSent) { 388 return true; 389 } 390 391 if (!mDragging) { 392 if (mCallback.getChildAtPosition(ev) != null) { 393 394 // We are dragging directly over a card, make sure that we also catch the gesture 395 // even if nobody else wants the touch event. 396 onInterceptTouchEvent(ev); 397 return true; 398 } else { 399 400 // We are not doing anything, make sure the long press callback 401 // is not still ticking like a bomb waiting to go off. 402 removeLongPressCallback(); 403 return false; 404 } 405 } 406 407 mVelocityTracker.addMovement(ev); 408 final int action = ev.getAction(); 409 switch (action) { 410 case MotionEvent.ACTION_OUTSIDE: 411 case MotionEvent.ACTION_MOVE: 412 if (mCurrView != null) { 413 float delta = getPos(ev) - mInitialTouchPos; 414 float absDelta = Math.abs(delta); 415 if (absDelta >= getFalsingThreshold()) { 416 mTouchAboveFalsingThreshold = true; 417 } 418 // don't let items that can't be dismissed be dragged more than 419 // maxScrollDistance 420 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 421 float size = getSize(mCurrAnimView); 422 float maxScrollDistance = 0.15f * size; 423 if (absDelta >= size) { 424 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 425 } else { 426 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); 427 } 428 } 429 setTranslation(mCurrAnimView, delta); 430 431 updateSwipeProgressFromOffset(mCurrAnimView, mCanCurrViewBeDimissed); 432 } 433 break; 434 case MotionEvent.ACTION_UP: 435 case MotionEvent.ACTION_CANCEL: 436 if (mCurrView != null) { 437 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 438 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 439 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 440 float velocity = getVelocity(mVelocityTracker); 441 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 442 443 // Decide whether to dismiss the current view 444 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 445 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); 446 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 447 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 448 (velocity > 0) == (getTranslation(mCurrAnimView) > 0); 449 boolean falsingDetected = mCallback.isAntiFalsingNeeded() 450 && !mTouchAboveFalsingThreshold; 451 452 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) 453 && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough); 454 455 if (dismissChild) { 456 // flingadingy 457 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 458 } else { 459 // snappity 460 mCallback.onDragCancelled(mCurrView); 461 snapChild(mCurrView, velocity); 462 } 463 } 464 break; 465 } 466 return true; 467 } 468 469 private int getFalsingThreshold() { 470 float factor = mCallback.getFalsingThresholdFactor(); 471 return (int) (mFalsingThreshold * factor); 472 } 473 474 public interface Callback { 475 View getChildAtPosition(MotionEvent ev); 476 477 View getChildContentView(View v); 478 479 boolean canChildBeDismissed(View v); 480 481 boolean isAntiFalsingNeeded(); 482 483 void onBeginDrag(View v); 484 485 void onChildDismissed(View v); 486 487 void onDragCancelled(View v); 488 489 void onChildSnappedBack(View animView); 490 491 /** 492 * Updates the swipe progress on a child. 493 * 494 * @return if true, prevents the default alpha fading. 495 */ 496 boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress); 497 498 /** 499 * @return The factor the falsing threshold should be multiplied with 500 */ 501 float getFalsingThresholdFactor(); 502 } 503 504 /** 505 * Equivalent to View.OnLongClickListener with coordinates 506 */ 507 public interface LongPressListener { 508 /** 509 * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates 510 * @return whether the longpress was handled 511 */ 512 boolean onLongPress(View v, int x, int y); 513 } 514 } 515