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