Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 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.ObjectAnimator; |
| 21 | import android.animation.Animator.AnimatorListener; |
| 22 | import android.animation.ValueAnimator; |
| 23 | import android.animation.ValueAnimator.AnimatorUpdateListener; |
| 24 | import android.graphics.RectF; |
| 25 | import android.util.Log; |
| 26 | import android.view.animation.LinearInterpolator; |
| 27 | import android.view.MotionEvent; |
| 28 | import android.view.VelocityTracker; |
| 29 | import android.view.View; |
| 30 | |
| 31 | public class SwipeHelper { |
| 32 | static final String TAG = "com.android.systemui.SwipeHelper"; |
Daniel Sandler | 96f4818 | 2011-08-17 09:50:35 -0400 | [diff] [blame] | 33 | private static final boolean DEBUG = false; |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 34 | private static final boolean DEBUG_INVALIDATE = false; |
| 35 | private static final boolean SLOW_ANIMATIONS = false; // DEBUG; |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 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; |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 39 | |
| 40 | public static final int X = 0; |
| 41 | public static final int Y = 1; |
| 42 | |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 43 | private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec |
| 44 | private int MAX_ESCAPE_ANIMATION_DURATION = 500; // ms |
Daniel Sandler | 0761e4c | 2011-08-11 00:19:49 -0400 | [diff] [blame] | 45 | private int MAX_DISMISS_VELOCITY = 1000; // dp/sec |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 46 | private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms |
| 47 | |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 48 | public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 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; |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 61 | private View mCurrAnimView; |
| 62 | private boolean mCanCurrViewBeDimissed; |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 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 | |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 86 | private float getTranslation(View v) { |
| 87 | return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 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 | |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 119 | private float getAlphaForOffset(View view) { |
| 120 | float viewSize = getSize(view); |
| 121 | final float fadeSize = ALPHA_FADE_END * viewSize; |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 122 | float result = 1.0f; |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 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; |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 128 | } |
| 129 | return result; |
| 130 | } |
| 131 | |
Daniel Sandler | a375c94 | 2011-07-29 00:33:53 -0400 | [diff] [blame] | 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) { |
Daniel Sandler | 96f4818 | 2011-08-17 09:50:35 -0400 | [diff] [blame] | 142 | //childBounds.offset(view.getTranslationX(), view.getTranslationY()); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 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); |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 168 | mCurrAnimView = mCallback.getChildContentView(mCurrView); |
| 169 | mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 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; |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 182 | mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 183 | } |
| 184 | } |
| 185 | break; |
| 186 | case MotionEvent.ACTION_UP: |
| 187 | mDragging = false; |
| 188 | mCurrView = null; |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 189 | mCurrAnimView = null; |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 190 | break; |
| 191 | } |
| 192 | return mDragging; |
| 193 | } |
| 194 | |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 195 | public void dismissChild(final View view, float velocity) { |
| 196 | final View animView = mCallback.getChildContentView(view); |
| 197 | final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 198 | float newPos; |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 199 | if (velocity < 0 || (velocity == 0 && getTranslation(animView) < 0)) { |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 200 | newPos = -getSize(animView); |
| 201 | } else { |
| 202 | newPos = getSize(animView); |
| 203 | } |
| 204 | int duration = MAX_ESCAPE_ANIMATION_DURATION; |
| 205 | if (velocity != 0) { |
| 206 | duration = Math.min(duration, |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 207 | (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 208 | .abs(velocity))); |
| 209 | } |
| 210 | ObjectAnimator anim = createTranslationAnimation(animView, newPos); |
| 211 | anim.setInterpolator(new LinearInterpolator()); |
| 212 | anim.setDuration(duration); |
| 213 | anim.addListener(new AnimatorListener() { |
| 214 | public void onAnimationStart(Animator animation) { |
| 215 | } |
| 216 | |
| 217 | public void onAnimationRepeat(Animator animation) { |
| 218 | } |
| 219 | |
| 220 | public void onAnimationEnd(Animator animation) { |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 221 | mCallback.onChildDismissed(view); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 222 | } |
| 223 | |
| 224 | public void onAnimationCancel(Animator animation) { |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 225 | mCallback.onChildDismissed(view); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 226 | } |
| 227 | }); |
| 228 | anim.addUpdateListener(new AnimatorUpdateListener() { |
| 229 | public void onAnimationUpdate(ValueAnimator animation) { |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 230 | if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { |
| 231 | animView.setAlpha(getAlphaForOffset(animView)); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 232 | } |
| 233 | invalidateGlobalRegion(animView); |
| 234 | } |
| 235 | }); |
| 236 | anim.start(); |
| 237 | } |
| 238 | |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 239 | public void snapChild(final View view, float velocity) { |
| 240 | final View animView = mCallback.getChildContentView(view); |
| 241 | final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 242 | ObjectAnimator anim = createTranslationAnimation(animView, 0); |
| 243 | int duration = SNAP_ANIM_LEN; |
| 244 | anim.setDuration(duration); |
| 245 | anim.addUpdateListener(new AnimatorUpdateListener() { |
| 246 | public void onAnimationUpdate(ValueAnimator animation) { |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 247 | if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { |
| 248 | animView.setAlpha(getAlphaForOffset(animView)); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 249 | } |
| 250 | invalidateGlobalRegion(animView); |
| 251 | } |
| 252 | }); |
| 253 | anim.start(); |
| 254 | } |
| 255 | |
| 256 | public boolean onTouchEvent(MotionEvent ev) { |
| 257 | if (!mDragging) { |
| 258 | return false; |
| 259 | } |
| 260 | |
| 261 | mVelocityTracker.addMovement(ev); |
| 262 | final int action = ev.getAction(); |
| 263 | switch (action) { |
| 264 | case MotionEvent.ACTION_OUTSIDE: |
| 265 | case MotionEvent.ACTION_MOVE: |
| 266 | if (mCurrView != null) { |
| 267 | float delta = getPos(ev) - mInitialTouchPos; |
| 268 | // don't let items that can't be dismissed be dragged more than |
| 269 | // maxScrollDistance |
| 270 | if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 271 | float size = getSize(mCurrAnimView); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 272 | float maxScrollDistance = 0.15f * size; |
| 273 | if (Math.abs(delta) >= size) { |
| 274 | delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; |
| 275 | } else { |
| 276 | delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); |
| 277 | } |
| 278 | } |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 279 | setTranslation(mCurrAnimView, delta); |
| 280 | if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { |
| 281 | mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 282 | } |
| 283 | invalidateGlobalRegion(mCurrView); |
| 284 | } |
| 285 | break; |
| 286 | case MotionEvent.ACTION_UP: |
| 287 | case MotionEvent.ACTION_CANCEL: |
| 288 | if (mCurrView != null) { |
Daniel Sandler | 0761e4c | 2011-08-11 00:19:49 -0400 | [diff] [blame] | 289 | float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 290 | mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); |
| 291 | float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; |
| 292 | float velocity = getVelocity(mVelocityTracker); |
| 293 | float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); |
| 294 | |
| 295 | // Decide whether to dismiss the current view |
| 296 | boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 297 | Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 298 | boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && |
| 299 | (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && |
Michael Jurka | 3cd0a59 | 2011-08-16 12:40:30 -0700 | [diff] [blame^] | 300 | (velocity > 0) == (getTranslation(mCurrAnimView) > 0); |
Michael Jurka | 07d4046e | 2011-07-19 10:54:38 -0700 | [diff] [blame] | 301 | |
| 302 | boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) && |
| 303 | (childSwipedFastEnough || childSwipedFarEnough); |
| 304 | |
| 305 | if (dismissChild) { |
| 306 | // flingadingy |
| 307 | dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); |
| 308 | } else { |
| 309 | // snappity |
| 310 | snapChild(mCurrView, velocity); |
| 311 | } |
| 312 | } |
| 313 | break; |
| 314 | } |
| 315 | return true; |
| 316 | } |
| 317 | |
| 318 | public interface Callback { |
| 319 | View getChildAtPosition(MotionEvent ev); |
| 320 | |
| 321 | View getChildContentView(View v); |
| 322 | |
| 323 | boolean canChildBeDismissed(View v); |
| 324 | |
| 325 | void onBeginDrag(View v); |
| 326 | |
| 327 | void onChildDismissed(View v); |
| 328 | } |
| 329 | } |