SwipeDismissLayout.java revision ca6234e084a71e0c968cff404620298bcd971fcc
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 TRANSLATION_MIN_ALPHA = 0.5f; 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 public SwipeDismissLayout(Context context) { 81 super(context); 82 init(context); 83 } 84 85 public SwipeDismissLayout(Context context, AttributeSet attrs) { 86 super(context, attrs); 87 init(context); 88 } 89 90 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) { 91 super(context, attrs, defStyle); 92 init(context); 93 } 94 95 private void init(Context context) { 96 ViewConfiguration vc = ViewConfiguration.get(getContext()); 97 mSlop = vc.getScaledTouchSlop(); 98 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16; 99 mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); 100 mAnimationTime = getContext().getResources().getInteger( 101 android.R.integer.config_shortAnimTime); 102 mCancelInterpolator = new DecelerateInterpolator(1.5f); 103 mDismissInterpolator = new AccelerateInterpolator(1.5f); 104 } 105 106 public void setOnDismissedListener(OnDismissedListener listener) { 107 mDismissedListener = listener; 108 } 109 110 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) { 111 mProgressListener = listener; 112 } 113 114 @Override 115 public boolean onInterceptTouchEvent(MotionEvent ev) { 116 // offset because the view is translated during swipe 117 ev.offsetLocation(mTranslationX, 0); 118 119 switch (ev.getActionMasked()) { 120 case MotionEvent.ACTION_DOWN: 121 resetMembers(); 122 mDownX = ev.getRawX(); 123 mDownY = ev.getRawY(); 124 mActiveTouchId = ev.getPointerId(0); 125 mVelocityTracker = VelocityTracker.obtain(); 126 mVelocityTracker.addMovement(ev); 127 break; 128 129 case MotionEvent.ACTION_CANCEL: 130 case MotionEvent.ACTION_UP: 131 resetMembers(); 132 break; 133 134 case MotionEvent.ACTION_MOVE: 135 if (mVelocityTracker == null || mDiscardIntercept) { 136 break; 137 } 138 139 int pointerIndex = ev.findPointerIndex(mActiveTouchId); 140 float dx = ev.getRawX() - mDownX; 141 float x = ev.getX(pointerIndex); 142 float y = ev.getY(pointerIndex); 143 if (dx != 0 && canScroll(this, false, dx, x, y)) { 144 mDiscardIntercept = true; 145 break; 146 } 147 updateSwiping(ev); 148 break; 149 } 150 151 return !mDiscardIntercept && mSwiping; 152 } 153 154 @Override 155 public boolean onTouchEvent(MotionEvent ev) { 156 if (mVelocityTracker == null) { 157 return super.onTouchEvent(ev); 158 } 159 switch (ev.getActionMasked()) { 160 case MotionEvent.ACTION_UP: 161 updateDismiss(ev); 162 if (mDismissed) { 163 dismiss(); 164 } else if (mSwiping) { 165 cancel(); 166 } 167 resetMembers(); 168 break; 169 170 case MotionEvent.ACTION_CANCEL: 171 cancel(); 172 resetMembers(); 173 break; 174 175 case MotionEvent.ACTION_MOVE: 176 mVelocityTracker.addMovement(ev); 177 updateSwiping(ev); 178 updateDismiss(ev); 179 if (mSwiping) { 180 setProgress(ev.getRawX() - mDownX); 181 break; 182 } 183 } 184 return true; 185 } 186 187 private void setProgress(float deltaX) { 188 mTranslationX = deltaX; 189 if (mProgressListener != null) { 190 mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX); 191 } 192 } 193 194 private void dismiss() { 195 if (mDismissedListener != null) { 196 mDismissedListener.onDismissed(this); 197 } 198 } 199 200 protected void cancel() { 201 if (mProgressListener != null) { 202 mProgressListener.onSwipeCancelled(this); 203 } 204 } 205 206 /** 207 * Resets internal members when canceling. 208 */ 209 private void resetMembers() { 210 if (mVelocityTracker != null) { 211 mVelocityTracker.recycle(); 212 } 213 mVelocityTracker = null; 214 mTranslationX = 0; 215 mDownX = 0; 216 mDownY = 0; 217 mSwiping = false; 218 mDismissed = false; 219 mDiscardIntercept = false; 220 } 221 222 private void updateSwiping(MotionEvent ev) { 223 if (!mSwiping) { 224 float deltaX = ev.getRawX() - mDownX; 225 float deltaY = ev.getRawY() - mDownY; 226 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2; 227 } 228 } 229 230 private void updateDismiss(MotionEvent ev) { 231 if (!mDismissed) { 232 mVelocityTracker.addMovement(ev); 233 mVelocityTracker.computeCurrentVelocity(1000); 234 235 float deltaX = ev.getRawX() - mDownX; 236 float velocityX = mVelocityTracker.getXVelocity(); 237 float absVelocityX = Math.abs(velocityX); 238 float absVelocityY = Math.abs(mVelocityTracker.getYVelocity()); 239 240 if (deltaX > getWidth() / 2) { 241 mDismissed = true; 242 } else if (absVelocityX >= mMinFlingVelocity 243 && absVelocityX <= mMaxFlingVelocity 244 && absVelocityY < absVelocityX / 2 245 && velocityX > 0 246 && deltaX > 0) { 247 mDismissed = true; 248 } 249 } 250 } 251 252 /** 253 * Tests scrollability within child views of v in the direction of dx. 254 * 255 * @param v View to test for horizontal scrollability 256 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 257 * or just its children (false). 258 * @param dx Delta scrolled in pixels. Only the sign of this is used. 259 * @param x X coordinate of the active touch point 260 * @param y Y coordinate of the active touch point 261 * @return true if child views of v can be scrolled by delta of dx. 262 */ 263 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) { 264 if (v instanceof ViewGroup) { 265 final ViewGroup group = (ViewGroup) v; 266 final int scrollX = v.getScrollX(); 267 final int scrollY = v.getScrollY(); 268 final int count = group.getChildCount(); 269 for (int i = count - 1; i >= 0; i--) { 270 final View child = group.getChildAt(i); 271 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 272 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 273 canScroll(child, true, dx, x + scrollX - child.getLeft(), 274 y + scrollY - child.getTop())) { 275 return true; 276 } 277 } 278 } 279 280 return checkV && v.canScrollHorizontally((int) -dx); 281 } 282} 283