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