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