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