SwipeHelper.java revision c6461ca5a0892d9b5a47649d49af69165e05b87f
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 mCurrAnimView = mCallback.getChildContentView(mCurrView); 169 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); 170 mVelocityTracker.clear(); 171 mVelocityTracker.addMovement(ev); 172 mInitialTouchPos = getPos(ev); 173 break; 174 case MotionEvent.ACTION_MOVE: 175 if (mCurrView != null) { 176 mVelocityTracker.addMovement(ev); 177 float pos = getPos(ev); 178 float delta = pos - mInitialTouchPos; 179 if (Math.abs(delta) > mPagingTouchSlop) { 180 mCallback.onBeginDrag(mCurrView); 181 mDragging = true; 182 mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); 183 } 184 } 185 break; 186 case MotionEvent.ACTION_UP: 187 mDragging = false; 188 mCurrView = null; 189 mCurrAnimView = null; 190 break; 191 } 192 return mDragging; 193 } 194 195 public void dismissChild(final View view, float velocity) { 196 final View animView = mCallback.getChildContentView(view); 197 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 198 float newPos; 199 200 if (velocity < 0 201 || (velocity == 0 && getTranslation(animView) < 0) 202 // if we use the Menu to dismiss an item in landscape, animate up 203 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) { 204 newPos = -getSize(animView); 205 } else { 206 newPos = getSize(animView); 207 } 208 int duration = MAX_ESCAPE_ANIMATION_DURATION; 209 if (velocity != 0) { 210 duration = Math.min(duration, 211 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math 212 .abs(velocity))); 213 } 214 ObjectAnimator anim = createTranslationAnimation(animView, newPos); 215 anim.setInterpolator(new LinearInterpolator()); 216 anim.setDuration(duration); 217 anim.addListener(new AnimatorListener() { 218 public void onAnimationStart(Animator animation) { 219 } 220 221 public void onAnimationRepeat(Animator animation) { 222 } 223 224 public void onAnimationEnd(Animator animation) { 225 mCallback.onChildDismissed(view); 226 } 227 228 public void onAnimationCancel(Animator animation) { 229 mCallback.onChildDismissed(view); 230 } 231 }); 232 anim.addUpdateListener(new AnimatorUpdateListener() { 233 public void onAnimationUpdate(ValueAnimator animation) { 234 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 235 animView.setAlpha(getAlphaForOffset(animView)); 236 } 237 invalidateGlobalRegion(animView); 238 } 239 }); 240 anim.start(); 241 } 242 243 public void snapChild(final View view, float velocity) { 244 final View animView = mCallback.getChildContentView(view); 245 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); 246 ObjectAnimator anim = createTranslationAnimation(animView, 0); 247 int duration = SNAP_ANIM_LEN; 248 anim.setDuration(duration); 249 anim.addUpdateListener(new AnimatorUpdateListener() { 250 public void onAnimationUpdate(ValueAnimator animation) { 251 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { 252 animView.setAlpha(getAlphaForOffset(animView)); 253 } 254 invalidateGlobalRegion(animView); 255 } 256 }); 257 anim.start(); 258 } 259 260 public boolean onTouchEvent(MotionEvent ev) { 261 if (!mDragging) { 262 return false; 263 } 264 265 mVelocityTracker.addMovement(ev); 266 final int action = ev.getAction(); 267 switch (action) { 268 case MotionEvent.ACTION_OUTSIDE: 269 case MotionEvent.ACTION_MOVE: 270 if (mCurrView != null) { 271 float delta = getPos(ev) - mInitialTouchPos; 272 // don't let items that can't be dismissed be dragged more than 273 // maxScrollDistance 274 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { 275 float size = getSize(mCurrAnimView); 276 float maxScrollDistance = 0.15f * size; 277 if (Math.abs(delta) >= size) { 278 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 279 } else { 280 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); 281 } 282 } 283 setTranslation(mCurrAnimView, delta); 284 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { 285 mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); 286 } 287 invalidateGlobalRegion(mCurrView); 288 } 289 break; 290 case MotionEvent.ACTION_UP: 291 case MotionEvent.ACTION_CANCEL: 292 if (mCurrView != null) { 293 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; 294 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); 295 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; 296 float velocity = getVelocity(mVelocityTracker); 297 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); 298 299 // Decide whether to dismiss the current view 300 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && 301 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); 302 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && 303 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && 304 (velocity > 0) == (getTranslation(mCurrAnimView) > 0); 305 306 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) && 307 (childSwipedFastEnough || childSwipedFarEnough); 308 309 if (dismissChild) { 310 // flingadingy 311 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); 312 } else { 313 // snappity 314 mCallback.onDragCancelled(mCurrView); 315 snapChild(mCurrView, velocity); 316 } 317 } 318 break; 319 } 320 return true; 321 } 322 323 public interface Callback { 324 View getChildAtPosition(MotionEvent ev); 325 326 View getChildContentView(View v); 327 328 boolean canChildBeDismissed(View v); 329 330 void onBeginDrag(View v); 331 332 void onChildDismissed(View v); 333 334 void onDragCancelled(View v); 335 } 336} 337