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