SwipeHelper.java revision 814086db674d8eb298541b7e601e29c5c68e2074
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 Console.log(Constants.Log.UI.TouchEvents, 182 "[SwipeHelper|interceptTouchEvent]", 183 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 184 final int action = ev.getAction(); 185 186 switch (action) { 187 case MotionEvent.ACTION_DOWN: 188 mDragging = false; 189 mCurrView = mCallback.getChildAtPosition(ev); 190 mVelocityTracker.clear(); 191 if (mCurrView != null) { 192 mRtl = isLayoutRtl(mCurrView); 193 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 194 mVelocityTracker.addMovement(ev); 195 mInitialTouchPos = getPos(ev); 196 } else { 197 mCanCurrViewBeDimissed = false; 198 } 199 break; 200 case MotionEvent.ACTION_MOVE: 201 if (mCurrView != null) { 202 mVelocityTracker.addMovement(ev); 203 float pos = getPos(ev); 204 float delta = pos - mInitialTouchPos; 205 if (Math.abs(delta) > mPagingTouchSlop) { 206 mCallback.onBeginDrag(mCurrView); 207 mDragging = true; 208 mInitialTouchPos = pos - getTranslation(mCurrView); 209 } 210 } 211 break; 212 case MotionEvent.ACTION_UP: 213 case MotionEvent.ACTION_CANCEL: 214 mDragging = false; 215 mCurrView = null; 216 break; 217 } 218 return mDragging; 219 } 220 221 /** 222 * @param view The view to be dismissed 223 * @param velocity The desired pixels/second speed at which the view should move 224 */ 225 private void dismissChild(final View view, float velocity) { 226 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 227 float newPos; 228 if (velocity < 0 229 || (velocity == 0 && getTranslation(view) < 0) 230 // if we use the Menu to dismiss an item in landscape, animate up 231 || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) { 232 newPos = -getSize(view); 233 } else { 234 newPos = getSize(view); 235 } 236 int duration = MAX_ESCAPE_ANIMATION_DURATION; 237 if (velocity != 0) { 238 duration = Math.min(duration, 239 (int) (Math.abs(newPos - getTranslation(view)) * 240 1000f / Math.abs(velocity))); 241 } else { 242 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 243 } 244 245 ValueAnimator anim = createTranslationAnimation(view, newPos); 246 anim.setInterpolator(sLinearInterpolator); 247 anim.setDuration(duration); 248 anim.addListener(new AnimatorListenerAdapter() { 249 @Override 250 public void onAnimationEnd(Animator animation) { 251 mCallback.onChildDismissed(view); 252 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 253 view.setAlpha(1.f); 254 } 255 } 256 }); 257 anim.addUpdateListener(new AnimatorUpdateListener() { 258 @Override 259 public void onAnimationUpdate(ValueAnimator animation) { 260 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 261 view.setAlpha(getAlphaForOffset(view)); 262 } 263 } 264 }); 265 anim.start(); 266 } 267 268 private void snapChild(final View view, float velocity) { 269 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 270 ValueAnimator anim = createTranslationAnimation(view, 0); 271 int duration = SNAP_ANIM_LEN; 272 anim.setDuration(duration); 273 anim.addUpdateListener(new AnimatorUpdateListener() { 274 @Override 275 public void onAnimationUpdate(ValueAnimator animation) { 276 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 277 view.setAlpha(getAlphaForOffset(view)); 278 } 279 } 280 }); 281 anim.addListener(new AnimatorListenerAdapter() { 282 @Override 283 public void onAnimationEnd(Animator animation) { 284 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 285 view.setAlpha(1.0f); 286 } 287 mCallback.onSnapBackCompleted(view); 288 } 289 }); 290 anim.start(); 291 } 292 293 public boolean onTouchEvent(MotionEvent ev) { 294 Console.log(Constants.Log.UI.TouchEvents, 295 "[SwipeHelper|touchEvent]", 296 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 297 298 if (!mDragging) { 299 if (!onInterceptTouchEvent(ev)) { 300 return mCanCurrViewBeDimissed; 301 } 302 } 303 304 mVelocityTracker.addMovement(ev); 305 final int action = ev.getAction(); 306 switch (action) { 307 case MotionEvent.ACTION_OUTSIDE: 308 case MotionEvent.ACTION_MOVE: 309 if (mCurrView != null) { 310 float delta = getPos(ev) - mInitialTouchPos; 311 setSwipeAmount(delta); 312 } 313 break; 314 case MotionEvent.ACTION_UP: 315 case MotionEvent.ACTION_CANCEL: 316 if (mCurrView != null) { 317 endSwipe(mVelocityTracker); 318 } 319 break; 320 } 321 return true; 322 } 323 324 private void setSwipeAmount(float amount) { 325 // don't let items that can't be dismissed be dragged more than 326 // maxScrollDistance 327 if (CONSTRAIN_SWIPE 328 && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) { 329 float size = getSize(mCurrView); 330 float maxScrollDistance = 0.15f * size; 331 if (Math.abs(amount) >= size) { 332 amount = amount > 0 ? maxScrollDistance : -maxScrollDistance; 333 } else { 334 amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2)); 335 } 336 } 337 setTranslation(mCurrView, amount); 338 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 339 float alpha = getAlphaForOffset(mCurrView); 340 mCurrView.setAlpha(alpha); 341 } 342 } 343 344 private boolean isValidSwipeDirection(float amount) { 345 if (mSwipeDirection == X) { 346 if (mRtl) { 347 return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart; 348 } else { 349 return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd; 350 } 351 } 352 353 // Vertical swipes are always valid. 354 return true; 355 } 356 357 private void endSwipe(VelocityTracker velocityTracker) { 358 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 359 velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 360 float velocity = getVelocity(velocityTracker); 361 float perpendicularVelocity = getPerpendicularVelocity(velocityTracker); 362 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 363 float translation = getTranslation(mCurrView); 364 // Decide whether to dismiss the current view 365 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 366 Math.abs(translation) > 0.6 * getSize(mCurrView); 367 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 368 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 369 (velocity > 0) == (translation > 0); 370 371 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) 372 && isValidSwipeDirection(translation) 373 && (childSwipedFastEnough || childSwipedFarEnough); 374 375 if (dismissChild) { 376 // flingadingy 377 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 378 } else { 379 // snappity 380 mCallback.onDragCancelled(mCurrView); 381 snapChild(mCurrView, velocity); 382 } 383 } 384 385 public interface Callback { 386 View getChildAtPosition(MotionEvent ev); 387 388 boolean canChildBeDismissed(View v); 389 390 void onBeginDrag(View v); 391 392 void onChildDismissed(View v); 393 394 void onSnapBackCompleted(View v); 395 396 void onDragCancelled(View v); 397 } 398} 399