SwipeHelper.java revision 4cf76548c08d8447546a5b8a11c2ea5377f91025
1/* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mail.ui; 19 20import android.animation.Animator; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.AnimatorSet; 23import android.animation.ObjectAnimator; 24import android.animation.ValueAnimator; 25import android.animation.ValueAnimator.AnimatorUpdateListener; 26import android.graphics.RectF; 27import android.util.Log; 28import android.view.MotionEvent; 29import android.view.VelocityTracker; 30import android.view.View; 31import android.view.animation.LinearInterpolator; 32 33import com.android.mail.browse.ConversationItemView; 34 35import java.util.ArrayList; 36import java.util.Collection; 37 38public class SwipeHelper { 39 static final String TAG = "com.android.systemui.SwipeHelper"; 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 private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false; // STOPSHIP - DEBUG ONLY 46 47 public static final int X = 0; 48 public static final int Y = 1; 49 50 private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); 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 : 1; // ms 57 private static final int DISMISS_ANIMATION_DURATION = 500; 58 59 public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width 60 // where fade starts 61 static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width 62 // beyond which alpha->0 63 private static final float FACTOR = 1.2f; 64 private float mMinAlpha = 0.5f; 65 66 private float mPagingTouchSlop; 67 private Callback mCallback; 68 private int mSwipeDirection; 69 private VelocityTracker mVelocityTracker; 70 71 private float mInitialTouchPosX; 72 private boolean mDragging; 73 private SwipeableItemView mCurrView; 74 private View mCurrAnimView; 75 private boolean mCanCurrViewBeDimissed; 76 private float mDensityScale; 77 private float mLastY; 78 private final float mScrollSlop; 79 private float mInitialTouchPosY; 80 private float mMinSwipe; 81 private float mMinVert; 82 private float mMinLock; 83 84 public SwipeHelper(int swipeDirection, Callback callback, float densityScale, 85 float pagingTouchSlop, float scrollSlop, float minSwipe, float minVert, float minLock) { 86 mCallback = callback; 87 mSwipeDirection = swipeDirection; 88 mVelocityTracker = VelocityTracker.obtain(); 89 mDensityScale = densityScale; 90 mPagingTouchSlop = pagingTouchSlop; 91 mScrollSlop = scrollSlop; 92 mMinSwipe = minSwipe; 93 mMinVert = minVert; 94 mMinLock = minLock; 95 } 96 97 public void setDensityScale(float densityScale) { 98 mDensityScale = densityScale; 99 } 100 101 public void setPagingTouchSlop(float pagingTouchSlop) { 102 mPagingTouchSlop = pagingTouchSlop; 103 } 104 105 private float getVelocity(VelocityTracker vt) { 106 return mSwipeDirection == X ? vt.getXVelocity() : 107 vt.getYVelocity(); 108 } 109 110 private ObjectAnimator createTranslationAnimation(View v, float newPos) { 111 ObjectAnimator anim = ObjectAnimator.ofFloat(v, 112 mSwipeDirection == X ? "translationX" : "translationY", newPos); 113 return anim; 114 } 115 116 private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) { 117 ObjectAnimator anim = createTranslationAnimation(v, newPos); 118 anim.setInterpolator(sLinearInterpolator); 119 anim.setDuration(duration); 120 return anim; 121 } 122 123 private float getPerpendicularVelocity(VelocityTracker vt) { 124 return mSwipeDirection == X ? vt.getYVelocity() : 125 vt.getXVelocity(); 126 } 127 128 private void setTranslation(View v, float translate) { 129 if (mSwipeDirection == X) { 130 v.setTranslationX(translate); 131 } else { 132 v.setTranslationY(translate); 133 } 134 } 135 136 private float getSize(View v) { 137 return mSwipeDirection == X ? v.getMeasuredWidth() : 138 v.getMeasuredHeight(); 139 } 140 141 public void setMinAlpha(float minAlpha) { 142 mMinAlpha = minAlpha; 143 } 144 145 private float getAlphaForOffset(View view) { 146 float viewSize = getSize(view); 147 final float fadeSize = ALPHA_FADE_END * viewSize; 148 float result = 1.0f; 149 float pos = view.getTranslationX(); 150 if (pos >= viewSize * ALPHA_FADE_START) { 151 result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; 152 } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { 153 result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; 154 } 155 return Math.max(mMinAlpha, result); 156 } 157 158 // invalidate the view's own bounds all the way up the view hierarchy 159 public static void invalidateGlobalRegion(View view) { 160 invalidateGlobalRegion( 161 view, 162 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); 163 } 164 165 // invalidate a rectangle relative to the view's coordinate system all the way up the view 166 // hierarchy 167 public static void invalidateGlobalRegion(View view, RectF childBounds) { 168 //childBounds.offset(view.getTranslationX(), view.getTranslationY()); 169 if (DEBUG_INVALIDATE) 170 Log.v(TAG, "-------------"); 171 while (view.getParent() != null && view.getParent() instanceof View) { 172 view = (View) view.getParent(); 173 view.getMatrix().mapRect(childBounds); 174 view.invalidate((int) Math.floor(childBounds.left), 175 (int) Math.floor(childBounds.top), 176 (int) Math.ceil(childBounds.right), 177 (int) Math.ceil(childBounds.bottom)); 178 if (DEBUG_INVALIDATE) { 179 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) 180 + "," + (int) Math.floor(childBounds.top) 181 + "," + (int) Math.ceil(childBounds.right) 182 + "," + (int) Math.ceil(childBounds.bottom)); 183 } 184 } 185 } 186 187 public boolean onInterceptTouchEvent(MotionEvent ev) { 188 final int action = ev.getAction(); 189 switch (action) { 190 case MotionEvent.ACTION_DOWN: 191 mLastY = ev.getY(); 192 mDragging = false; 193 View view = mCallback.getChildAtPosition(ev); 194 if (view instanceof SwipeableItemView) { 195 mCurrView = (SwipeableItemView) view; 196 } 197 mVelocityTracker.clear(); 198 if (mCurrView != null) { 199 mCurrAnimView = mCurrView.getView(); 200 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 201 mVelocityTracker.addMovement(ev); 202 mInitialTouchPosX = ev.getX(); 203 mInitialTouchPosY = ev.getY(); 204 } 205 break; 206 case MotionEvent.ACTION_MOVE: 207 if (mCurrView != null) { 208 // Check the movement direction. 209 if (mLastY >= 0 && !mDragging) { 210 float currY = ev.getY(); 211 float currX = ev.getX(); 212 float deltaY = Math.abs(currY - mInitialTouchPosY); 213 float deltaX = Math.abs(currX - mInitialTouchPosX); 214 if (deltaY > mScrollSlop && deltaY > (FACTOR * deltaX)) { 215 mLastY = ev.getY(); 216 mCurrView.cancelTap(); 217 return false; 218 } 219 } 220 mVelocityTracker.addMovement(ev); 221 float pos = ev.getX(); 222 float delta = pos - mInitialTouchPosX; 223 if (Math.abs(delta) > mPagingTouchSlop) { 224 mCallback.onBeginDrag(mCurrView.getView()); 225 mDragging = true; 226 mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX(); 227 mInitialTouchPosY = ev.getY(); 228 mCurrView.cancelTap(); 229 } 230 } 231 mLastY = ev.getY(); 232 break; 233 case MotionEvent.ACTION_UP: 234 case MotionEvent.ACTION_CANCEL: 235 mDragging = false; 236 mCurrView = null; 237 mCurrAnimView = null; 238 mLastY = -1; 239 break; 240 } 241 return mDragging; 242 } 243 244 /** 245 * @param view The view to be dismissed 246 * @param velocity The desired pixels/second speed at which the view should 247 * move 248 */ 249 private void dismissChild(final SwipeableItemView view, float velocity) { 250 final View animView = mCurrView.getView(); 251 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 252 float newPos = determinePos(animView, velocity); 253 int duration = determineDuration(animView, newPos, velocity); 254 255 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 256 ObjectAnimator anim = createDismissAnimation(animView, newPos, duration); 257 anim.addListener(new AnimatorListenerAdapter() { 258 @Override 259 public void onAnimationEnd(Animator animation) { 260 mCallback.onChildDismissed(mCurrView); 261 animView.setLayerType(View.LAYER_TYPE_NONE, null); 262 } 263 }); 264 anim.addUpdateListener(new AnimatorUpdateListener() { 265 @Override 266 public void onAnimationUpdate(ValueAnimator animation) { 267 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 268 animView.setAlpha(getAlphaForOffset(animView)); 269 } 270 invalidateGlobalRegion(animView); 271 } 272 }); 273 anim.start(); 274 } 275 276 private void dismissChildren(final Collection<ConversationItemView> views, float velocity, 277 AnimatorListenerAdapter listener) { 278 final View animView = mCurrView.getView(); 279 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(mCurrView); 280 float newPos = determinePos(animView, velocity); 281 int duration = DISMISS_ANIMATION_DURATION; 282 ArrayList<Animator> animations = new ArrayList<Animator>(); 283 ObjectAnimator anim; 284 for (final ConversationItemView view : views) { 285 view.setLayerType(View.LAYER_TYPE_HARDWARE, null); 286 anim = createDismissAnimation(view, newPos, duration); 287 anim.addUpdateListener(new AnimatorUpdateListener() { 288 @Override 289 public void onAnimationUpdate(ValueAnimator animation) { 290 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 291 view.setAlpha(getAlphaForOffset(view)); 292 } 293 invalidateGlobalRegion(view); 294 } 295 }); 296 animations.add(anim); 297 } 298 AnimatorSet transitionSet = new AnimatorSet(); 299 transitionSet.playTogether(animations); 300 transitionSet.addListener(listener); 301 transitionSet.start(); 302 } 303 304 public void dismissChildren(ConversationItemView first, 305 final Collection<ConversationItemView> views, AnimatorListenerAdapter listener) { 306 mCurrView = first; 307 dismissChildren(views, 0f, listener); 308 } 309 310 private int determineDuration(View animView, float newPos, float velocity) { 311 int duration = MAX_ESCAPE_ANIMATION_DURATION; 312 if (velocity != 0) { 313 duration = Math 314 .min(duration, 315 (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math 316 .abs(velocity))); 317 } else { 318 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 319 } 320 return duration; 321 } 322 323 private float determinePos(View animView, float velocity) { 324 float newPos = 0; 325 if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0) 326 // if we use the Menu to dismiss an item in landscape, animate up 327 || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) { 328 newPos = -getSize(animView); 329 } else { 330 newPos = getSize(animView); 331 } 332 return newPos; 333 } 334 335 public void snapChild(final SwipeableItemView view, float velocity) { 336 final View animView = view.getView(); 337 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 338 ObjectAnimator anim = createTranslationAnimation(animView, 0); 339 int duration = SNAP_ANIM_LEN; 340 anim.setDuration(duration); 341 anim.addUpdateListener(new AnimatorUpdateListener() { 342 @Override 343 public void onAnimationUpdate(ValueAnimator animation) { 344 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 345 animView.setAlpha(getAlphaForOffset(animView)); 346 } 347 invalidateGlobalRegion(animView); 348 } 349 }); 350 anim.addListener(new Animator.AnimatorListener() { 351 @Override 352 public void onAnimationStart(Animator animation) { 353 } 354 @Override 355 public void onAnimationEnd(Animator animation) { 356 animView.setAlpha(1.0f); 357 } 358 @Override 359 public void onAnimationCancel(Animator animation) { 360 } 361 @Override 362 public void onAnimationRepeat(Animator animation) { 363 } 364 }); 365 anim.start(); 366 } 367 368 public boolean onTouchEvent(MotionEvent ev) { 369 if (!mDragging) { 370 return false; 371 } 372 // If this item is being dragged, cancel any tap handlers/ events/ 373 // actions for this item. 374 if (mCurrView != null) { 375 mCurrView.cancelTap(); 376 } 377 mVelocityTracker.addMovement(ev); 378 final int action = ev.getAction(); 379 switch (action) { 380 case MotionEvent.ACTION_OUTSIDE: 381 case MotionEvent.ACTION_MOVE: 382 if (mCurrView != null) { 383 float deltaX = ev.getX() - mInitialTouchPosX; 384 float deltaY = Math.abs(ev.getY() - mInitialTouchPosY); 385 // If the user has gone vertical and not gone horizontalish AT 386 // LEAST minBeforeLock, switch to scroll. Otherwise, cancel 387 // the swipe. 388 if (!mDragging && deltaY > mMinVert && (Math.abs(deltaX)) < mMinLock 389 && deltaY > (FACTOR * Math.abs(deltaX))) { 390 return false; 391 } 392 float minDistance = mMinSwipe; 393 if (Math.abs(deltaX) < minDistance) { 394 // Don't start the drag until at least X distance has 395 // occurred. 396 return true; 397 } 398 // don't let items that can't be dismissed be dragged more 399 // than maxScrollDistance 400 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 401 float size = getSize(mCurrAnimView); 402 float maxScrollDistance = 0.15f * size; 403 if (Math.abs(deltaX) >= size) { 404 deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance; 405 } else { 406 deltaX = maxScrollDistance 407 * (float) Math.sin((deltaX / size) * (Math.PI / 2)); 408 } 409 } 410 setTranslation(mCurrAnimView, deltaX); 411 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 412 mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); 413 } 414 invalidateGlobalRegion(mCurrView.getView()); 415 } 416 break; 417 case MotionEvent.ACTION_UP: 418 case MotionEvent.ACTION_CANCEL: 419 if (mCurrView != null) { 420 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 421 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 422 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 423 float velocity = getVelocity(mVelocityTracker); 424 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 425 426 // Decide whether to dismiss the current view 427 // Tweak constants below as required to prevent erroneous 428 // swipe/dismiss 429 float translation = Math.abs(mCurrAnimView.getTranslationX()); 430 float currAnimViewSize = getSize(mCurrAnimView); 431 // Long swipe = translation of .4 * width 432 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH 433 && translation > 0.4 * currAnimViewSize; 434 // Fast swipe = > escapeVelocity and translation of .1 * 435 // width 436 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) 437 && (Math.abs(velocity) > Math.abs(perpendicularVelocity)) 438 && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0) 439 && translation > 0.05 * currAnimViewSize; 440 if (LOG_SWIPE_DISMISS_VELOCITY) { 441 Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/" 442 + perpendicularVelocity + ", x: " + translation + "/" 443 + currAnimViewSize); 444 } 445 446 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) 447 && (childSwipedFastEnough || childSwipedFarEnough); 448 449 if (dismissChild) { 450 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 451 } else { 452 snapChild(mCurrView, velocity); 453 // snappity 454 mCallback.onDragCancelled(mCurrView); 455 } 456 } 457 break; 458 } 459 return true; 460 } 461 462 public interface Callback { 463 View getChildAtPosition(MotionEvent ev); 464 465 boolean canChildBeDismissed(SwipeableItemView v); 466 467 void onBeginDrag(View v); 468 469 void onChildDismissed(SwipeableItemView v); 470 471 void onChildrenDismissed(SwipeableItemView target, Collection<ConversationItemView> v); 472 473 void onDragCancelled(SwipeableItemView v); 474 475 ConversationSelectionSet getSelectionSet(); 476 } 477} 478