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