SwipeHelper.java revision a538984fcc19e7624f2650b119ede39bf1f35846
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.getSwipeableView(); 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 return false; 228 } 229 } 230 mVelocityTracker.addMovement(ev); 231 float pos = ev.getX(); 232 float delta = pos - mInitialTouchPosX; 233 if (Math.abs(delta) > mPagingTouchSlop) { 234 mCallback.onBeginDrag(mCurrView.getSwipeableView()); 235 mDragging = true; 236 mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX(); 237 mInitialTouchPosY = ev.getY(); 238 } 239 } 240 mLastY = ev.getY(); 241 break; 242 case MotionEvent.ACTION_UP: 243 case MotionEvent.ACTION_CANCEL: 244 mDragging = false; 245 mCurrView = null; 246 mCurrAnimView = null; 247 mLastY = -1; 248 break; 249 } 250 return mDragging; 251 } 252 253 /** 254 * @param view The view to be dismissed 255 * @param velocity The desired pixels/second speed at which the view should 256 * move 257 */ 258 private void dismissChild(final SwipeableItemView view, float velocity) { 259 final View animView = mCurrView.getSwipeableView(); 260 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 261 float newPos = determinePos(animView, velocity); 262 int duration = determineDuration(animView, newPos, velocity); 263 264 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 265 ObjectAnimator anim = createDismissAnimation(animView, newPos, duration); 266 anim.addListener(new AnimatorListenerAdapter() { 267 @Override 268 public void onAnimationEnd(Animator animation) { 269 mCallback.onChildDismissed(mCurrView); 270 animView.setLayerType(View.LAYER_TYPE_NONE, null); 271 } 272 }); 273 anim.addUpdateListener(new AnimatorUpdateListener() { 274 @Override 275 public void onAnimationUpdate(ValueAnimator animation) { 276 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 277 animView.setAlpha(getAlphaForOffset(animView)); 278 } 279 invalidateGlobalRegion(animView); 280 } 281 }); 282 anim.start(); 283 } 284 285 private void dismissChildren(final Collection<ConversationItemView> views, float velocity, 286 AnimatorListenerAdapter listener) { 287 final View animView = mCurrView.getSwipeableView(); 288 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(mCurrView); 289 float newPos = determinePos(animView, velocity); 290 int duration = DISMISS_ANIMATION_DURATION; 291 ArrayList<Animator> animations = new ArrayList<Animator>(); 292 ObjectAnimator anim; 293 for (final ConversationItemView view : views) { 294 view.setLayerType(View.LAYER_TYPE_HARDWARE, null); 295 anim = createDismissAnimation(view, newPos, duration); 296 anim.addUpdateListener(new AnimatorUpdateListener() { 297 @Override 298 public void onAnimationUpdate(ValueAnimator animation) { 299 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 300 view.setAlpha(getAlphaForOffset(view)); 301 } 302 invalidateGlobalRegion(view); 303 } 304 }); 305 animations.add(anim); 306 } 307 AnimatorSet transitionSet = new AnimatorSet(); 308 transitionSet.playTogether(animations); 309 transitionSet.addListener(listener); 310 transitionSet.start(); 311 } 312 313 public void dismissChildren(ConversationItemView first, 314 final Collection<ConversationItemView> views, AnimatorListenerAdapter listener) { 315 mCurrView = first; 316 dismissChildren(views, 0f, listener); 317 } 318 319 private int determineDuration(View animView, float newPos, float velocity) { 320 int duration = MAX_ESCAPE_ANIMATION_DURATION; 321 if (velocity != 0) { 322 duration = Math 323 .min(duration, 324 (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math 325 .abs(velocity))); 326 } else { 327 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 328 } 329 return duration; 330 } 331 332 private float determinePos(View animView, float velocity) { 333 float newPos = 0; 334 if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0) 335 // if we use the Menu to dismiss an item in landscape, animate up 336 || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) { 337 newPos = -getSize(animView); 338 } else { 339 newPos = getSize(animView); 340 } 341 return newPos; 342 } 343 344 public void snapChild(final SwipeableItemView view, float velocity) { 345 final View animView = view.getSwipeableView(); 346 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 347 ObjectAnimator anim = createTranslationAnimation(animView, 0); 348 int duration = SNAP_ANIM_LEN; 349 anim.setDuration(duration); 350 anim.addUpdateListener(new AnimatorUpdateListener() { 351 @Override 352 public void onAnimationUpdate(ValueAnimator animation) { 353 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 354 animView.setAlpha(getAlphaForOffset(animView)); 355 } 356 invalidateGlobalRegion(animView); 357 } 358 }); 359 anim.addListener(new Animator.AnimatorListener() { 360 @Override 361 public void onAnimationStart(Animator animation) { 362 } 363 @Override 364 public void onAnimationEnd(Animator animation) { 365 animView.setAlpha(1.0f); 366 mCallback.onDragCancelled(mCurrView); 367 } 368 @Override 369 public void onAnimationCancel(Animator animation) { 370 } 371 @Override 372 public void onAnimationRepeat(Animator animation) { 373 } 374 }); 375 anim.start(); 376 } 377 378 public boolean onTouchEvent(MotionEvent ev) { 379 if (!mDragging) { 380 return false; 381 } 382 mVelocityTracker.addMovement(ev); 383 final int action = ev.getAction(); 384 switch (action) { 385 case MotionEvent.ACTION_OUTSIDE: 386 case MotionEvent.ACTION_MOVE: 387 if (mCurrView != null) { 388 float deltaX = ev.getX() - mInitialTouchPosX; 389 float deltaY = Math.abs(ev.getY() - mInitialTouchPosY); 390 // If the user has gone vertical and not gone horizontalish AT 391 // LEAST minBeforeLock, switch to scroll. Otherwise, cancel 392 // the swipe. 393 if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK 394 && deltaY > (FACTOR * Math.abs(deltaX))) { 395 return false; 396 } 397 float minDistance = MIN_SWIPE; 398 if (Math.abs(deltaX) < minDistance) { 399 // Don't start the drag until at least X distance has 400 // occurred. 401 return true; 402 } 403 // don't let items that can't be dismissed be dragged more 404 // than maxScrollDistance 405 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 406 float size = getSize(mCurrAnimView); 407 float maxScrollDistance = 0.15f * size; 408 if (Math.abs(deltaX) >= size) { 409 deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance; 410 } else { 411 deltaX = maxScrollDistance 412 * (float) Math.sin((deltaX / size) * (Math.PI / 2)); 413 } 414 } 415 setTranslation(mCurrAnimView, deltaX); 416 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 417 mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); 418 } 419 invalidateGlobalRegion(mCurrView.getSwipeableView()); 420 } 421 break; 422 case MotionEvent.ACTION_UP: 423 case MotionEvent.ACTION_CANCEL: 424 if (mCurrView != null) { 425 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 426 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 427 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 428 float velocity = getVelocity(mVelocityTracker); 429 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 430 431 // Decide whether to dismiss the current view 432 // Tweak constants below as required to prevent erroneous 433 // swipe/dismiss 434 float translation = Math.abs(mCurrAnimView.getTranslationX()); 435 float currAnimViewSize = getSize(mCurrAnimView); 436 // Long swipe = translation of .4 * width 437 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH 438 && translation > 0.4 * currAnimViewSize; 439 // Fast swipe = > escapeVelocity and translation of .1 * 440 // width 441 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) 442 && (Math.abs(velocity) > Math.abs(perpendicularVelocity)) 443 && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0) 444 && translation > 0.05 * currAnimViewSize; 445 if (LOG_SWIPE_DISMISS_VELOCITY) { 446 Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/" 447 + perpendicularVelocity + ", x: " + translation + "/" 448 + currAnimViewSize); 449 } 450 451 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) 452 && (childSwipedFastEnough || childSwipedFarEnough); 453 454 if (dismissChild) { 455 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 456 } else { 457 snapChild(mCurrView, velocity); 458 } 459 } 460 break; 461 } 462 return true; 463 } 464 465 public interface Callback { 466 View getChildAtPosition(MotionEvent ev); 467 468 boolean canChildBeDismissed(SwipeableItemView v); 469 470 void onBeginDrag(View v); 471 472 void onChildDismissed(SwipeableItemView v); 473 474 void onDragCancelled(SwipeableItemView v); 475 476 ConversationSelectionSet getSelectionSet(); 477 } 478} 479