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 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 snapChild(final View view, float velocity)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 onTouchEvent(MotionEvent ev)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 getFalsingThreshold()469 private int getFalsingThreshold() { 470 float factor = mCallback.getFalsingThresholdFactor(); 471 return (int) (mFalsingThreshold * factor); 472 } 473 474 public interface Callback { getChildAtPosition(MotionEvent ev)475 View getChildAtPosition(MotionEvent ev); 476 getChildContentView(View v)477 View getChildContentView(View v); 478 canChildBeDismissed(View v)479 boolean canChildBeDismissed(View v); 480 isAntiFalsingNeeded()481 boolean isAntiFalsingNeeded(); 482 onBeginDrag(View v)483 void onBeginDrag(View v); 484 onChildDismissed(View v)485 void onChildDismissed(View v); 486 onDragCancelled(View v)487 void onDragCancelled(View v); 488 onChildSnappedBack(View animView)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 */ updateSwipeProgress(View animView, boolean dismissable, float swipeProgress)496 boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress); 497 498 /** 499 * @return The factor the falsing threshold should be multiplied with 500 */ getFalsingThresholdFactor()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 */ onLongPress(View v, int x, int y)512 boolean onLongPress(View v, int x, int y); 513 } 514 } 515