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