• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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 
18 package com.android.systemui;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ObjectAnimator;
23 import android.content.Context;
24 import android.media.AudioAttributes;
25 import android.os.Vibrator;
26 import android.util.Log;
27 import android.view.Gravity;
28 import android.view.HapticFeedbackConstants;
29 import android.view.MotionEvent;
30 import android.view.ScaleGestureDetector;
31 import android.view.ScaleGestureDetector.OnScaleGestureListener;
32 import android.view.VelocityTracker;
33 import android.view.View;
34 import android.view.ViewConfiguration;
35 
36 import com.android.systemui.statusbar.ExpandableNotificationRow;
37 import com.android.systemui.statusbar.ExpandableView;
38 import com.android.systemui.statusbar.FlingAnimationUtils;
39 import com.android.systemui.statusbar.policy.ScrollAdapter;
40 
41 public class ExpandHelper implements Gefingerpoken {
42     public interface Callback {
getChildAtRawPosition(float x, float y)43         ExpandableView getChildAtRawPosition(float x, float y);
getChildAtPosition(float x, float y)44         ExpandableView getChildAtPosition(float x, float y);
canChildBeExpanded(View v)45         boolean canChildBeExpanded(View v);
setUserExpandedChild(View v, boolean userExpanded)46         void setUserExpandedChild(View v, boolean userExpanded);
setUserLockedChild(View v, boolean userLocked)47         void setUserLockedChild(View v, boolean userLocked);
expansionStateChanged(boolean isExpanding)48         void expansionStateChanged(boolean isExpanding);
49     }
50 
51     private static final String TAG = "ExpandHelper";
52     protected static final boolean DEBUG = false;
53     protected static final boolean DEBUG_SCALE = false;
54     private static final float EXPAND_DURATION = 0.3f;
55 
56     // Set to false to disable focus-based gestures (spread-finger vertical pull).
57     private static final boolean USE_DRAG = true;
58     // Set to false to disable scale-based gestures (both horizontal and vertical).
59     private static final boolean USE_SPAN = true;
60     // Both gestures types may be active at the same time.
61     // At least one gesture type should be active.
62     // A variant of the screwdriver gesture will emerge from either gesture type.
63 
64     // amount of overstretch for maximum brightness expressed in U
65     // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
66     private static final float STRETCH_INTERVAL = 2f;
67 
68     @SuppressWarnings("unused")
69     private Context mContext;
70 
71     private boolean mExpanding;
72     private static final int NONE    = 0;
73     private static final int BLINDS  = 1<<0;
74     private static final int PULL    = 1<<1;
75     private static final int STRETCH = 1<<2;
76     private int mExpansionStyle = NONE;
77     private boolean mWatchingForPull;
78     private boolean mHasPopped;
79     private View mEventSource;
80     private float mOldHeight;
81     private float mNaturalHeight;
82     private float mInitialTouchFocusY;
83     private float mInitialTouchY;
84     private float mInitialTouchSpan;
85     private float mLastFocusY;
86     private float mLastSpanY;
87     private int mTouchSlop;
88     private float mLastMotionY;
89     private float mPullGestureMinXSpan;
90     private Callback mCallback;
91     private ScaleGestureDetector mSGD;
92     private ViewScaler mScaler;
93     private ObjectAnimator mScaleAnimation;
94     private boolean mEnabled = true;
95     private ExpandableView mResizedView;
96     private float mCurrentHeight;
97 
98     private int mSmallSize;
99     private int mLargeSize;
100     private float mMaximumStretch;
101     private boolean mOnlyMovements;
102 
103     private int mGravity;
104 
105     private ScrollAdapter mScrollAdapter;
106     private FlingAnimationUtils mFlingAnimationUtils;
107     private VelocityTracker mVelocityTracker;
108 
109     private OnScaleGestureListener mScaleGestureListener
110             = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
111         @Override
112         public boolean onScaleBegin(ScaleGestureDetector detector) {
113             if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()");
114 
115             if (!mOnlyMovements) {
116                 startExpanding(mResizedView, STRETCH);
117             }
118             return mExpanding;
119         }
120 
121         @Override
122         public boolean onScale(ScaleGestureDetector detector) {
123             if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView);
124             return true;
125         }
126 
127         @Override
128         public void onScaleEnd(ScaleGestureDetector detector) {
129         }
130     };
131 
132     private class ViewScaler {
133         ExpandableView mView;
134 
ViewScaler()135         public ViewScaler() {}
setView(ExpandableView v)136         public void setView(ExpandableView v) {
137             mView = v;
138         }
setHeight(float h)139         public void setHeight(float h) {
140             if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
141             mView.setContentHeight((int) h);
142             mCurrentHeight = h;
143         }
getHeight()144         public float getHeight() {
145             return mView.getContentHeight();
146         }
getNaturalHeight(int maximum)147         public int getNaturalHeight(int maximum) {
148             return Math.min(maximum, mView.getMaxContentHeight());
149         }
150     }
151 
152     /**
153      * Handle expansion gestures to expand and contract children of the callback.
154      *
155      * @param context application context
156      * @param callback the container that holds the items to be manipulated
157      * @param small the smallest allowable size for the manuipulated items.
158      * @param large the largest allowable size for the manuipulated items.
159      */
ExpandHelper(Context context, Callback callback, int small, int large)160     public ExpandHelper(Context context, Callback callback, int small, int large) {
161         mSmallSize = small;
162         mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
163         mLargeSize = large;
164         mContext = context;
165         mCallback = callback;
166         mScaler = new ViewScaler();
167         mGravity = Gravity.TOP;
168         mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
169         mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
170 
171         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
172         mTouchSlop = configuration.getScaledTouchSlop();
173 
174         mSGD = new ScaleGestureDetector(context, mScaleGestureListener);
175         mFlingAnimationUtils = new FlingAnimationUtils(context, EXPAND_DURATION);
176     }
177 
updateExpansion()178     private void updateExpansion() {
179         if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()");
180         // are we scaling or dragging?
181         float span = mSGD.getCurrentSpan() - mInitialTouchSpan;
182         span *= USE_SPAN ? 1f : 0f;
183         float drag = mSGD.getFocusY() - mInitialTouchFocusY;
184         drag *= USE_DRAG ? 1f : 0f;
185         drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
186         float pull = Math.abs(drag) + Math.abs(span) + 1f;
187         float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
188         float target = hand + mOldHeight;
189         float newHeight = clamp(target);
190         mScaler.setHeight(newHeight);
191         mLastFocusY = mSGD.getFocusY();
192         mLastSpanY = mSGD.getCurrentSpan();
193     }
194 
clamp(float target)195     private float clamp(float target) {
196         float out = target;
197         out = out < mSmallSize ? mSmallSize : (out > mLargeSize ? mLargeSize : out);
198         out = out > mNaturalHeight ? mNaturalHeight : out;
199         return out;
200     }
201 
findView(float x, float y)202     private ExpandableView findView(float x, float y) {
203         ExpandableView v;
204         if (mEventSource != null) {
205             int[] location = new int[2];
206             mEventSource.getLocationOnScreen(location);
207             x += location[0];
208             y += location[1];
209             v = mCallback.getChildAtRawPosition(x, y);
210         } else {
211             v = mCallback.getChildAtPosition(x, y);
212         }
213         return v;
214     }
215 
isInside(View v, float x, float y)216     private boolean isInside(View v, float x, float y) {
217         if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")");
218 
219         if (v == null) {
220             if (DEBUG) Log.d(TAG, "isinside null subject");
221             return false;
222         }
223         if (mEventSource != null) {
224             int[] location = new int[2];
225             mEventSource.getLocationOnScreen(location);
226             x += location[0];
227             y += location[1];
228             if (DEBUG) Log.d(TAG, "  to global (" + x + ", " + y + ")");
229         }
230         int[] location = new int[2];
231         v.getLocationOnScreen(location);
232         x -= location[0];
233         y -= location[1];
234         if (DEBUG) Log.d(TAG, "  to local (" + x + ", " + y + ")");
235         if (DEBUG) Log.d(TAG, "  inside (" + v.getWidth() + ", " + v.getHeight() + ")");
236         boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
237         return inside;
238     }
239 
setEventSource(View eventSource)240     public void setEventSource(View eventSource) {
241         mEventSource = eventSource;
242     }
243 
setGravity(int gravity)244     public void setGravity(int gravity) {
245         mGravity = gravity;
246     }
247 
setScrollAdapter(ScrollAdapter adapter)248     public void setScrollAdapter(ScrollAdapter adapter) {
249         mScrollAdapter = adapter;
250     }
251 
252     @Override
onInterceptTouchEvent(MotionEvent ev)253     public boolean onInterceptTouchEvent(MotionEvent ev) {
254         if (!isEnabled()) {
255             return false;
256         }
257         trackVelocity(ev);
258         final int action = ev.getAction();
259         if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
260                          " expanding=" + mExpanding +
261                          (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
262                          (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
263                          (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
264         // check for a spread-finger vertical pull gesture
265         mSGD.onTouchEvent(ev);
266         final int x = (int) mSGD.getFocusX();
267         final int y = (int) mSGD.getFocusY();
268 
269         mInitialTouchFocusY = y;
270         mInitialTouchSpan = mSGD.getCurrentSpan();
271         mLastFocusY = mInitialTouchFocusY;
272         mLastSpanY = mInitialTouchSpan;
273         if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan);
274 
275         if (mExpanding) {
276             mLastMotionY = ev.getRawY();
277             maybeRecycleVelocityTracker(ev);
278             return true;
279         } else {
280             if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
281                 // we've begun Venetian blinds style expansion
282                 return true;
283             }
284             switch (action & MotionEvent.ACTION_MASK) {
285             case MotionEvent.ACTION_MOVE: {
286                 final float xspan = mSGD.getCurrentSpanX();
287                 if (xspan > mPullGestureMinXSpan &&
288                         xspan > mSGD.getCurrentSpanY() && !mExpanding) {
289                     // detect a vertical pulling gesture with fingers somewhat separated
290                     if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
291                     startExpanding(mResizedView, PULL);
292                     mWatchingForPull = false;
293                 }
294                 if (mWatchingForPull) {
295                     final float yDiff = ev.getRawY() - mInitialTouchY;
296                     if (yDiff > mTouchSlop) {
297                         if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
298                         mWatchingForPull = false;
299                         if (mResizedView != null && !isFullyExpanded(mResizedView)) {
300                             if (startExpanding(mResizedView, BLINDS)) {
301                                 mLastMotionY = ev.getRawY();
302                                 mInitialTouchY = ev.getRawY();
303                                 mHasPopped = false;
304                             }
305                         }
306                     }
307                 }
308                 break;
309             }
310 
311             case MotionEvent.ACTION_DOWN:
312                 mWatchingForPull = mScrollAdapter != null &&
313                         isInside(mScrollAdapter.getHostView(), x, y)
314                         && mScrollAdapter.isScrolledToTop();
315                 mResizedView = findView(x, y);
316                 if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) {
317                     mResizedView = null;
318                     mWatchingForPull = false;
319                 }
320                 mInitialTouchY = ev.getY();
321                 break;
322 
323             case MotionEvent.ACTION_CANCEL:
324             case MotionEvent.ACTION_UP:
325                 if (DEBUG) Log.d(TAG, "up/cancel");
326                 finishExpanding(false, getCurrentVelocity());
327                 clearView();
328                 break;
329             }
330             mLastMotionY = ev.getRawY();
331             maybeRecycleVelocityTracker(ev);
332             return mExpanding;
333         }
334     }
335 
trackVelocity(MotionEvent event)336     private void trackVelocity(MotionEvent event) {
337         int action = event.getActionMasked();
338         switch(action) {
339             case MotionEvent.ACTION_DOWN:
340                 if (mVelocityTracker == null) {
341                     mVelocityTracker = VelocityTracker.obtain();
342                 } else {
343                     mVelocityTracker.clear();
344                 }
345                 mVelocityTracker.addMovement(event);
346                 break;
347             case MotionEvent.ACTION_MOVE:
348                 if (mVelocityTracker == null) {
349                     mVelocityTracker = VelocityTracker.obtain();
350                 }
351                 mVelocityTracker.addMovement(event);
352                 break;
353             default:
354                 break;
355         }
356     }
357 
maybeRecycleVelocityTracker(MotionEvent event)358     private void maybeRecycleVelocityTracker(MotionEvent event) {
359         if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL
360                 || event.getActionMasked() == MotionEvent.ACTION_UP)) {
361             mVelocityTracker.recycle();
362             mVelocityTracker = null;
363         }
364     }
365 
getCurrentVelocity()366     private float getCurrentVelocity() {
367         if (mVelocityTracker != null) {
368             mVelocityTracker.computeCurrentVelocity(1000);
369             return mVelocityTracker.getYVelocity();
370         } else {
371             return 0f;
372         }
373     }
374 
setEnabled(boolean enable)375     public void setEnabled(boolean enable) {
376         mEnabled = enable;
377     }
378 
isEnabled()379     private boolean isEnabled() {
380         return mEnabled;
381     }
382 
isFullyExpanded(ExpandableView underFocus)383     private boolean isFullyExpanded(ExpandableView underFocus) {
384         return underFocus.areChildrenExpanded() || underFocus.getIntrinsicHeight()
385                 - underFocus.getBottomDecorHeight() == underFocus.getMaxContentHeight();
386     }
387 
388     @Override
onTouchEvent(MotionEvent ev)389     public boolean onTouchEvent(MotionEvent ev) {
390         if (!isEnabled()) {
391             return false;
392         }
393         trackVelocity(ev);
394         final int action = ev.getActionMasked();
395         if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
396                 " expanding=" + mExpanding +
397                 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
398                 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
399                 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
400 
401         mSGD.onTouchEvent(ev);
402         final int x = (int) mSGD.getFocusX();
403         final int y = (int) mSGD.getFocusY();
404 
405         if (mOnlyMovements) {
406             mLastMotionY = ev.getRawY();
407             return false;
408         }
409         switch (action) {
410             case MotionEvent.ACTION_DOWN:
411                 mWatchingForPull = mScrollAdapter != null &&
412                         isInside(mScrollAdapter.getHostView(), x, y);
413                 mResizedView = findView(x, y);
414                 mInitialTouchY = ev.getY();
415                 break;
416             case MotionEvent.ACTION_MOVE: {
417                 if (mWatchingForPull) {
418                     final float yDiff = ev.getRawY() - mInitialTouchY;
419                     if (yDiff > mTouchSlop) {
420                         if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
421                         mWatchingForPull = false;
422                         if (mResizedView != null && !isFullyExpanded(mResizedView)) {
423                             if (startExpanding(mResizedView, BLINDS)) {
424                                 mInitialTouchY = ev.getRawY();
425                                 mLastMotionY = ev.getRawY();
426                                 mHasPopped = false;
427                             }
428                         }
429                     }
430                 }
431                 if (mExpanding && 0 != (mExpansionStyle & BLINDS)) {
432                     final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight;
433                     final float newHeight = clamp(rawHeight);
434                     boolean isFinished = false;
435                     boolean expanded = false;
436                     if (rawHeight > mNaturalHeight) {
437                         isFinished = true;
438                         expanded = true;
439                     }
440                     if (rawHeight < mSmallSize) {
441                         isFinished = true;
442                         expanded = false;
443                     }
444 
445                     if (!mHasPopped) {
446                         if (mEventSource != null) {
447                             mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
448                         }
449                         mHasPopped = true;
450                     }
451 
452                     mScaler.setHeight(newHeight);
453                     mLastMotionY = ev.getRawY();
454                     if (isFinished) {
455                         mCallback.setUserExpandedChild(mResizedView, expanded);
456                         mCallback.expansionStateChanged(false);
457                         return false;
458                     } else {
459                         mCallback.expansionStateChanged(true);
460                     }
461                     return true;
462                 }
463 
464                 if (mExpanding) {
465 
466                     // Gestural expansion is running
467                     updateExpansion();
468                     mLastMotionY = ev.getRawY();
469                     return true;
470                 }
471 
472                 break;
473             }
474 
475             case MotionEvent.ACTION_POINTER_UP:
476             case MotionEvent.ACTION_POINTER_DOWN:
477                 if (DEBUG) Log.d(TAG, "pointer change");
478                 mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
479                 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
480                 break;
481 
482             case MotionEvent.ACTION_UP:
483             case MotionEvent.ACTION_CANCEL:
484                 if (DEBUG) Log.d(TAG, "up/cancel");
485                 finishExpanding(false, getCurrentVelocity());
486                 clearView();
487                 break;
488         }
489         mLastMotionY = ev.getRawY();
490         maybeRecycleVelocityTracker(ev);
491         return mResizedView != null;
492     }
493 
494     /**
495      * @return True if the view is expandable, false otherwise.
496      */
startExpanding(ExpandableView v, int expandType)497     private boolean startExpanding(ExpandableView v, int expandType) {
498         if (!(v instanceof ExpandableNotificationRow)) {
499             return false;
500         }
501         mExpansionStyle = expandType;
502         if (mExpanding && v == mResizedView) {
503             return true;
504         }
505         mExpanding = true;
506         mCallback.expansionStateChanged(true);
507         if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
508         mCallback.setUserLockedChild(v, true);
509         mScaler.setView(v);
510         mOldHeight = mScaler.getHeight();
511         mCurrentHeight = mOldHeight;
512         if (mCallback.canChildBeExpanded(v)) {
513             if (DEBUG) Log.d(TAG, "working on an expandable child");
514             mNaturalHeight = mScaler.getNaturalHeight(mLargeSize);
515         } else {
516             if (DEBUG) Log.d(TAG, "working on a non-expandable child");
517             mNaturalHeight = mOldHeight;
518         }
519         if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
520                     " mNaturalHeight: " + mNaturalHeight);
521         return true;
522     }
523 
finishExpanding(boolean force, float velocity)524     private void finishExpanding(boolean force, float velocity) {
525         if (!mExpanding) return;
526 
527         if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView);
528 
529         float currentHeight = mScaler.getHeight();
530         float targetHeight = mSmallSize;
531         float h = mScaler.getHeight();
532         final boolean wasClosed = (mOldHeight == mSmallSize);
533         if (wasClosed) {
534             targetHeight = (force || currentHeight > mSmallSize) ? mNaturalHeight : mSmallSize;
535         } else {
536             targetHeight = (force || currentHeight < mNaturalHeight) ? mSmallSize : mNaturalHeight;
537         }
538         if (mScaleAnimation.isRunning()) {
539             mScaleAnimation.cancel();
540         }
541         mCallback.setUserExpandedChild(mResizedView, targetHeight == mNaturalHeight);
542         mCallback.expansionStateChanged(false);
543         if (targetHeight != currentHeight) {
544             mScaleAnimation.setFloatValues(targetHeight);
545             mScaleAnimation.setupStartValues();
546             final View scaledView = mResizedView;
547             mScaleAnimation.addListener(new AnimatorListenerAdapter() {
548                 @Override
549                 public void onAnimationEnd(Animator animation) {
550                     mCallback.setUserLockedChild(scaledView, false);
551                     mScaleAnimation.removeListener(this);
552                 }
553             });
554             mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity);
555             mScaleAnimation.start();
556         } else {
557             mCallback.setUserLockedChild(mResizedView, false);
558         }
559 
560         mExpanding = false;
561         mExpansionStyle = NONE;
562 
563         if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed);
564         if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight);
565         if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize);
566         if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight);
567         if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView);
568     }
569 
clearView()570     private void clearView() {
571         mResizedView = null;
572     }
573 
574     /**
575      * Use this to abort any pending expansions in progress.
576      */
cancel()577     public void cancel() {
578         finishExpanding(true, 0f /* velocity */);
579         clearView();
580 
581         // reset the gesture detector
582         mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
583     }
584 
585     /**
586      * Change the expansion mode to only observe movements and don't perform any resizing.
587      * This is needed when the expanding is finished and the scroller kicks in,
588      * performing an overscroll motion. We only want to shrink it again when we are not
589      * overscrolled.
590      *
591      * @param onlyMovements Should only movements be observed?
592      */
onlyObserveMovements(boolean onlyMovements)593     public void onlyObserveMovements(boolean onlyMovements) {
594         mOnlyMovements = onlyMovements;
595     }
596 }
597 
598