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 private void updateAlphaFromOffset(View animView, boolean dismissable) { 158 if (FADE_OUT_DURING_SWIPE && dismissable) { 159 float alpha = getAlphaForOffset(animView); 160 if (alpha != 0f && alpha != 1f) { 161 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 162 } else { 163 animView.setLayerType(View.LAYER_TYPE_NONE, null); 164 } 165 animView.setAlpha(getAlphaForOffset(animView)); 166 } 167 invalidateGlobalRegion(animView); 168 } 169 170 // invalidate the view's own bounds all the way up the view hierarchy 171 public static void invalidateGlobalRegion(View view) { 172 invalidateGlobalRegion( 173 view, 174 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); 175 } 176 177 // invalidate a rectangle relative to the view's coordinate system all the way up the view 178 // hierarchy 179 public static void invalidateGlobalRegion(View view, RectF childBounds) { 180 //childBounds.offset(view.getTranslationX(), view.getTranslationY()); 181 if (DEBUG_INVALIDATE) 182 Log.v(TAG, "-------------"); 183 while (view.getParent() != null && view.getParent() instanceof View) { 184 view = (View) view.getParent(); 185 view.getMatrix().mapRect(childBounds); 186 view.invalidate((int) Math.floor(childBounds.left), 187 (int) Math.floor(childBounds.top), 188 (int) Math.ceil(childBounds.right), 189 (int) Math.ceil(childBounds.bottom)); 190 if (DEBUG_INVALIDATE) { 191 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) 192 + "," + (int) Math.floor(childBounds.top) 193 + "," + (int) Math.ceil(childBounds.right) 194 + "," + (int) Math.ceil(childBounds.bottom)); 195 } 196 } 197 } 198 199 public void removeLongPressCallback() { 200 if (mWatchLongPress != null) { 201 mHandler.removeCallbacks(mWatchLongPress); 202 mWatchLongPress = null; 203 } 204 } 205 206 public boolean onInterceptTouchEvent(MotionEvent ev) { 207 final int action = ev.getAction(); 208 209 switch (action) { 210 case MotionEvent.ACTION_DOWN: 211 mDragging = false; 212 mLongPressSent = false; 213 mCurrView = mCallback.getChildAtPosition(ev); 214 mVelocityTracker.clear(); 215 if (mCurrView != null) { 216 mCurrAnimView = mCallback.getChildContentView(mCurrView); 217 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 218 mVelocityTracker.addMovement(ev); 219 mInitialTouchPos = getPos(ev); 220 221 if (mLongPressListener != null) { 222 if (mWatchLongPress == null) { 223 mWatchLongPress = new Runnable() { 224 @Override 225 public void run() { 226 if (mCurrView != null && !mLongPressSent) { 227 mLongPressSent = true; 228 mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 229 mLongPressListener.onLongClick(mCurrView); 230 } 231 } 232 }; 233 } 234 mHandler.postDelayed(mWatchLongPress, mLongPressTimeout); 235 } 236 237 } 238 break; 239 240 case MotionEvent.ACTION_MOVE: 241 if (mCurrView != null && !mLongPressSent) { 242 mVelocityTracker.addMovement(ev); 243 float pos = getPos(ev); 244 float delta = pos - mInitialTouchPos; 245 if (Math.abs(delta) > mPagingTouchSlop) { 246 mCallback.onBeginDrag(mCurrView); 247 mDragging = true; 248 mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); 249 250 removeLongPressCallback(); 251 } 252 } 253 254 break; 255 256 case MotionEvent.ACTION_UP: 257 case MotionEvent.ACTION_CANCEL: 258 mDragging = false; 259 mCurrView = null; 260 mCurrAnimView = null; 261 mLongPressSent = false; 262 removeLongPressCallback(); 263 break; 264 } 265 return mDragging; 266 } 267 268 /** 269 * @param view The view to be dismissed 270 * @param velocity The desired pixels/second speed at which the view should move 271 */ 272 public void dismissChild(final View view, float velocity) { 273 final View animView = mCallback.getChildContentView(view); 274 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 275 float newPos; 276 277 if (velocity < 0 278 || (velocity == 0 && getTranslation(animView) < 0) 279 // if we use the Menu to dismiss an item in landscape, animate up 280 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) { 281 newPos = -getSize(animView); 282 } else { 283 newPos = getSize(animView); 284 } 285 int duration = MAX_ESCAPE_ANIMATION_DURATION; 286 if (velocity != 0) { 287 duration = Math.min(duration, 288 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math 289 .abs(velocity))); 290 } else { 291 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 292 } 293 294 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 295 ObjectAnimator anim = createTranslationAnimation(animView, newPos); 296 anim.setInterpolator(sLinearInterpolator); 297 anim.setDuration(duration); 298 anim.addListener(new AnimatorListenerAdapter() { 299 public void onAnimationEnd(Animator animation) { 300 mCallback.onChildDismissed(view); 301 animView.setLayerType(View.LAYER_TYPE_NONE, null); 302 } 303 }); 304 anim.addUpdateListener(new AnimatorUpdateListener() { 305 public void onAnimationUpdate(ValueAnimator animation) { 306 updateAlphaFromOffset(animView, canAnimViewBeDismissed); 307 } 308 }); 309 anim.start(); 310 } 311 312 public void snapChild(final View view, float velocity) { 313 final View animView = mCallback.getChildContentView(view); 314 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); 315 ObjectAnimator anim = createTranslationAnimation(animView, 0); 316 int duration = SNAP_ANIM_LEN; 317 anim.setDuration(duration); 318 anim.addUpdateListener(new AnimatorUpdateListener() { 319 public void onAnimationUpdate(ValueAnimator animation) { 320 updateAlphaFromOffset(animView, canAnimViewBeDismissed); 321 } 322 }); 323 anim.addListener(new AnimatorListenerAdapter() { 324 public void onAnimationEnd(Animator animator) { 325 updateAlphaFromOffset(animView, canAnimViewBeDismissed); 326 } 327 }); 328 anim.start(); 329 } 330 331 public boolean onTouchEvent(MotionEvent ev) { 332 if (mLongPressSent) { 333 return true; 334 } 335 336 if (!mDragging) { 337 // We are not doing anything, make sure the long press callback 338 // is not still ticking like a bomb waiting to go off. 339 removeLongPressCallback(); 340 return false; 341 } 342 343 mVelocityTracker.addMovement(ev); 344 final int action = ev.getAction(); 345 switch (action) { 346 case MotionEvent.ACTION_OUTSIDE: 347 case MotionEvent.ACTION_MOVE: 348 if (mCurrView != null) { 349 float delta = getPos(ev) - mInitialTouchPos; 350 // don't let items that can't be dismissed be dragged more than 351 // maxScrollDistance 352 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 353 float size = getSize(mCurrAnimView); 354 float maxScrollDistance = 0.15f * size; 355 if (Math.abs(delta) >= size) { 356 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 357 } else { 358 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); 359 } 360 } 361 setTranslation(mCurrAnimView, delta); 362 363 updateAlphaFromOffset(mCurrAnimView, mCanCurrViewBeDimissed); 364 } 365 break; 366 case MotionEvent.ACTION_UP: 367 case MotionEvent.ACTION_CANCEL: 368 if (mCurrView != null) { 369 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 370 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 371 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 372 float velocity = getVelocity(mVelocityTracker); 373 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 374 375 // Decide whether to dismiss the current view 376 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 377 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); 378 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 379 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 380 (velocity > 0) == (getTranslation(mCurrAnimView) > 0); 381 382 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) && 383 (childSwipedFastEnough || childSwipedFarEnough); 384 385 if (dismissChild) { 386 // flingadingy 387 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 388 } else { 389 // snappity 390 mCallback.onDragCancelled(mCurrView); 391 snapChild(mCurrView, velocity); 392 } 393 } 394 break; 395 } 396 return true; 397 } 398 399 public interface Callback { 400 View getChildAtPosition(MotionEvent ev); 401 402 View getChildContentView(View v); 403 404 boolean canChildBeDismissed(View v); 405 406 void onBeginDrag(View v); 407 408 void onChildDismissed(View v); 409 410 void onDragCancelled(View v); 411 } 412} 413