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