SwipeHelper.java revision 10f8139d3b8dd7cd08a2fc688285b3b74a34f0db
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.systemui.recents.views; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.ValueAnimator; 23import android.animation.ValueAnimator.AnimatorUpdateListener; 24import android.annotation.TargetApi; 25import android.os.Build; 26import android.util.DisplayMetrics; 27import android.view.MotionEvent; 28import android.view.VelocityTracker; 29import android.view.View; 30import android.view.animation.LinearInterpolator; 31import com.android.systemui.recents.Console; 32import com.android.systemui.recents.Constants; 33 34/** 35 * This class facilitates swipe to dismiss. It defines an interface to be implemented by the 36 * by the class hosting the views that need to swiped, and, using this interface, handles touch 37 * events and translates / fades / animates the view as it is dismissed. 38 */ 39public class SwipeHelper { 40 static final String TAG = "SwipeHelper"; 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 = 75; // ms 53 private int MAX_ESCAPE_ANIMATION_DURATION = 150; // 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 = 0.15f; // fraction of thumbnail width 58 // where fade starts 59 static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width 60 // beyond which alpha->0 61 private float mMinAlpha = 0f; 62 63 private float mPagingTouchSlop; 64 Callback mCallback; 65 private int mSwipeDirection; 66 private VelocityTracker mVelocityTracker; 67 68 private float mInitialTouchPos; 69 private boolean mDragging; 70 71 private View mCurrView; 72 private boolean mCanCurrViewBeDimissed; 73 private float mDensityScale; 74 75 public boolean mAllowSwipeTowardsStart = true; 76 public boolean mAllowSwipeTowardsEnd = true; 77 private boolean mRtl; 78 79 public SwipeHelper(int swipeDirection, Callback callback, float densityScale, 80 float pagingTouchSlop) { 81 mCallback = callback; 82 mSwipeDirection = swipeDirection; 83 mVelocityTracker = VelocityTracker.obtain(); 84 mDensityScale = densityScale; 85 mPagingTouchSlop = pagingTouchSlop; 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 public void cancelOngoingDrag() { 97 if (mDragging) { 98 if (mCurrView != null) { 99 mCallback.onDragCancelled(mCurrView); 100 setTranslation(mCurrView, 0); 101 mCallback.onSnapBackCompleted(mCurrView); 102 mCurrView = null; 103 } 104 mDragging = false; 105 } 106 } 107 108 public void resetTranslation(View v) { 109 setTranslation(v, 0); 110 } 111 112 private float getPos(MotionEvent ev) { 113 return mSwipeDirection == X ? ev.getX() : ev.getY(); 114 } 115 116 private float getTranslation(View v) { 117 return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); 118 } 119 120 private float getVelocity(VelocityTracker vt) { 121 return mSwipeDirection == X ? vt.getXVelocity() : 122 vt.getYVelocity(); 123 } 124 125 private ObjectAnimator createTranslationAnimation(View v, float newPos) { 126 ObjectAnimator anim = ObjectAnimator.ofFloat(v, 127 mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos); 128 return anim; 129 } 130 131 private float getPerpendicularVelocity(VelocityTracker vt) { 132 return mSwipeDirection == X ? vt.getYVelocity() : 133 vt.getXVelocity(); 134 } 135 136 private void setTranslation(View v, float translate) { 137 if (mSwipeDirection == X) { 138 v.setTranslationX(translate); 139 } else { 140 v.setTranslationY(translate); 141 } 142 } 143 144 private float getSize(View v) { 145 final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics(); 146 return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels; 147 } 148 149 public void setMinAlpha(float minAlpha) { 150 mMinAlpha = minAlpha; 151 } 152 153 float getAlphaForOffset(View view) { 154 float viewSize = getSize(view); 155 final float fadeSize = ALPHA_FADE_END * viewSize; 156 float result = 1.0f; 157 float pos = getTranslation(view); 158 if (pos >= viewSize * ALPHA_FADE_START) { 159 result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; 160 } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { 161 result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; 162 } 163 result = Math.min(result, 1.0f); 164 result = Math.max(result, 0f); 165 return Math.max(mMinAlpha, result); 166 } 167 168 /** 169 * Determines whether the given view has RTL layout. 170 */ 171 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 172 public static boolean isLayoutRtl(View view) { 173 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 174 return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); 175 } else { 176 return false; 177 } 178 } 179 180 public boolean onInterceptTouchEvent(MotionEvent ev) { 181 if (Console.Enabled) { 182 Console.log(Constants.Log.UI.TouchEvents, 183 "[SwipeHelper|interceptTouchEvent]", 184 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 185 } 186 final int action = ev.getAction(); 187 188 switch (action) { 189 case MotionEvent.ACTION_DOWN: 190 mDragging = false; 191 mCurrView = mCallback.getChildAtPosition(ev); 192 mVelocityTracker.clear(); 193 if (mCurrView != null) { 194 mRtl = isLayoutRtl(mCurrView); 195 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 196 mVelocityTracker.addMovement(ev); 197 mInitialTouchPos = getPos(ev); 198 } else { 199 mCanCurrViewBeDimissed = false; 200 } 201 break; 202 case MotionEvent.ACTION_MOVE: 203 if (mCurrView != null) { 204 mVelocityTracker.addMovement(ev); 205 float pos = getPos(ev); 206 float delta = pos - mInitialTouchPos; 207 if (Math.abs(delta) > mPagingTouchSlop) { 208 mCallback.onBeginDrag(mCurrView); 209 mDragging = true; 210 mInitialTouchPos = pos - getTranslation(mCurrView); 211 } 212 } 213 break; 214 case MotionEvent.ACTION_UP: 215 case MotionEvent.ACTION_CANCEL: 216 mDragging = false; 217 mCurrView = null; 218 break; 219 } 220 return mDragging; 221 } 222 223 /** 224 * @param view The view to be dismissed 225 * @param velocity The desired pixels/second speed at which the view should move 226 */ 227 private void dismissChild(final View view, float velocity) { 228 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 229 float newPos; 230 if (velocity < 0 231 || (velocity == 0 && getTranslation(view) < 0) 232 // if we use the Menu to dismiss an item in landscape, animate up 233 || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) { 234 newPos = -getSize(view); 235 } else { 236 newPos = getSize(view); 237 } 238 int duration = MAX_ESCAPE_ANIMATION_DURATION; 239 if (velocity != 0) { 240 duration = Math.min(duration, 241 (int) (Math.abs(newPos - getTranslation(view)) * 242 1000f / Math.abs(velocity))); 243 } else { 244 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 245 } 246 247 ValueAnimator anim = createTranslationAnimation(view, newPos); 248 anim.setInterpolator(sLinearInterpolator); 249 anim.setDuration(duration); 250 anim.addListener(new AnimatorListenerAdapter() { 251 @Override 252 public void onAnimationEnd(Animator animation) { 253 mCallback.onChildDismissed(view); 254 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 255 view.setAlpha(1.f); 256 } 257 } 258 }); 259 anim.addUpdateListener(new AnimatorUpdateListener() { 260 @Override 261 public void onAnimationUpdate(ValueAnimator animation) { 262 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 263 view.setAlpha(getAlphaForOffset(view)); 264 } 265 } 266 }); 267 anim.start(); 268 } 269 270 private void snapChild(final View view, float velocity) { 271 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 272 ValueAnimator anim = createTranslationAnimation(view, 0); 273 int duration = SNAP_ANIM_LEN; 274 anim.setDuration(duration); 275 anim.addUpdateListener(new AnimatorUpdateListener() { 276 @Override 277 public void onAnimationUpdate(ValueAnimator animation) { 278 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 279 view.setAlpha(getAlphaForOffset(view)); 280 } 281 } 282 }); 283 anim.addListener(new AnimatorListenerAdapter() { 284 @Override 285 public void onAnimationEnd(Animator animation) { 286 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 287 view.setAlpha(1.0f); 288 } 289 mCallback.onSnapBackCompleted(view); 290 } 291 }); 292 anim.start(); 293 } 294 295 public boolean onTouchEvent(MotionEvent ev) { 296 if (Console.Enabled) { 297 Console.log(Constants.Log.UI.TouchEvents, 298 "[SwipeHelper|touchEvent]", 299 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 300 } 301 302 if (!mDragging) { 303 if (!onInterceptTouchEvent(ev)) { 304 return mCanCurrViewBeDimissed; 305 } 306 } 307 308 mVelocityTracker.addMovement(ev); 309 final int action = ev.getAction(); 310 switch (action) { 311 case MotionEvent.ACTION_OUTSIDE: 312 case MotionEvent.ACTION_MOVE: 313 if (mCurrView != null) { 314 float delta = getPos(ev) - mInitialTouchPos; 315 setSwipeAmount(delta); 316 } 317 break; 318 case MotionEvent.ACTION_UP: 319 case MotionEvent.ACTION_CANCEL: 320 if (mCurrView != null) { 321 endSwipe(mVelocityTracker); 322 } 323 break; 324 } 325 return true; 326 } 327 328 private void setSwipeAmount(float amount) { 329 // don't let items that can't be dismissed be dragged more than 330 // maxScrollDistance 331 if (CONSTRAIN_SWIPE 332 && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) { 333 float size = getSize(mCurrView); 334 float maxScrollDistance = 0.15f * size; 335 if (Math.abs(amount) >= size) { 336 amount = amount > 0 ? maxScrollDistance : -maxScrollDistance; 337 } else { 338 amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2)); 339 } 340 } 341 setTranslation(mCurrView, amount); 342 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 343 float alpha = getAlphaForOffset(mCurrView); 344 mCurrView.setAlpha(alpha); 345 } 346 } 347 348 private boolean isValidSwipeDirection(float amount) { 349 if (mSwipeDirection == X) { 350 if (mRtl) { 351 return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart; 352 } else { 353 return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd; 354 } 355 } 356 357 // Vertical swipes are always valid. 358 return true; 359 } 360 361 private void endSwipe(VelocityTracker velocityTracker) { 362 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 363 velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 364 float velocity = getVelocity(velocityTracker); 365 float perpendicularVelocity = getPerpendicularVelocity(velocityTracker); 366 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 367 float translation = getTranslation(mCurrView); 368 // Decide whether to dismiss the current view 369 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 370 Math.abs(translation) > 0.6 * getSize(mCurrView); 371 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 372 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 373 (velocity > 0) == (translation > 0); 374 375 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) 376 && isValidSwipeDirection(translation) 377 && (childSwipedFastEnough || childSwipedFarEnough); 378 379 if (dismissChild) { 380 // flingadingy 381 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 382 } else { 383 // snappity 384 mCallback.onDragCancelled(mCurrView); 385 snapChild(mCurrView, velocity); 386 } 387 } 388 389 public interface Callback { 390 View getChildAtPosition(MotionEvent ev); 391 392 boolean canChildBeDismissed(View v); 393 394 void onBeginDrag(View v); 395 396 void onChildDismissed(View v); 397 398 void onSnapBackCompleted(View v); 399 400 void onDragCancelled(View v); 401 } 402} 403