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