2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.internal.widget;
19 import android.animation.Animator;
20 import android.animation.TimeInterpolator;
21 import android.animation.ValueAnimator;
22 import android.animation.ValueAnimator.AnimatorUpdateListener;
23 import android.app.Activity;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.ContextWrapper;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.res.TypedArray;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.MotionEvent;
33 import android.view.VelocityTracker;
34 import android.view.View;
35 import android.view.ViewConfiguration;
36 import android.view.ViewGroup;
37 import android.view.animation.DecelerateInterpolator;
38 import android.widget.FrameLayout;
41 * Special layout that finishes its activity when swiped away.
43 public class SwipeDismissLayout extends FrameLayout {
44 private static final String TAG = "SwipeDismissLayout";
46 private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f;
48 public interface OnDismissedListener {
49 void onDismissed(SwipeDismissLayout layout);
52 public interface OnSwipeProgressChangedListener {
54 * Called when the layout has been swiped and the position of the window should change.
56 * @param alpha A number in [0, 1] representing what the alpha transparency of the window
58 * @param translate A number in [0, w], where w is the width of the
59 * layout. This is equivalent to progress * layout.getWidth().
61 void onSwipeProgressChanged(SwipeDismissLayout layout, float alpha, float translate);
63 void onSwipeCancelled(SwipeDismissLayout layout);
66 private boolean mIsWindowNativelyTranslucent;
68 // Cached ViewConfiguration and system-wide constant values
70 private int mMinFlingVelocity;
72 // Transient properties
73 private int mActiveTouchId;
76 private boolean mSwiping;
77 private boolean mDismissed;
78 private boolean mDiscardIntercept;
79 private VelocityTracker mVelocityTracker;
80 private float mTranslationX;
81 private boolean mBlockGesture = false;
82 private boolean mActivityTranslucencyConverted = false;
84 private final DismissAnimator mDismissAnimator = new DismissAnimator();
86 private OnDismissedListener mDismissedListener;
87 private OnSwipeProgressChangedListener mProgressListener;
88 private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
89 private Runnable mRunnable = new Runnable() {
102 public void onReceive(Context context, Intent intent) {
106 private IntentFilter mScreenOffFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
108 private float mLastX;
110 private boolean mDismissable = true;
112 public SwipeDismissLayout(Context context) {
117 public SwipeDismissLayout(Context context, AttributeSet attrs) {
118 super(context, attrs);
122 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
123 super(context, attrs, defStyle);
127 private void init(Context context) {
128 ViewConfiguration vc = ViewConfiguration.get(context);
129 mSlop = vc.getScaledTouchSlop();
130 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
131 TypedArray a = context.getTheme().obtainStyledAttributes(
132 com.android.internal.R.styleable.Theme);
133 mIsWindowNativelyTranslucent = a.getBoolean(
134 com.android.internal.R.styleable.Window_windowIsTranslucent, false);
138 public void setOnDismissedListener(OnDismissedListener listener) {
139 mDismissedListener = listener;
142 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
143 mProgressListener = listener;
147 protected void onAttachedToWindow() {
148 super.onAttachedToWindow();
149 getContext().registerReceiver(mScreenOffReceiver, mScreenOffFilter);
153 protected void onDetachedFromWindow() {
154 getContext().unregisterReceiver(mScreenOffReceiver);
155 super.onDetachedFromWindow();
159 public boolean onInterceptTouchEvent(MotionEvent ev) {
165 return super.onInterceptTouchEvent(ev);
168 // offset because the view is translated during swipe
169 ev.offsetLocation(mTranslationX, 0);
171 switch (ev.getActionMasked()) {
172 case MotionEvent.ACTION_DOWN:
174 mDownX = ev.getRawX();
175 mDownY = ev.getRawY();
176 mActiveTouchId = ev.getPointerId(0);
177 mVelocityTracker = VelocityTracker.obtain();
178 mVelocityTracker.addMovement(ev);
181 case MotionEvent.ACTION_POINTER_DOWN:
182 int actionIndex = ev.getActionIndex();
183 mActiveTouchId = ev.getPointerId(actionIndex);
185 case MotionEvent.ACTION_POINTER_UP:
186 actionIndex = ev.getActionIndex();
187 int pointerId = ev.getPointerId(actionIndex);
188 if (pointerId == mActiveTouchId) {
189 // This was our active pointer going up. Choose a new active pointer.
190 int newActionIndex = actionIndex == 0 ? 1 : 0;
191 mActiveTouchId = ev.getPointerId(newActionIndex);
195 case MotionEvent.ACTION_CANCEL:
196 case MotionEvent.ACTION_UP:
200 case MotionEvent.ACTION_MOVE:
201 if (mVelocityTracker == null || mDiscardIntercept) {
205 int pointerIndex = ev.findPointerIndex(mActiveTouchId);
206 if (pointerIndex == -1) {
207 Log.e(TAG, "Invalid pointer index: ignoring.");
208 mDiscardIntercept = true;
211 float dx = ev.getRawX() - mDownX;
212 float x = ev.getX(pointerIndex);
213 float y = ev.getY(pointerIndex);
214 if (dx != 0 && canScroll(this, false, dx, x, y)) {
215 mDiscardIntercept = true;
222 return !mDiscardIntercept && mSwiping;
226 public boolean onTouchEvent(MotionEvent ev) {
231 if (mVelocityTracker == null || !mDismissable) {
232 return super.onTouchEvent(ev);
234 // offset because the view is translated during swipe
235 ev.offsetLocation(mTranslationX, 0);
236 switch (ev.getActionMasked()) {
237 case MotionEvent.ACTION_UP:
240 mDismissAnimator.animateDismissal(ev.getRawX() - mDownX);
241 } else if (mSwiping) {
242 mDismissAnimator.animateRecovery(ev.getRawX() - mDownX);
247 case MotionEvent.ACTION_CANCEL:
252 case MotionEvent.ACTION_MOVE:
253 mVelocityTracker.addMovement(ev);
254 mLastX = ev.getRawX();
257 setProgress(ev.getRawX() - mDownX);
264 private void setProgress(float deltaX) {
265 mTranslationX = deltaX;
266 if (mProgressListener != null && deltaX >= 0) {
267 mProgressListener.onSwipeProgressChanged(
268 this, progressToAlpha(deltaX / getWidth()), deltaX);
272 private void dismiss() {
273 if (mDismissedListener != null) {
274 mDismissedListener.onDismissed(this);
278 protected void cancel() {
279 if (!mIsWindowNativelyTranslucent) {
280 Activity activity = findActivity();
281 if (activity != null && mActivityTranslucencyConverted) {
282 activity.convertFromTranslucent();
283 mActivityTranslucencyConverted = false;
286 if (mProgressListener != null) {
287 mProgressListener.onSwipeCancelled(this);
292 * Resets internal members when canceling.
294 private void resetMembers() {
295 if (mVelocityTracker != null) {
296 mVelocityTracker.recycle();
298 mVelocityTracker = null;
304 mDiscardIntercept = false;
307 private void updateSwiping(MotionEvent ev) {
308 boolean oldSwiping = mSwiping;
310 float deltaX = ev.getRawX() - mDownX;
311 float deltaY = ev.getRawY() - mDownY;
312 if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) {
313 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < Math.abs(deltaX);
319 if (mSwiping && !oldSwiping) {
320 // Swiping has started
321 if (!mIsWindowNativelyTranslucent) {
322 Activity activity = findActivity();
323 if (activity != null) {
324 mActivityTranslucencyConverted = activity.convertToTranslucent(null, null);
330 private void updateDismiss(MotionEvent ev) {
331 float deltaX = ev.getRawX() - mDownX;
332 mVelocityTracker.addMovement(ev);
333 mVelocityTracker.computeCurrentVelocity(1000);
335 if ((deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
336 ev.getRawX() >= mLastX)
337 || mVelocityTracker.getXVelocity() >= mMinFlingVelocity) {
341 // Check if the user tried to undo this.
342 if (mDismissed && mSwiping) {
343 // Check if the user's finger is actually flinging back to left
344 if (mVelocityTracker.getXVelocity() < -mMinFlingVelocity) {
351 * Tests scrollability within child views of v in the direction of dx.
353 * @param v View to test for horizontal scrollability
354 * @param checkV Whether the view v passed should itself be checked for scrollability (true),
355 * or just its children (false).
356 * @param dx Delta scrolled in pixels. Only the sign of this is used.
357 * @param x X coordinate of the active touch point
358 * @param y Y coordinate of the active touch point
359 * @return true if child views of v can be scrolled by delta of dx.
361 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) {
362 if (v instanceof ViewGroup) {
363 final ViewGroup group = (ViewGroup) v;
364 final int scrollX = v.getScrollX();
365 final int scrollY = v.getScrollY();
366 final int count = group.getChildCount();
367 for (int i = count - 1; i >= 0; i--) {
368 final View child = group.getChildAt(i);
369 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
370 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
371 canScroll(child, true, dx, x + scrollX - child.getLeft(),
372 y + scrollY - child.getTop())) {
378 return checkV && v.canScrollHorizontally((int) -dx);
381 public void setDismissable(boolean dismissable) {
382 if (!dismissable && mDismissable) {
387 mDismissable = dismissable;
390 private void checkGesture(MotionEvent ev) {
391 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
392 mBlockGesture = mDismissAnimator.isAnimating();
396 private float progressToAlpha(float progress) {
397 return 1 - progress * progress * progress;
400 private Activity findActivity() {
401 Context context = getContext();
402 while (context instanceof ContextWrapper) {
403 if (context instanceof Activity) {
404 return (Activity) context;
406 context = ((ContextWrapper) context).getBaseContext();
411 private class DismissAnimator implements AnimatorUpdateListener, Animator.AnimatorListener {
412 private final TimeInterpolator DISMISS_INTERPOLATOR = new DecelerateInterpolator(1.5f);
413 private final long DISMISS_DURATION = 250;
415 private final ValueAnimator mDismissAnimator = new ValueAnimator();
416 private boolean mWasCanceled = false;
417 private boolean mDismissOnComplete = false;
419 /* package */ DismissAnimator() {
420 mDismissAnimator.addUpdateListener(this);
421 mDismissAnimator.addListener(this);
424 /* package */ void animateDismissal(float currentTranslation) {
426 currentTranslation / getWidth(),
429 DISMISS_INTERPOLATOR,
433 /* package */ void animateRecovery(float currentTranslation) {
435 currentTranslation / getWidth(),
438 DISMISS_INTERPOLATOR,
439 false /* don't dismiss */);
442 /* package */ boolean isAnimating() {
443 return mDismissAnimator.isStarted();
446 private void animate(float from, float to, long duration, TimeInterpolator interpolator,
447 boolean dismissOnComplete) {
448 mDismissAnimator.cancel();
449 mDismissOnComplete = dismissOnComplete;
450 mDismissAnimator.setFloatValues(from, to);
451 mDismissAnimator.setDuration(duration);
452 mDismissAnimator.setInterpolator(interpolator);
453 mDismissAnimator.start();
457 public void onAnimationUpdate(ValueAnimator animation) {
458 float value = (Float) animation.getAnimatedValue();
459 setProgress(value * getWidth());
463 public void onAnimationStart(Animator animation) {
464 mWasCanceled = false;
468 public void onAnimationCancel(Animator animation) {
473 public void onAnimationEnd(Animator animation) {
475 if (mDismissOnComplete) {
484 public void onAnimationRepeat(Animator animation) {