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