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 private Runnable mRunnable = new Runnable() { 93 @Override 94 public void run() { 95 if (mDismissed) { 96 dismiss(); 97 } else { 98 cancel(); 99 } 100 resetMembers(); 101 } 102 }; 103 104 @Override 105 public void onReceive(Context context, Intent intent) { 106 post(mRunnable); 107 } 108 }; 109 private IntentFilter mScreenOffFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF); 110 111 private float mLastX; 112 SwipeDismissLayout(Context context)113 public SwipeDismissLayout(Context context) { 114 super(context); 115 init(context); 116 } 117 SwipeDismissLayout(Context context, AttributeSet attrs)118 public SwipeDismissLayout(Context context, AttributeSet attrs) { 119 super(context, attrs); 120 init(context); 121 } 122 SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle)123 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) { 124 super(context, attrs, defStyle); 125 init(context); 126 } 127 init(Context context)128 private void init(Context context) { 129 ViewConfiguration vc = ViewConfiguration.get(context); 130 mSlop = vc.getScaledTouchSlop(); 131 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); 132 TypedArray a = context.getTheme().obtainStyledAttributes( 133 com.android.internal.R.styleable.Theme); 134 mUseDynamicTranslucency = !a.hasValue( 135 com.android.internal.R.styleable.Window_windowIsTranslucent); 136 a.recycle(); 137 } 138 setOnDismissedListener(OnDismissedListener listener)139 public void setOnDismissedListener(OnDismissedListener listener) { 140 mDismissedListener = listener; 141 } 142 setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener)143 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) { 144 mProgressListener = listener; 145 } 146 147 @Override onAttachedToWindow()148 protected void onAttachedToWindow() { 149 super.onAttachedToWindow(); 150 if (getContext() instanceof Activity) { 151 getViewTreeObserver().addOnEnterAnimationCompleteListener( 152 mOnEnterAnimationCompleteListener); 153 } 154 getContext().registerReceiver(mScreenOffReceiver, mScreenOffFilter); 155 } 156 157 @Override onDetachedFromWindow()158 protected void onDetachedFromWindow() { 159 getContext().unregisterReceiver(mScreenOffReceiver); 160 if (getContext() instanceof Activity) { 161 getViewTreeObserver().removeOnEnterAnimationCompleteListener( 162 mOnEnterAnimationCompleteListener); 163 } 164 super.onDetachedFromWindow(); 165 } 166 167 @Override onInterceptTouchEvent(MotionEvent ev)168 public boolean onInterceptTouchEvent(MotionEvent ev) { 169 // offset because the view is translated during swipe 170 ev.offsetLocation(mTranslationX, 0); 171 172 switch (ev.getActionMasked()) { 173 case MotionEvent.ACTION_DOWN: 174 resetMembers(); 175 mDownX = ev.getRawX(); 176 mDownY = ev.getRawY(); 177 mActiveTouchId = ev.getPointerId(0); 178 mVelocityTracker = VelocityTracker.obtain(); 179 mVelocityTracker.addMovement(ev); 180 break; 181 182 case MotionEvent.ACTION_POINTER_DOWN: 183 int actionIndex = ev.getActionIndex(); 184 mActiveTouchId = ev.getPointerId(actionIndex); 185 break; 186 case MotionEvent.ACTION_POINTER_UP: 187 actionIndex = ev.getActionIndex(); 188 int pointerId = ev.getPointerId(actionIndex); 189 if (pointerId == mActiveTouchId) { 190 // This was our active pointer going up. Choose a new active pointer. 191 int newActionIndex = actionIndex == 0 ? 1 : 0; 192 mActiveTouchId = ev.getPointerId(newActionIndex); 193 } 194 break; 195 196 case MotionEvent.ACTION_CANCEL: 197 case MotionEvent.ACTION_UP: 198 resetMembers(); 199 break; 200 201 case MotionEvent.ACTION_MOVE: 202 if (mVelocityTracker == null || mDiscardIntercept) { 203 break; 204 } 205 206 int pointerIndex = ev.findPointerIndex(mActiveTouchId); 207 if (pointerIndex == -1) { 208 Log.e(TAG, "Invalid pointer index: ignoring."); 209 mDiscardIntercept = true; 210 break; 211 } 212 float dx = ev.getRawX() - mDownX; 213 float x = ev.getX(pointerIndex); 214 float y = ev.getY(pointerIndex); 215 if (dx != 0 && canScroll(this, false, dx, x, y)) { 216 mDiscardIntercept = true; 217 break; 218 } 219 updateSwiping(ev); 220 break; 221 } 222 223 return !mDiscardIntercept && mSwiping; 224 } 225 226 @Override onTouchEvent(MotionEvent ev)227 public boolean onTouchEvent(MotionEvent ev) { 228 if (mVelocityTracker == null) { 229 return super.onTouchEvent(ev); 230 } 231 // offset because the view is translated during swipe 232 ev.offsetLocation(mTranslationX, 0); 233 switch (ev.getActionMasked()) { 234 case MotionEvent.ACTION_UP: 235 updateDismiss(ev); 236 if (mDismissed) { 237 dismiss(); 238 } else if (mSwiping) { 239 cancel(); 240 } 241 resetMembers(); 242 break; 243 244 case MotionEvent.ACTION_CANCEL: 245 cancel(); 246 resetMembers(); 247 break; 248 249 case MotionEvent.ACTION_MOVE: 250 mVelocityTracker.addMovement(ev); 251 mLastX = ev.getRawX(); 252 updateSwiping(ev); 253 if (mSwiping) { 254 if (mUseDynamicTranslucency && getContext() instanceof Activity) { 255 ((Activity) getContext()).convertToTranslucent(null, null); 256 } 257 setProgress(ev.getRawX() - mDownX); 258 break; 259 } 260 } 261 return true; 262 } 263 setProgress(float deltaX)264 private void setProgress(float deltaX) { 265 mTranslationX = deltaX; 266 if (mProgressListener != null && deltaX >= 0) { 267 mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX); 268 } 269 } 270 dismiss()271 private void dismiss() { 272 if (mDismissedListener != null) { 273 mDismissedListener.onDismissed(this); 274 } 275 } 276 cancel()277 protected void cancel() { 278 if (mUseDynamicTranslucency && getContext() instanceof Activity) { 279 ((Activity) getContext()).convertFromTranslucent(); 280 } 281 if (mProgressListener != null) { 282 mProgressListener.onSwipeCancelled(this); 283 } 284 } 285 286 /** 287 * Resets internal members when canceling. 288 */ resetMembers()289 private void resetMembers() { 290 if (mVelocityTracker != null) { 291 mVelocityTracker.recycle(); 292 } 293 mVelocityTracker = null; 294 mTranslationX = 0; 295 mDownX = 0; 296 mDownY = 0; 297 mSwiping = false; 298 mDismissed = false; 299 mDiscardIntercept = false; 300 } 301 updateSwiping(MotionEvent ev)302 private void updateSwiping(MotionEvent ev) { 303 if (!mSwiping) { 304 float deltaX = ev.getRawX() - mDownX; 305 float deltaY = ev.getRawY() - mDownY; 306 if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) { 307 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < Math.abs(deltaX); 308 } else { 309 mSwiping = false; 310 } 311 } 312 } 313 314 private void updateDismiss(MotionEvent ev) { 315 float deltaX = ev.getRawX() - mDownX; 316 mVelocityTracker.addMovement(ev); 317 mVelocityTracker.computeCurrentVelocity(1000); 318 if (!mDismissed) { 319 320 if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) && 321 ev.getRawX() >= mLastX) { 322 mDismissed = true; 323 } 324 } 325 // Check if the user tried to undo this. 326 if (mDismissed && mSwiping) { 327 // Check if the user's finger is actually back 328 if (deltaX < (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) || 329 // or user is flinging back left 330 mVelocityTracker.getXVelocity() < -mMinFlingVelocity) { 331 mDismissed = false; 332 } 333 } 334 } 335 336 /** 337 * Tests scrollability within child views of v in the direction of dx. 338 * 339 * @param v View to test for horizontal scrollability 340 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 341 * or just its children (false). 342 * @param dx Delta scrolled in pixels. Only the sign of this is used. 343 * @param x X coordinate of the active touch point 344 * @param y Y coordinate of the active touch point 345 * @return true if child views of v can be scrolled by delta of dx. 346 */ 347 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) { 348 if (v instanceof ViewGroup) { 349 final ViewGroup group = (ViewGroup) v; 350 final int scrollX = v.getScrollX(); 351 final int scrollY = v.getScrollY(); 352 final int count = group.getChildCount(); 353 for (int i = count - 1; i >= 0; i--) { 354 final View child = group.getChildAt(i); 355 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 356 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 357 canScroll(child, true, dx, x + scrollX - child.getLeft(), 358 y + scrollY - child.getTop())) { 359 return true; 360 } 361 } 362 } 363 364 return checkV && v.canScrollHorizontally((int) -dx); 365 } 366 } 367