SwipeDismissLayout.java revision 11b1469144d73ad0280ef9d0c2e478d180f17074
1/* 2 * Copyright (C) 2014 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.internal.widget; 18 19import android.animation.TimeInterpolator; 20import android.content.Context; 21import android.util.AttributeSet; 22import android.util.Log; 23import android.view.MotionEvent; 24import android.view.VelocityTracker; 25import android.view.View; 26import android.view.ViewConfiguration; 27import android.view.ViewGroup; 28import android.view.animation.AccelerateInterpolator; 29import android.view.animation.DecelerateInterpolator; 30import android.widget.FrameLayout; 31 32/** 33 * Special layout that finishes its activity when swiped away. 34 */ 35public class SwipeDismissLayout extends FrameLayout { 36 private static final String TAG = "SwipeDismissLayout"; 37 38 private static final float DISMISS_MIN_PROGRESS = 0.6f; 39 40 public interface OnDismissedListener { 41 void onDismissed(SwipeDismissLayout layout); 42 } 43 44 public interface OnSwipeProgressChangedListener { 45 /** 46 * Called when the layout has been swiped and the position of the window should change. 47 * 48 * @param progress A number in [-1, 1] representing how far to the left 49 * or right the window has been swiped. Negative values are swipes 50 * left, and positives are right. 51 * @param translate A number in [-w, w], where w is the width of the 52 * layout. This is equivalent to progress * layout.getWidth(). 53 */ 54 void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate); 55 56 void onSwipeCancelled(SwipeDismissLayout layout); 57 } 58 59 // Cached ViewConfiguration and system-wide constant values 60 private int mSlop; 61 private int mMinFlingVelocity; 62 private int mMaxFlingVelocity; 63 private long mAnimationTime; 64 private TimeInterpolator mCancelInterpolator; 65 private TimeInterpolator mDismissInterpolator; 66 67 // Transient properties 68 private int mActiveTouchId; 69 private float mDownX; 70 private float mDownY; 71 private boolean mSwiping; 72 private boolean mDismissed; 73 private boolean mDiscardIntercept; 74 private VelocityTracker mVelocityTracker; 75 private float mTranslationX; 76 77 private OnDismissedListener mDismissedListener; 78 private OnSwipeProgressChangedListener mProgressListener; 79 80 private float mLastX; 81 82 public SwipeDismissLayout(Context context) { 83 super(context); 84 init(context); 85 } 86 87 public SwipeDismissLayout(Context context, AttributeSet attrs) { 88 super(context, attrs); 89 init(context); 90 } 91 92 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) { 93 super(context, attrs, defStyle); 94 init(context); 95 } 96 97 private void init(Context context) { 98 ViewConfiguration vc = ViewConfiguration.get(getContext()); 99 mSlop = vc.getScaledTouchSlop(); 100 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); 101 mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); 102 mAnimationTime = getContext().getResources().getInteger( 103 android.R.integer.config_shortAnimTime); 104 mCancelInterpolator = new DecelerateInterpolator(1.5f); 105 mDismissInterpolator = new AccelerateInterpolator(1.5f); 106 } 107 108 public void setOnDismissedListener(OnDismissedListener listener) { 109 mDismissedListener = listener; 110 } 111 112 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) { 113 mProgressListener = listener; 114 } 115 116 @Override 117 public boolean onInterceptTouchEvent(MotionEvent ev) { 118 // offset because the view is translated during swipe 119 ev.offsetLocation(mTranslationX, 0); 120 121 switch (ev.getActionMasked()) { 122 case MotionEvent.ACTION_DOWN: 123 resetMembers(); 124 mDownX = ev.getRawX(); 125 mDownY = ev.getRawY(); 126 mActiveTouchId = ev.getPointerId(0); 127 mVelocityTracker = VelocityTracker.obtain(); 128 mVelocityTracker.addMovement(ev); 129 break; 130 131 case MotionEvent.ACTION_POINTER_DOWN: 132 int actionIndex = ev.getActionIndex(); 133 mActiveTouchId = ev.getPointerId(actionIndex); 134 break; 135 case MotionEvent.ACTION_POINTER_UP: 136 actionIndex = ev.getActionIndex(); 137 int pointerId = ev.getPointerId(actionIndex); 138 if (pointerId == mActiveTouchId) { 139 // This was our active pointer going up. Choose a new active pointer. 140 int newActionIndex = actionIndex == 0 ? 1 : 0; 141 mActiveTouchId = ev.getPointerId(newActionIndex); 142 } 143 break; 144 145 case MotionEvent.ACTION_CANCEL: 146 case MotionEvent.ACTION_UP: 147 resetMembers(); 148 break; 149 150 case MotionEvent.ACTION_MOVE: 151 if (mVelocityTracker == null || mDiscardIntercept) { 152 break; 153 } 154 155 int pointerIndex = ev.findPointerIndex(mActiveTouchId); 156 if (pointerIndex == -1) { 157 Log.e(TAG, "Invalid pointer index: ignoring."); 158 mDiscardIntercept = true; 159 break; 160 } 161 float dx = ev.getRawX() - mDownX; 162 float x = ev.getX(pointerIndex); 163 float y = ev.getY(pointerIndex); 164 if (dx != 0 && canScroll(this, false, dx, x, y)) { 165 mDiscardIntercept = true; 166 break; 167 } 168 updateSwiping(ev); 169 break; 170 } 171 172 return !mDiscardIntercept && mSwiping; 173 } 174 175 @Override 176 public boolean onTouchEvent(MotionEvent ev) { 177 if (mVelocityTracker == null) { 178 return super.onTouchEvent(ev); 179 } 180 switch (ev.getActionMasked()) { 181 case MotionEvent.ACTION_UP: 182 updateDismiss(ev); 183 if (mDismissed) { 184 dismiss(); 185 } else if (mSwiping) { 186 cancel(); 187 } 188 resetMembers(); 189 break; 190 191 case MotionEvent.ACTION_CANCEL: 192 cancel(); 193 resetMembers(); 194 break; 195 196 case MotionEvent.ACTION_MOVE: 197 mVelocityTracker.addMovement(ev); 198 mLastX = ev.getRawX(); 199 updateSwiping(ev); 200 if (mSwiping) { 201 setProgress(ev.getRawX() - mDownX); 202 break; 203 } 204 } 205 return true; 206 } 207 208 private void setProgress(float deltaX) { 209 mTranslationX = deltaX; 210 if (mProgressListener != null) { 211 mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX); 212 } 213 } 214 215 private void dismiss() { 216 if (mDismissedListener != null) { 217 mDismissedListener.onDismissed(this); 218 } 219 } 220 221 protected void cancel() { 222 if (mProgressListener != null) { 223 mProgressListener.onSwipeCancelled(this); 224 } 225 } 226 227 /** 228 * Resets internal members when canceling. 229 */ 230 private void resetMembers() { 231 if (mVelocityTracker != null) { 232 mVelocityTracker.recycle(); 233 } 234 mVelocityTracker = null; 235 mTranslationX = 0; 236 mDownX = 0; 237 mDownY = 0; 238 mSwiping = false; 239 mDismissed = false; 240 mDiscardIntercept = false; 241 } 242 243 private void updateSwiping(MotionEvent ev) { 244 if (!mSwiping) { 245 float deltaX = ev.getRawX() - mDownX; 246 float deltaY = ev.getRawY() - mDownY; 247 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2; 248 } 249 } 250 251 private void updateDismiss(MotionEvent ev) { 252 float deltaX = ev.getRawX() - mDownX; 253 if (!mDismissed) { 254 mVelocityTracker.addMovement(ev); 255 mVelocityTracker.computeCurrentVelocity(1000); 256 257 float velocityX = mVelocityTracker.getXVelocity(); 258 float absVelocityX = Math.abs(velocityX); 259 float absVelocityY = Math.abs(mVelocityTracker.getYVelocity()); 260 261 if (deltaX > (getWidth() * DISMISS_MIN_PROGRESS) && 262 absVelocityX < mMinFlingVelocity && 263 ev.getRawX() >= mLastX) { 264 mDismissed = true; 265 } 266 } 267 // Check if the user tried to undo this. 268 if (mDismissed && mSwiping) { 269 // Check if the user's finger is actually back 270 if (deltaX < (getWidth() * DISMISS_MIN_PROGRESS)) { 271 mDismissed = false; 272 } 273 } 274 } 275 276 /** 277 * Tests scrollability within child views of v in the direction of dx. 278 * 279 * @param v View to test for horizontal scrollability 280 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 281 * or just its children (false). 282 * @param dx Delta scrolled in pixels. Only the sign of this is used. 283 * @param x X coordinate of the active touch point 284 * @param y Y coordinate of the active touch point 285 * @return true if child views of v can be scrolled by delta of dx. 286 */ 287 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) { 288 if (v instanceof ViewGroup) { 289 final ViewGroup group = (ViewGroup) v; 290 final int scrollX = v.getScrollX(); 291 final int scrollY = v.getScrollY(); 292 final int count = group.getChildCount(); 293 for (int i = count - 1; i >= 0; i--) { 294 final View child = group.getChildAt(i); 295 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 296 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 297 canScroll(child, true, dx, x + scrollX - child.getLeft(), 298 y + scrollY - child.getTop())) { 299 return true; 300 } 301 } 302 } 303 304 return checkV && v.canScrollHorizontally((int) -dx); 305 } 306} 307