[go: nahoru, domu]

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