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