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