OSDN Git Service

Check if the activity translucency was converted before converted back.
[android-x86/frameworks-base.git] / core / java / com / android / internal / widget / SwipeDismissLayout.java
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.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;
39
40 /**
41  * Special layout that finishes its activity when swiped away.
42  */
43 public class SwipeDismissLayout extends FrameLayout {
44     private static final String TAG = "SwipeDismissLayout";
45
46     private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f;
47
48     public interface OnDismissedListener {
49         void onDismissed(SwipeDismissLayout layout);
50     }
51
52     public interface OnSwipeProgressChangedListener {
53         /**
54          * Called when the layout has been swiped and the position of the window should change.
55          *
56          * @param alpha A number in [0, 1] representing what the alpha transparency of the window
57          * should be.
58          * @param translate A number in [0, w], where w is the width of the
59          * layout. This is equivalent to progress * layout.getWidth().
60          */
61         void onSwipeProgressChanged(SwipeDismissLayout layout, float alpha, float translate);
62
63         void onSwipeCancelled(SwipeDismissLayout layout);
64     }
65
66     private boolean mIsWindowNativelyTranslucent;
67
68     // Cached ViewConfiguration and system-wide constant values
69     private int mSlop;
70     private int mMinFlingVelocity;
71
72     // Transient properties
73     private int mActiveTouchId;
74     private float mDownX;
75     private float mDownY;
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;
83
84     private final DismissAnimator mDismissAnimator = new DismissAnimator();
85
86     private OnDismissedListener mDismissedListener;
87     private OnSwipeProgressChangedListener mProgressListener;
88     private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
89         private Runnable mRunnable = new Runnable() {
90             @Override
91             public void run() {
92                 if (mDismissed) {
93                     dismiss();
94                 } else {
95                     cancel();
96                 }
97                 resetMembers();
98             }
99         };
100
101         @Override
102         public void onReceive(Context context, Intent intent) {
103             post(mRunnable);
104         }
105     };
106     private IntentFilter mScreenOffFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
107
108     private float mLastX;
109
110     private boolean mDismissable = true;
111
112     public SwipeDismissLayout(Context context) {
113         super(context);
114         init(context);
115     }
116
117     public SwipeDismissLayout(Context context, AttributeSet attrs) {
118         super(context, attrs);
119         init(context);
120     }
121
122     public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
123         super(context, attrs, defStyle);
124         init(context);
125     }
126
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);
135         a.recycle();
136     }
137
138     public void setOnDismissedListener(OnDismissedListener listener) {
139         mDismissedListener = listener;
140     }
141
142     public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
143         mProgressListener = listener;
144     }
145
146     @Override
147     protected void onAttachedToWindow() {
148         super.onAttachedToWindow();
149         getContext().registerReceiver(mScreenOffReceiver, mScreenOffFilter);
150     }
151
152     @Override
153     protected void onDetachedFromWindow() {
154         getContext().unregisterReceiver(mScreenOffReceiver);
155         super.onDetachedFromWindow();
156     }
157
158     @Override
159     public boolean onInterceptTouchEvent(MotionEvent ev) {
160         checkGesture((ev));
161         if (mBlockGesture) {
162             return true;
163         }
164         if (!mDismissable) {
165             return super.onInterceptTouchEvent(ev);
166         }
167
168         // offset because the view is translated during swipe
169         ev.offsetLocation(mTranslationX, 0);
170
171         switch (ev.getActionMasked()) {
172             case MotionEvent.ACTION_DOWN:
173                 resetMembers();
174                 mDownX = ev.getRawX();
175                 mDownY = ev.getRawY();
176                 mActiveTouchId = ev.getPointerId(0);
177                 mVelocityTracker = VelocityTracker.obtain();
178                 mVelocityTracker.addMovement(ev);
179                 break;
180
181             case MotionEvent.ACTION_POINTER_DOWN:
182                 int actionIndex = ev.getActionIndex();
183                 mActiveTouchId = ev.getPointerId(actionIndex);
184                 break;
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);
192                 }
193                 break;
194
195             case MotionEvent.ACTION_CANCEL:
196             case MotionEvent.ACTION_UP:
197                 resetMembers();
198                 break;
199
200             case MotionEvent.ACTION_MOVE:
201                 if (mVelocityTracker == null || mDiscardIntercept) {
202                     break;
203                 }
204
205                 int pointerIndex = ev.findPointerIndex(mActiveTouchId);
206                 if (pointerIndex == -1) {
207                     Log.e(TAG, "Invalid pointer index: ignoring.");
208                     mDiscardIntercept = true;
209                     break;
210                 }
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;
216                     break;
217                 }
218                 updateSwiping(ev);
219                 break;
220         }
221
222         return !mDiscardIntercept && mSwiping;
223     }
224
225     @Override
226     public boolean onTouchEvent(MotionEvent ev) {
227         checkGesture((ev));
228         if (mBlockGesture) {
229             return true;
230         }
231         if (mVelocityTracker == null || !mDismissable) {
232             return super.onTouchEvent(ev);
233         }
234         // offset because the view is translated during swipe
235         ev.offsetLocation(mTranslationX, 0);
236         switch (ev.getActionMasked()) {
237             case MotionEvent.ACTION_UP:
238                 updateDismiss(ev);
239                 if (mDismissed) {
240                     mDismissAnimator.animateDismissal(ev.getRawX() - mDownX);
241                 } else if (mSwiping) {
242                     mDismissAnimator.animateRecovery(ev.getRawX() - mDownX);
243                 }
244                 resetMembers();
245                 break;
246
247             case MotionEvent.ACTION_CANCEL:
248                 cancel();
249                 resetMembers();
250                 break;
251
252             case MotionEvent.ACTION_MOVE:
253                 mVelocityTracker.addMovement(ev);
254                 mLastX = ev.getRawX();
255                 updateSwiping(ev);
256                 if (mSwiping) {
257                     setProgress(ev.getRawX() - mDownX);
258                     break;
259                 }
260         }
261         return true;
262     }
263
264     private void setProgress(float deltaX) {
265         mTranslationX = deltaX;
266         if (mProgressListener != null && deltaX >= 0)  {
267             mProgressListener.onSwipeProgressChanged(
268                     this, progressToAlpha(deltaX / getWidth()), deltaX);
269         }
270     }
271
272     private void dismiss() {
273         if (mDismissedListener != null) {
274             mDismissedListener.onDismissed(this);
275         }
276     }
277
278     protected void cancel() {
279         if (!mIsWindowNativelyTranslucent) {
280             Activity activity = findActivity();
281             if (activity != null && mActivityTranslucencyConverted) {
282                 activity.convertFromTranslucent();
283                 mActivityTranslucencyConverted = false;
284             }
285         }
286         if (mProgressListener != null) {
287             mProgressListener.onSwipeCancelled(this);
288         }
289     }
290
291     /**
292      * Resets internal members when canceling.
293      */
294     private void resetMembers() {
295         if (mVelocityTracker != null) {
296             mVelocityTracker.recycle();
297         }
298         mVelocityTracker = null;
299         mTranslationX = 0;
300         mDownX = 0;
301         mDownY = 0;
302         mSwiping = false;
303         mDismissed = false;
304         mDiscardIntercept = false;
305     }
306
307     private void updateSwiping(MotionEvent ev) {
308         boolean oldSwiping = mSwiping;
309         if (!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);
314             } else {
315                 mSwiping = false;
316             }
317         }
318
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);
325                 }
326             }
327         }
328     }
329
330     private void updateDismiss(MotionEvent ev) {
331         float deltaX = ev.getRawX() - mDownX;
332         mVelocityTracker.addMovement(ev);
333         mVelocityTracker.computeCurrentVelocity(1000);
334         if (!mDismissed) {
335             if ((deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
336                     ev.getRawX() >= mLastX)
337                     || mVelocityTracker.getXVelocity() >= mMinFlingVelocity) {
338                 mDismissed = true;
339             }
340         }
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) {
345                 mDismissed = false;
346             }
347         }
348     }
349
350     /**
351      * Tests scrollability within child views of v in the direction of dx.
352      *
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.
360      */
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())) {
373                     return true;
374                 }
375             }
376         }
377
378         return checkV && v.canScrollHorizontally((int) -dx);
379     }
380
381     public void setDismissable(boolean dismissable) {
382         if (!dismissable && mDismissable) {
383             cancel();
384             resetMembers();
385         }
386
387         mDismissable = dismissable;
388     }
389
390     private void checkGesture(MotionEvent ev) {
391         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
392             mBlockGesture = mDismissAnimator.isAnimating();
393         }
394     }
395
396     private float progressToAlpha(float progress) {
397         return 1 - progress * progress * progress;
398     }
399
400     private Activity findActivity() {
401         Context context = getContext();
402         while (context instanceof ContextWrapper) {
403             if (context instanceof Activity) {
404                 return (Activity) context;
405             }
406             context = ((ContextWrapper) context).getBaseContext();
407         }
408         return null;
409     }
410
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;
414
415         private final ValueAnimator mDismissAnimator = new ValueAnimator();
416         private boolean mWasCanceled = false;
417         private boolean mDismissOnComplete = false;
418
419         /* package */ DismissAnimator() {
420             mDismissAnimator.addUpdateListener(this);
421             mDismissAnimator.addListener(this);
422         }
423
424         /* package */ void animateDismissal(float currentTranslation) {
425             animate(
426                     currentTranslation / getWidth(),
427                     1,
428                     DISMISS_DURATION,
429                     DISMISS_INTERPOLATOR,
430                     true /* dismiss */);
431         }
432
433         /* package */ void animateRecovery(float currentTranslation) {
434             animate(
435                     currentTranslation / getWidth(),
436                     0,
437                     DISMISS_DURATION,
438                     DISMISS_INTERPOLATOR,
439                     false /* don't dismiss */);
440         }
441
442         /* package */ boolean isAnimating() {
443             return mDismissAnimator.isStarted();
444         }
445
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();
454         }
455
456         @Override
457         public void onAnimationUpdate(ValueAnimator animation) {
458             float value = (Float) animation.getAnimatedValue();
459             setProgress(value * getWidth());
460         }
461
462         @Override
463         public void onAnimationStart(Animator animation) {
464             mWasCanceled = false;
465         }
466
467         @Override
468         public void onAnimationCancel(Animator animation) {
469             mWasCanceled = true;
470         }
471
472         @Override
473         public void onAnimationEnd(Animator animation) {
474             if (!mWasCanceled) {
475                 if (mDismissOnComplete) {
476                     dismiss();
477                 } else {
478                     cancel();
479                 }
480             }
481         }
482
483         @Override
484         public void onAnimationRepeat(Animator animation) {
485         }
486     }
487 }