SwipeHelper.java revision 469e96e206f8df44b32ce393f8d19f0cae730030
1/* 2 * Copyright (C) 2011 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; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.Animator.AnimatorListener; 23import android.animation.ValueAnimator; 24import android.animation.ValueAnimator.AnimatorUpdateListener; 25import android.graphics.RectF; 26import android.os.Handler; 27import android.util.Log; 28import android.view.accessibility.AccessibilityEvent; 29import android.view.animation.LinearInterpolator; 30import android.view.MotionEvent; 31import android.view.VelocityTracker; 32import android.view.View; 33import android.view.ViewConfiguration; 34 35public class SwipeHelper implements Gefingerpoken { 36 static final String TAG = "com.android.systemui.SwipeHelper"; 37 private static final boolean DEBUG = false; 38 private static final boolean DEBUG_INVALIDATE = false; 39 private static final boolean SLOW_ANIMATIONS = false; // DEBUG; 40 private static final boolean CONSTRAIN_SWIPE = true; 41 private static final boolean FADE_OUT_DURING_SWIPE = true; 42 private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; 43 44 public static final int X = 0; 45 public static final int Y = 1; 46 47 private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); 48 49 private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec 50 private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms 51 private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms 52 private int MAX_DISMISS_VELOCITY = 2000; // dp/sec 53 private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms 54 55 public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width 56 // where fade starts 57 static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width 58 // beyond which alpha->0 59 private float mMinAlpha = 0f; 60 61 private float mPagingTouchSlop; 62 private Callback mCallback; 63 private Handler mHandler; 64 private int mSwipeDirection; 65 private VelocityTracker mVelocityTracker; 66 67 private float mInitialTouchPos; 68 private boolean mDragging; 69 private View mCurrView; 70 private View mCurrAnimView; 71 private boolean mCanCurrViewBeDimissed; 72 private float mDensityScale; 73 74 private boolean mLongPressSent; 75 private View.OnLongClickListener mLongPressListener; 76 private Runnable mWatchLongPress; 77 private long mLongPressTimeout; 78 79 public SwipeHelper(int swipeDirection, Callback callback, float densityScale, 80 float pagingTouchSlop) { 81 mCallback = callback; 82 mHandler = new Handler(); 83 mSwipeDirection = swipeDirection; 84 mVelocityTracker = VelocityTracker.obtain(); 85 mDensityScale = densityScale; 86 mPagingTouchSlop = pagingTouchSlop; 87 88 mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press! 89 } 90 91 public void setLongPressListener(View.OnLongClickListener listener) { 92 mLongPressListener = listener; 93 } 94 95 public void setDensityScale(float densityScale) { 96 mDensityScale = densityScale; 97 } 98 99 public void setPagingTouchSlop(float pagingTouchSlop) { 100 mPagingTouchSlop = pagingTouchSlop; 101 } 102 103 private float getPos(MotionEvent ev) { 104 return mSwipeDirection == X ? ev.getX() : ev.getY(); 105 } 106 107 private float getTranslation(View v) { 108 return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); 109 } 110 111 private float getVelocity(VelocityTracker vt) { 112 return mSwipeDirection == X ? vt.getXVelocity() : 113 vt.getYVelocity(); 114 } 115 116 private ObjectAnimator createTranslationAnimation(View v, float newPos) { 117 ObjectAnimator anim = ObjectAnimator.ofFloat(v, 118 mSwipeDirection == X ? "translationX" : "translationY", newPos); 119 return anim; 120 } 121 122 private float getPerpendicularVelocity(VelocityTracker vt) { 123 return mSwipeDirection == X ? vt.getYVelocity() : 124 vt.getXVelocity(); 125 } 126 127 private void setTranslation(View v, float translate) { 128 if (mSwipeDirection == X) { 129 v.setTranslationX(translate); 130 } else { 131 v.setTranslationY(translate); 132 } 133 } 134 135 private float getSize(View v) { 136 return mSwipeDirection == X ? v.getMeasuredWidth() : 137 v.getMeasuredHeight(); 138 } 139 140 public void setMinAlpha(float minAlpha) { 141 mMinAlpha = minAlpha; 142 } 143 144 private float getAlphaForOffset(View view) { 145 float viewSize = getSize(view); 146 final float fadeSize = ALPHA_FADE_END * viewSize; 147 float result = 1.0f; 148 float pos = getTranslation(view); 149 if (pos >= viewSize * ALPHA_FADE_START) { 150 result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; 151 } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { 152 result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; 153 } 154 return Math.max(mMinAlpha, result); 155 } 156 157 // invalidate the view's own bounds all the way up the view hierarchy 158 public static void invalidateGlobalRegion(View view) { 159 invalidateGlobalRegion( 160 view, 161 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); 162 } 163 164 // invalidate a rectangle relative to the view's coordinate system all the way up the view 165 // hierarchy 166 public static void invalidateGlobalRegion(View view, RectF childBounds) { 167 //childBounds.offset(view.getTranslationX(), view.getTranslationY()); 168 if (DEBUG_INVALIDATE) 169 Log.v(TAG, "-------------"); 170 while (view.getParent() != null && view.getParent() instanceof View) { 171 view = (View) view.getParent(); 172 view.getMatrix().mapRect(childBounds); 173 view.invalidate((int) Math.floor(childBounds.left), 174 (int) Math.floor(childBounds.top), 175 (int) Math.ceil(childBounds.right), 176 (int) Math.ceil(childBounds.bottom)); 177 if (DEBUG_INVALIDATE) { 178 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) 179 + "," + (int) Math.floor(childBounds.top) 180 + "," + (int) Math.ceil(childBounds.right) 181 + "," + (int) Math.ceil(childBounds.bottom)); 182 } 183 } 184 } 185 186 public void removeLongPressCallback() { 187 if (mWatchLongPress != null) { 188 mHandler.removeCallbacks(mWatchLongPress); 189 } 190 } 191 192 public boolean onInterceptTouchEvent(MotionEvent ev) { 193 final int action = ev.getAction(); 194 195 switch (action) { 196 case MotionEvent.ACTION_DOWN: 197 mDragging = false; 198 mLongPressSent = false; 199 mCurrView = mCallback.getChildAtPosition(ev); 200 mVelocityTracker.clear(); 201 if (mCurrView != null) { 202 mCurrAnimView = mCallback.getChildContentView(mCurrView); 203 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 204 mVelocityTracker.addMovement(ev); 205 mInitialTouchPos = getPos(ev); 206 207 if (mLongPressListener != null) { 208 if (mWatchLongPress == null) { 209 mWatchLongPress = new Runnable() { 210 @Override 211 public void run() { 212 if (mCurrView != null && !mLongPressSent) { 213 mLongPressSent = true; 214 mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 215 mLongPressListener.onLongClick(mCurrView); 216 } 217 } 218 }; 219 } 220 mHandler.postDelayed(mWatchLongPress, mLongPressTimeout); 221 } 222 223 } 224 break; 225 226 case MotionEvent.ACTION_MOVE: 227 if (mCurrView != null && !mLongPressSent) { 228 mVelocityTracker.addMovement(ev); 229 float pos = getPos(ev); 230 float delta = pos - mInitialTouchPos; 231 if (Math.abs(delta) > mPagingTouchSlop) { 232 mCallback.onBeginDrag(mCurrView); 233 mDragging = true; 234 mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); 235 236 removeLongPressCallback(); 237 } 238 } 239 240 break; 241 242 case MotionEvent.ACTION_UP: 243 case MotionEvent.ACTION_CANCEL: 244 mDragging = false; 245 mCurrView = null; 246 mCurrAnimView = null; 247 mLongPressSent = false; 248 break; 249 } 250 return mDragging; 251 } 252 253 /** 254 * @param view The view to be dismissed 255 * @param velocity The desired pixels/second speed at which the view should move 256 */ 257 public void dismissChild(final View view, float velocity) { 258 final View animView = mCallback.getChildContentView(view); 259 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 260 float newPos; 261 262 if (velocity < 0 263 || (velocity == 0 && getTranslation(animView) < 0) 264 // if we use the Menu to dismiss an item in landscape, animate up 265 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) { 266 newPos = -getSize(animView); 267 } else { 268 newPos = getSize(animView); 269 } 270 int duration = MAX_ESCAPE_ANIMATION_DURATION; 271 if (velocity != 0) { 272 duration = Math.min(duration, 273 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math 274 .abs(velocity))); 275 } else { 276 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 277 } 278 279 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 280 ObjectAnimator anim = createTranslationAnimation(animView, newPos); 281 anim.setInterpolator(sLinearInterpolator); 282 anim.setDuration(duration); 283 anim.addListener(new AnimatorListenerAdapter() { 284 public void onAnimationEnd(Animator animation) { 285 mCallback.onChildDismissed(view); 286 animView.setLayerType(View.LAYER_TYPE_NONE, null); 287 } 288 }); 289 anim.addUpdateListener(new AnimatorUpdateListener() { 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 public void snapChild(final View view, float velocity) { 301 final View animView = mCallback.getChildContentView(view); 302 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); 303 ObjectAnimator anim = createTranslationAnimation(animView, 0); 304 int duration = SNAP_ANIM_LEN; 305 anim.setDuration(duration); 306 anim.addUpdateListener(new AnimatorUpdateListener() { 307 public void onAnimationUpdate(ValueAnimator animation) { 308 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 309 animView.setAlpha(getAlphaForOffset(animView)); 310 } 311 invalidateGlobalRegion(animView); 312 } 313 }); 314 anim.start(); 315 } 316 317 public boolean onTouchEvent(MotionEvent ev) { 318 if (mLongPressSent) { 319 return true; 320 } 321 322 if (!mDragging) { 323 return false; 324 } 325 326 mVelocityTracker.addMovement(ev); 327 final int action = ev.getAction(); 328 switch (action) { 329 case MotionEvent.ACTION_OUTSIDE: 330 case MotionEvent.ACTION_MOVE: 331 if (mCurrView != null) { 332 float delta = getPos(ev) - mInitialTouchPos; 333 // don't let items that can't be dismissed be dragged more than 334 // maxScrollDistance 335 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 336 float size = getSize(mCurrAnimView); 337 float maxScrollDistance = 0.15f * size; 338 if (Math.abs(delta) >= size) { 339 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 340 } else { 341 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); 342 } 343 } 344 setTranslation(mCurrAnimView, delta); 345 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 346 mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); 347 } 348 invalidateGlobalRegion(mCurrView); 349 } 350 break; 351 case MotionEvent.ACTION_UP: 352 case MotionEvent.ACTION_CANCEL: 353 if (mCurrView != null) { 354 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 355 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 356 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 357 float velocity = getVelocity(mVelocityTracker); 358 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 359 360 // Decide whether to dismiss the current view 361 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 362 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); 363 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 364 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 365 (velocity > 0) == (getTranslation(mCurrAnimView) > 0); 366 367 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) && 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 break; 380 } 381 return true; 382 } 383 384 public interface Callback { 385 View getChildAtPosition(MotionEvent ev); 386 387 View getChildContentView(View v); 388 389 boolean canChildBeDismissed(View v); 390 391 void onBeginDrag(View v); 392 393 void onChildDismissed(View v); 394 395 void onDragCancelled(View v); 396 } 397} 398