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