SwipeHelper.java revision 7af43c93d4ca90bb0a43ec790a434c95128cc99f
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.content.Context; 27import android.content.res.Resources; 28import android.graphics.RectF; 29import android.util.Log; 30import android.view.MotionEvent; 31import android.view.VelocityTracker; 32import android.view.View; 33import android.view.animation.DecelerateInterpolator; 34 35import com.android.mail.R; 36import com.android.mail.browse.ConversationItemView; 37import com.android.mail.utils.Utils; 38 39import java.util.ArrayList; 40import java.util.Collection; 41 42public class SwipeHelper { 43 static final String TAG = "com.android.systemui.SwipeHelper"; 44 private static final boolean DEBUG_INVALIDATE = false; 45 private static final boolean CONSTRAIN_SWIPE = true; 46 private static final boolean FADE_OUT_DURING_SWIPE = true; 47 private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; 48 private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false; // STOPSHIP - DEBUG ONLY 49 50 public static final int X = 0; 51 public static final int Y = 1; 52 53 private static DecelerateInterpolator sDecelerateInterpolator = 54 new DecelerateInterpolator(1.0f); 55 56 private static int SWIPE_ESCAPE_VELOCITY = -1; 57 private static int DEFAULT_ESCAPE_ANIMATION_DURATION; 58 private static int MAX_ESCAPE_ANIMATION_DURATION; 59 private static int MAX_DISMISS_VELOCITY; 60 private static int SNAP_ANIM_LEN; 61 private static int DISMISS_ANIMATION_DURATION; 62 private static float MIN_SWIPE; 63 private static float MIN_VERT; 64 private static float MIN_LOCK; 65 66 public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width 67 // where fade starts 68 public static float ALPHA_TEXT_FADE_START = 0.4f; 69 static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width 70 // beyond which alpha->0 71 private static final float FACTOR = 1.2f; 72 private float mMinAlpha = 0.5f; 73 74 private float mPagingTouchSlop; 75 private Callback mCallback; 76 private int mSwipeDirection; 77 private VelocityTracker mVelocityTracker; 78 79 private float mInitialTouchPosX; 80 private boolean mDragging; 81 private SwipeableItemView mCurrView; 82 private View mCurrAnimView; 83 private boolean mCanCurrViewBeDimissed; 84 private float mDensityScale; 85 private float mLastY; 86 private float mInitialTouchPosY; 87 private LeaveBehindItem mPrevView; 88 89 public SwipeHelper(Context context, int swipeDirection, Callback callback, float densityScale, 90 float pagingTouchSlop) { 91 mCallback = callback; 92 mSwipeDirection = swipeDirection; 93 mVelocityTracker = VelocityTracker.obtain(); 94 mDensityScale = densityScale; 95 mPagingTouchSlop = pagingTouchSlop; 96 if (SWIPE_ESCAPE_VELOCITY == -1) { 97 Resources res = context.getResources(); 98 SWIPE_ESCAPE_VELOCITY = res.getInteger(R.integer.swipe_escape_velocity); 99 DEFAULT_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.escape_animation_duration); 100 MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration); 101 MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity); 102 SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration); 103 DISMISS_ANIMATION_DURATION = res.getInteger(R.integer.dismiss_animation_duration); 104 MIN_SWIPE = res.getDimension(R.dimen.min_swipe); 105 MIN_VERT = res.getDimension(R.dimen.min_vert); 106 MIN_LOCK = res.getDimension(R.dimen.min_lock); 107 } 108 } 109 110 public void setDensityScale(float densityScale) { 111 mDensityScale = densityScale; 112 } 113 114 public void setPagingTouchSlop(float pagingTouchSlop) { 115 mPagingTouchSlop = pagingTouchSlop; 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 ObjectAnimator createDismissAnimation(View v, float newPos, int duration) { 130 ObjectAnimator anim = createTranslationAnimation(v, newPos); 131 anim.setInterpolator(sDecelerateInterpolator); 132 anim.setDuration(duration); 133 return anim; 134 } 135 136 private float getPerpendicularVelocity(VelocityTracker vt) { 137 return mSwipeDirection == X ? vt.getYVelocity() : 138 vt.getXVelocity(); 139 } 140 141 private void setTranslation(View v, float translate) { 142 if (mSwipeDirection == X) { 143 v.setTranslationX(translate); 144 } else { 145 v.setTranslationY(translate); 146 } 147 } 148 149 private float getSize(View v) { 150 return mSwipeDirection == X ? v.getMeasuredWidth() : 151 v.getMeasuredHeight(); 152 } 153 154 public void setMinAlpha(float minAlpha) { 155 mMinAlpha = minAlpha; 156 } 157 158 private float getAlphaForOffset(View view) { 159 float viewSize = getSize(view); 160 final float fadeSize = ALPHA_FADE_END * viewSize; 161 float result = 1.0f; 162 float pos = view.getTranslationX(); 163 if (pos >= viewSize * ALPHA_FADE_START) { 164 result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; 165 } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { 166 result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; 167 } 168 return Math.max(mMinAlpha, result); 169 } 170 171 private float getTextAlphaForOffset(View view) { 172 float viewSize = getSize(view); 173 final float fadeSize = ALPHA_TEXT_FADE_START * viewSize; 174 float result = 1.0f; 175 float pos = view.getTranslationX(); 176 if (pos >= 0) { 177 result = 1.0f - pos / fadeSize; 178 } else if (pos < 0) { 179 result = 1.0f + pos / fadeSize; 180 } 181 return Math.max(0, result); 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 boolean onInterceptTouchEvent(MotionEvent ev) { 214 final int action = ev.getAction(); 215 switch (action) { 216 case MotionEvent.ACTION_DOWN: 217 mLastY = ev.getY(); 218 mDragging = false; 219 View view = mCallback.getChildAtPosition(ev); 220 if (view instanceof SwipeableItemView) { 221 mCurrView = (SwipeableItemView) view; 222 } 223 mVelocityTracker.clear(); 224 if (mCurrView != null) { 225 mCurrAnimView = mCurrView.getSwipeableView(); 226 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 227 mVelocityTracker.addMovement(ev); 228 mInitialTouchPosX = ev.getX(); 229 mInitialTouchPosY = ev.getY(); 230 } 231 mCallback.cancelDismissCounter(); 232 break; 233 case MotionEvent.ACTION_MOVE: 234 if (mCurrView != null) { 235 // Check the movement direction. 236 if (mLastY >= 0 && !mDragging) { 237 float currY = ev.getY(); 238 float currX = ev.getX(); 239 float deltaY = Math.abs(currY - mInitialTouchPosY); 240 float deltaX = Math.abs(currX - mInitialTouchPosX); 241 if (deltaY > mCurrView.getMinAllowScrollDistance() 242 && deltaY > (FACTOR * deltaX)) { 243 mLastY = ev.getY(); 244 mCallback.onScroll(); 245 return false; 246 } 247 } 248 mVelocityTracker.addMovement(ev); 249 float pos = ev.getX(); 250 float delta = pos - mInitialTouchPosX; 251 if (Math.abs(delta) > mPagingTouchSlop) { 252 mCallback.onBeginDrag(mCurrView.getSwipeableView()); 253 mPrevView = mCallback.getLastSwipedItem(); 254 mDragging = true; 255 mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX(); 256 mInitialTouchPosY = ev.getY(); 257 } 258 } 259 mLastY = ev.getY(); 260 break; 261 case MotionEvent.ACTION_UP: 262 case MotionEvent.ACTION_CANCEL: 263 mDragging = false; 264 mCurrView = null; 265 mCurrAnimView = null; 266 mLastY = -1; 267 break; 268 } 269 return mDragging; 270 } 271 272 /** 273 * @param view The view to be dismissed 274 * @param velocity The desired pixels/second speed at which the view should 275 * move 276 */ 277 private void dismissChild(final SwipeableItemView view, float velocity) { 278 final View animView = mCurrView.getSwipeableView(); 279 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 280 float newPos = determinePos(animView, velocity); 281 int duration = determineDuration(animView, newPos, velocity); 282 283 Utils.enableHardwareLayer(animView); 284 ObjectAnimator anim = createDismissAnimation(animView, newPos, duration); 285 anim.addListener(new AnimatorListenerAdapter() { 286 @Override 287 public void onAnimationEnd(Animator animation) { 288 mCallback.onChildDismissed(mCurrView); 289 animView.setLayerType(View.LAYER_TYPE_NONE, null); 290 } 291 }); 292 anim.addUpdateListener(new AnimatorUpdateListener() { 293 @Override 294 public void onAnimationUpdate(ValueAnimator animation) { 295 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 296 animView.setAlpha(getAlphaForOffset(animView)); 297 } 298 invalidateGlobalRegion(animView); 299 } 300 }); 301 anim.start(); 302 } 303 304 private void dismissChildren(final Collection<ConversationItemView> views, float velocity, 305 AnimatorListenerAdapter listener) { 306 final View animView = mCurrView.getSwipeableView(); 307 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(mCurrView); 308 float newPos = determinePos(animView, velocity); 309 int duration = DISMISS_ANIMATION_DURATION; 310 ArrayList<Animator> animations = new ArrayList<Animator>(); 311 ObjectAnimator anim; 312 for (final ConversationItemView view : views) { 313 Utils.enableHardwareLayer(view); 314 anim = createDismissAnimation(view, newPos, duration); 315 anim.addUpdateListener(new AnimatorUpdateListener() { 316 @Override 317 public void onAnimationUpdate(ValueAnimator animation) { 318 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 319 view.setAlpha(getAlphaForOffset(view)); 320 } 321 invalidateGlobalRegion(view); 322 } 323 }); 324 anim.addListener(new AnimatorListenerAdapter() { 325 @Override 326 public void onAnimationEnd(Animator animation) { 327 view.setLayerType(View.LAYER_TYPE_NONE, null); 328 } 329 }); 330 animations.add(anim); 331 } 332 AnimatorSet transitionSet = new AnimatorSet(); 333 transitionSet.playTogether(animations); 334 transitionSet.addListener(listener); 335 transitionSet.start(); 336 } 337 338 public void dismissChildren(ConversationItemView first, 339 final Collection<ConversationItemView> views, AnimatorListenerAdapter listener) { 340 mCurrView = first; 341 dismissChildren(views, 0f, listener); 342 } 343 344 private int determineDuration(View animView, float newPos, float velocity) { 345 int duration = MAX_ESCAPE_ANIMATION_DURATION; 346 if (velocity != 0) { 347 duration = Math 348 .min(duration, 349 (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math 350 .abs(velocity))); 351 } else { 352 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 353 } 354 return duration; 355 } 356 357 private float determinePos(View animView, float velocity) { 358 float newPos = 0; 359 if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0) 360 // if we use the Menu to dismiss an item in landscape, animate up 361 || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) { 362 newPos = -getSize(animView); 363 } else { 364 newPos = getSize(animView); 365 } 366 return newPos; 367 } 368 369 public void snapChild(final SwipeableItemView view, float velocity) { 370 final View animView = view.getSwipeableView(); 371 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 372 ObjectAnimator anim = createTranslationAnimation(animView, 0); 373 int duration = SNAP_ANIM_LEN; 374 anim.setDuration(duration); 375 anim.addUpdateListener(new AnimatorUpdateListener() { 376 @Override 377 public void onAnimationUpdate(ValueAnimator animation) { 378 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 379 animView.setAlpha(getAlphaForOffset(animView)); 380 } 381 invalidateGlobalRegion(animView); 382 } 383 }); 384 anim.addListener(new Animator.AnimatorListener() { 385 @Override 386 public void onAnimationStart(Animator animation) { 387 } 388 @Override 389 public void onAnimationEnd(Animator animation) { 390 animView.setAlpha(1.0f); 391 mCallback.onDragCancelled(mCurrView); 392 } 393 @Override 394 public void onAnimationCancel(Animator animation) { 395 } 396 @Override 397 public void onAnimationRepeat(Animator animation) { 398 } 399 }); 400 anim.start(); 401 } 402 403 public boolean onTouchEvent(MotionEvent ev) { 404 if (!mDragging) { 405 return false; 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 deltaX = ev.getX() - mInitialTouchPosX; 414 float deltaY = Math.abs(ev.getY() - mInitialTouchPosY); 415 // If the user has gone vertical and not gone horizontalish AT 416 // LEAST minBeforeLock, switch to scroll. Otherwise, cancel 417 // the swipe. 418 if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK 419 && deltaY > (FACTOR * Math.abs(deltaX))) { 420 mCallback.onScroll(); 421 return false; 422 } 423 float minDistance = MIN_SWIPE; 424 if (Math.abs(deltaX) < minDistance) { 425 // Don't start the drag until at least X distance has 426 // occurred. 427 return true; 428 } 429 // don't let items that can't be dismissed be dragged more 430 // than maxScrollDistance 431 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 432 float size = getSize(mCurrAnimView); 433 float maxScrollDistance = 0.15f * size; 434 if (Math.abs(deltaX) >= size) { 435 deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance; 436 } else { 437 deltaX = maxScrollDistance 438 * (float) Math.sin((deltaX / size) * (Math.PI / 2)); 439 } 440 } 441 setTranslation(mCurrAnimView, deltaX); 442 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 443 mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); 444 if (mPrevView != null) { 445 // Base how much the text of the prev item is faded 446 // on how far the current item has moved. 447 mPrevView.fadeOutText(getTextAlphaForOffset(mCurrAnimView)); 448 } 449 } 450 invalidateGlobalRegion(mCurrView.getSwipeableView()); 451 } 452 break; 453 case MotionEvent.ACTION_UP: 454 case MotionEvent.ACTION_CANCEL: 455 if (mCurrView != null) { 456 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 457 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 458 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 459 float velocity = getVelocity(mVelocityTracker); 460 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 461 462 // Decide whether to dismiss the current view 463 // Tweak constants below as required to prevent erroneous 464 // swipe/dismiss 465 float translation = Math.abs(mCurrAnimView.getTranslationX()); 466 float currAnimViewSize = getSize(mCurrAnimView); 467 // Long swipe = translation of .4 * width 468 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH 469 && translation > 0.4 * currAnimViewSize; 470 // Fast swipe = > escapeVelocity and translation of .1 * 471 // width 472 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) 473 && (Math.abs(velocity) > Math.abs(perpendicularVelocity)) 474 && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0) 475 && translation > 0.05 * currAnimViewSize; 476 if (LOG_SWIPE_DISMISS_VELOCITY) { 477 Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/" 478 + perpendicularVelocity + ", x: " + translation + "/" 479 + currAnimViewSize); 480 } 481 482 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) 483 && (childSwipedFastEnough || childSwipedFarEnough); 484 485 if (dismissChild) { 486 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 487 } else { 488 snapChild(mCurrView, velocity); 489 } 490 } 491 break; 492 } 493 return true; 494 } 495 496 public interface Callback { 497 View getChildAtPosition(MotionEvent ev); 498 499 void cancelDismissCounter(); 500 501 void onScroll(); 502 503 boolean canChildBeDismissed(SwipeableItemView v); 504 505 void onBeginDrag(View v); 506 507 void onChildDismissed(SwipeableItemView v); 508 509 void onDragCancelled(SwipeableItemView v); 510 511 ConversationSelectionSet getSelectionSet(); 512 513 LeaveBehindItem getLastSwipedItem(); 514 } 515} 516