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.Animator.AnimatorListener; 23 import android.animation.ValueAnimator; 24 import android.animation.ValueAnimator.AnimatorUpdateListener; 25 import android.graphics.RectF; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.view.accessibility.AccessibilityEvent; 29 import android.view.animation.LinearInterpolator; 30 import android.view.MotionEvent; 31 import android.view.VelocityTracker; 32 import android.view.View; 33 import android.view.ViewConfiguration; 34 35 public 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 mWatchLongPress = null; 190 } 191 } 192 193 public boolean onInterceptTouchEvent(MotionEvent ev) { 194 final int action = ev.getAction(); 195 196 switch (action) { 197 case MotionEvent.ACTION_DOWN: 198 mDragging = false; 199 mLongPressSent = false; 200 mCurrView = mCallback.getChildAtPosition(ev); 201 mVelocityTracker.clear(); 202 if (mCurrView != null) { 203 mCurrAnimView = mCallback.getChildContentView(mCurrView); 204 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 205 mVelocityTracker.addMovement(ev); 206 mInitialTouchPos = getPos(ev); 207 208 if (mLongPressListener != null) { 209 if (mWatchLongPress == null) { 210 mWatchLongPress = new Runnable() { 211 @Override 212 public void run() { 213 if (mCurrView != null && !mLongPressSent) { 214 mLongPressSent = true; 215 mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 216 mLongPressListener.onLongClick(mCurrView); 217 } 218 } 219 }; 220 } 221 mHandler.postDelayed(mWatchLongPress, mLongPressTimeout); 222 } 223 224 } 225 break; 226 227 case MotionEvent.ACTION_MOVE: 228 if (mCurrView != null && !mLongPressSent) { 229 mVelocityTracker.addMovement(ev); 230 float pos = getPos(ev); 231 float delta = pos - mInitialTouchPos; 232 if (Math.abs(delta) > mPagingTouchSlop) { 233 mCallback.onBeginDrag(mCurrView); 234 mDragging = true; 235 mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); 236 237 removeLongPressCallback(); 238 } 239 } 240 241 break; 242 243 case MotionEvent.ACTION_UP: 244 case MotionEvent.ACTION_CANCEL: 245 mDragging = false; 246 mCurrView = null; 247 mCurrAnimView = null; 248 mLongPressSent = false; 249 removeLongPressCallback(); 250 break; 251 } 252 return mDragging; 253 } 254 255 /** 256 * @param view The view to be dismissed 257 * @param velocity The desired pixels/second speed at which the view should move 258 */ 259 public void dismissChild(final View view, float velocity) { 260 final View animView = mCallback.getChildContentView(view); 261 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 262 float newPos; 263 264 if (velocity < 0 265 || (velocity == 0 && getTranslation(animView) < 0) 266 // if we use the Menu to dismiss an item in landscape, animate up 267 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) { 268 newPos = -getSize(animView); 269 } else { 270 newPos = getSize(animView); 271 } 272 int duration = MAX_ESCAPE_ANIMATION_DURATION; 273 if (velocity != 0) { 274 duration = Math.min(duration, 275 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math 276 .abs(velocity))); 277 } else { 278 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 279 } 280 281 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 282 ObjectAnimator anim = createTranslationAnimation(animView, newPos); 283 anim.setInterpolator(sLinearInterpolator); 284 anim.setDuration(duration); 285 anim.addListener(new AnimatorListenerAdapter() { 286 public void onAnimationEnd(Animator animation) { 287 mCallback.onChildDismissed(view); 288 animView.setLayerType(View.LAYER_TYPE_NONE, null); 289 } 290 }); 291 anim.addUpdateListener(new AnimatorUpdateListener() { 292 public void onAnimationUpdate(ValueAnimator animation) { 293 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 294 animView.setAlpha(getAlphaForOffset(animView)); 295 } 296 invalidateGlobalRegion(animView); 297 } 298 }); 299 anim.start(); 300 } 301 302 public void snapChild(final View view, float velocity) { 303 final View animView = mCallback.getChildContentView(view); 304 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); 305 ObjectAnimator anim = createTranslationAnimation(animView, 0); 306 int duration = SNAP_ANIM_LEN; 307 anim.setDuration(duration); 308 anim.addUpdateListener(new AnimatorUpdateListener() { 309 public void onAnimationUpdate(ValueAnimator animation) { 310 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 311 animView.setAlpha(getAlphaForOffset(animView)); 312 } 313 invalidateGlobalRegion(animView); 314 } 315 }); 316 anim.start(); 317 } 318 319 public boolean onTouchEvent(MotionEvent ev) { 320 if (mLongPressSent) { 321 return true; 322 } 323 324 if (!mDragging) { 325 // We are not doing anything, make sure the long press callback 326 // is not still ticking like a bomb waiting to go off. 327 removeLongPressCallback(); 328 return false; 329 } 330 331 mVelocityTracker.addMovement(ev); 332 final int action = ev.getAction(); 333 switch (action) { 334 case MotionEvent.ACTION_OUTSIDE: 335 case MotionEvent.ACTION_MOVE: 336 if (mCurrView != null) { 337 float delta = getPos(ev) - mInitialTouchPos; 338 // don't let items that can't be dismissed be dragged more than 339 // maxScrollDistance 340 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 341 float size = getSize(mCurrAnimView); 342 float maxScrollDistance = 0.15f * size; 343 if (Math.abs(delta) >= size) { 344 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 345 } else { 346 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); 347 } 348 } 349 setTranslation(mCurrAnimView, delta); 350 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 351 mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); 352 } 353 invalidateGlobalRegion(mCurrView); 354 } 355 break; 356 case MotionEvent.ACTION_UP: 357 case MotionEvent.ACTION_CANCEL: 358 if (mCurrView != null) { 359 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 360 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 361 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 362 float velocity = getVelocity(mVelocityTracker); 363 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 364 365 // Decide whether to dismiss the current view 366 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 367 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); 368 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 369 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 370 (velocity > 0) == (getTranslation(mCurrAnimView) > 0); 371 372 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) && 373 (childSwipedFastEnough || childSwipedFarEnough); 374 375 if (dismissChild) { 376 // flingadingy 377 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 378 } else { 379 // snappity 380 mCallback.onDragCancelled(mCurrView); 381 snapChild(mCurrView, velocity); 382 } 383 } 384 break; 385 } 386 return true; 387 } 388 389 public interface Callback { 390 View getChildAtPosition(MotionEvent ev); 391 392 View getChildContentView(View v); 393 394 boolean canChildBeDismissed(View v); 395 396 void onBeginDrag(View v); 397 398 void onChildDismissed(View v); 399 400 void onDragCancelled(View v); 401 } 402 } 403