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