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