ExpandHelper.java revision b6d85ebfe4f9f5d3b7d7ab7b6123af02a0deb516
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.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.content.Context;
25import android.media.AudioManager;
26import android.os.Vibrator;
27import android.util.Log;
28import android.view.Gravity;
29import android.view.MotionEvent;
30import android.view.ScaleGestureDetector;
31import android.view.ScaleGestureDetector.OnScaleGestureListener;
32import android.view.View;
33import android.view.View.OnClickListener;
34import android.view.ViewConfiguration;
35import android.view.ViewGroup;
36
37import com.android.systemui.statusbar.policy.ScrollAdapter;
38
39public class ExpandHelper implements Gefingerpoken, OnClickListener {
40    public interface Callback {
41        View getChildAtRawPosition(float x, float y);
42        View 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    }
47
48    private static final String TAG = "ExpandHelper";
49    protected static final boolean DEBUG = false;
50    protected static final boolean DEBUG_SCALE = false;
51    protected static final boolean DEBUG_GLOW = false;
52    private static final long EXPAND_DURATION = 250;
53    private static final long GLOW_DURATION = 150;
54
55    // Set to false to disable focus-based gestures (spread-finger vertical pull).
56    private static final boolean USE_DRAG = true;
57    // Set to false to disable scale-based gestures (both horizontal and vertical).
58    private static final boolean USE_SPAN = true;
59    // Both gestures types may be active at the same time.
60    // At least one gesture type should be active.
61    // A variant of the screwdriver gesture will emerge from either gesture type.
62
63    // amount of overstretch for maximum brightness expressed in U
64    // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
65    private static final float STRETCH_INTERVAL = 2f;
66
67    // level of glow for a touch, without overstretch
68    // overstretch fills the range (GLOW_BASE, 1.0]
69    private static final float GLOW_BASE = 0.5f;
70
71    @SuppressWarnings("unused")
72    private Context mContext;
73
74    private boolean mExpanding;
75    private static final int NONE    = 0;
76    private static final int BLINDS  = 1<<0;
77    private static final int PULL    = 1<<1;
78    private static final int STRETCH = 1<<2;
79    private int mExpansionStyle = NONE;
80    private boolean mWatchingForPull;
81    private boolean mHasPopped;
82    private View mEventSource;
83    private View mCurrView;
84    private View mCurrViewTopGlow;
85    private View mCurrViewBottomGlow;
86    private float mOldHeight;
87    private float mNaturalHeight;
88    private float mInitialTouchFocusY;
89    private float mInitialTouchY;
90    private float mInitialTouchSpan;
91    private float mLastFocusY;
92    private float mLastSpanY;
93    private int mTouchSlop;
94    private int mLastMotionY;
95    private float mPopLimit;
96    private int mPopDuration;
97    private float mPullGestureMinXSpan;
98    private Callback mCallback;
99    private ScaleGestureDetector mSGD;
100    private ViewScaler mScaler;
101    private ObjectAnimator mScaleAnimation;
102    private AnimatorSet mGlowAnimationSet;
103    private ObjectAnimator mGlowTopAnimation;
104    private ObjectAnimator mGlowBottomAnimation;
105    private Vibrator mVibrator;
106
107    private int mSmallSize;
108    private int mLargeSize;
109    private float mMaximumStretch;
110
111    private int mGravity;
112
113    private ScrollAdapter mScrollAdapter;
114
115    private OnScaleGestureListener mScaleGestureListener
116            = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
117        @Override
118        public boolean onScaleBegin(ScaleGestureDetector detector) {
119            if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()");
120            float focusX = detector.getFocusX();
121            float focusY = detector.getFocusY();
122
123            final View underFocus = findView(focusX, focusY);
124            if (underFocus != null) {
125                startExpanding(underFocus, STRETCH);
126            }
127            return mExpanding;
128        }
129
130        @Override
131        public boolean onScale(ScaleGestureDetector detector) {
132            if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mCurrView);
133            return true;
134        }
135
136        @Override
137        public void onScaleEnd(ScaleGestureDetector detector) {
138        }
139    };
140
141    private class ViewScaler {
142        View mView;
143
144        public ViewScaler() {}
145        public void setView(View v) {
146            mView = v;
147        }
148        public void setHeight(float h) {
149            if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
150            ViewGroup.LayoutParams lp = mView.getLayoutParams();
151            lp.height = (int)h;
152            mView.setLayoutParams(lp);
153            mView.requestLayout();
154        }
155        public float getHeight() {
156            int height = mView.getLayoutParams().height;
157            if (height < 0) {
158                height = mView.getMeasuredHeight();
159            }
160            return height;
161        }
162        public int getNaturalHeight(int maximum) {
163            ViewGroup.LayoutParams lp = mView.getLayoutParams();
164            if (DEBUG_SCALE) Log.v(TAG, "Inspecting a child of type: " +
165                    mView.getClass().getName());
166            int oldHeight = lp.height;
167            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
168            mView.setLayoutParams(lp);
169            mView.measure(
170                    View.MeasureSpec.makeMeasureSpec(mView.getMeasuredWidth(),
171                                                     View.MeasureSpec.EXACTLY),
172                    View.MeasureSpec.makeMeasureSpec(maximum,
173                                                     View.MeasureSpec.AT_MOST));
174            lp.height = oldHeight;
175            mView.setLayoutParams(lp);
176            return mView.getMeasuredHeight();
177        }
178    }
179
180    /**
181     * Handle expansion gestures to expand and contract children of the callback.
182     *
183     * @param context application context
184     * @param callback the container that holds the items to be manipulated
185     * @param small the smallest allowable size for the manuipulated items.
186     * @param large the largest allowable size for the manuipulated items.
187     */
188    public ExpandHelper(Context context, Callback callback, int small, int large) {
189        mSmallSize = small;
190        mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
191        mLargeSize = large;
192        mContext = context;
193        mCallback = callback;
194        mScaler = new ViewScaler();
195        mGravity = Gravity.TOP;
196        mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
197        mScaleAnimation.setDuration(EXPAND_DURATION);
198        mPopLimit = mContext.getResources().getDimension(R.dimen.blinds_pop_threshold);
199        mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms);
200        mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
201
202        AnimatorListenerAdapter glowVisibilityController = new AnimatorListenerAdapter() {
203            @Override
204            public void onAnimationStart(Animator animation) {
205                View target = (View) ((ObjectAnimator) animation).getTarget();
206                if (target.getAlpha() <= 0.0f) {
207                    target.setVisibility(View.VISIBLE);
208                }
209            }
210
211            @Override
212            public void onAnimationEnd(Animator animation) {
213                View target = (View) ((ObjectAnimator) animation).getTarget();
214                if (target.getAlpha() <= 0.0f) {
215                    target.setVisibility(View.INVISIBLE);
216                }
217            }
218        };
219
220        mGlowTopAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
221        mGlowTopAnimation.addListener(glowVisibilityController);
222        mGlowBottomAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
223        mGlowBottomAnimation.addListener(glowVisibilityController);
224        mGlowAnimationSet = new AnimatorSet();
225        mGlowAnimationSet.play(mGlowTopAnimation).with(mGlowBottomAnimation);
226        mGlowAnimationSet.setDuration(GLOW_DURATION);
227
228        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
229        mTouchSlop = configuration.getScaledTouchSlop();
230
231        mSGD = new ScaleGestureDetector(context, mScaleGestureListener);
232    }
233
234    private void updateExpansion() {
235        if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()");
236        // are we scaling or dragging?
237        float span = mSGD.getCurrentSpan() - mInitialTouchSpan;
238        span *= USE_SPAN ? 1f : 0f;
239        float drag = mSGD.getFocusY() - mInitialTouchFocusY;
240        drag *= USE_DRAG ? 1f : 0f;
241        drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
242        float pull = Math.abs(drag) + Math.abs(span) + 1f;
243        float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
244        float target = hand + mOldHeight;
245        float newHeight = clamp(target);
246        mScaler.setHeight(newHeight);
247
248        setGlow(calculateGlow(target, newHeight));
249        mLastFocusY = mSGD.getFocusY();
250        mLastSpanY = mSGD.getCurrentSpan();
251    }
252
253    private float clamp(float target) {
254        float out = target;
255        out = out < mSmallSize ? mSmallSize : (out > mLargeSize ? mLargeSize : out);
256        out = out > mNaturalHeight ? mNaturalHeight : out;
257        return out;
258    }
259
260    private View findView(float x, float y) {
261        View v = null;
262        if (mEventSource != null) {
263            int[] location = new int[2];
264            mEventSource.getLocationOnScreen(location);
265            x += location[0];
266            y += location[1];
267            v = mCallback.getChildAtRawPosition(x, y);
268        } else {
269            v = mCallback.getChildAtPosition(x, y);
270        }
271        return v;
272    }
273
274    private boolean isInside(View v, float x, float y) {
275        if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")");
276
277        if (v == null) {
278            if (DEBUG) Log.d(TAG, "isinside null subject");
279            return false;
280        }
281        if (mEventSource != null) {
282            int[] location = new int[2];
283            mEventSource.getLocationOnScreen(location);
284            x += location[0];
285            y += location[1];
286            if (DEBUG) Log.d(TAG, "  to global (" + x + ", " + y + ")");
287        }
288        int[] location = new int[2];
289        v.getLocationOnScreen(location);
290        x -= location[0];
291        y -= location[1];
292        if (DEBUG) Log.d(TAG, "  to local (" + x + ", " + y + ")");
293        if (DEBUG) Log.d(TAG, "  inside (" + v.getWidth() + ", " + v.getHeight() + ")");
294        boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
295        return inside;
296    }
297
298    public void setEventSource(View eventSource) {
299        mEventSource = eventSource;
300    }
301
302    public void setGravity(int gravity) {
303        mGravity = gravity;
304    }
305
306    public void setScrollAdapter(ScrollAdapter adapter) {
307        mScrollAdapter = adapter;
308    }
309
310    private float calculateGlow(float target, float actual) {
311        // glow if overscale
312        if (DEBUG_GLOW) Log.d(TAG, "target: " + target + " actual: " + actual);
313        float stretch = Math.abs((target - actual) / mMaximumStretch);
314        float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
315        if (DEBUG_GLOW) Log.d(TAG, "stretch: " + stretch + " strength: " + strength);
316        return (GLOW_BASE + strength * (1f - GLOW_BASE));
317    }
318
319    public void setGlow(float glow) {
320        if (!mGlowAnimationSet.isRunning() || glow == 0f) {
321            if (mGlowAnimationSet.isRunning()) {
322                mGlowAnimationSet.end();
323            }
324            if (mCurrViewTopGlow != null && mCurrViewBottomGlow != null) {
325                if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) {
326                    // animate glow in and out
327                    mGlowTopAnimation.setTarget(mCurrViewTopGlow);
328                    mGlowBottomAnimation.setTarget(mCurrViewBottomGlow);
329                    mGlowTopAnimation.setFloatValues(glow);
330                    mGlowBottomAnimation.setFloatValues(glow);
331                    mGlowAnimationSet.setupStartValues();
332                    mGlowAnimationSet.start();
333                } else {
334                    // set it explicitly in reponse to touches.
335                    mCurrViewTopGlow.setAlpha(glow);
336                    mCurrViewBottomGlow.setAlpha(glow);
337                    handleGlowVisibility();
338                }
339            }
340        }
341    }
342
343    private void handleGlowVisibility() {
344        mCurrViewTopGlow.setVisibility(mCurrViewTopGlow.getAlpha() <= 0.0f ?
345                View.INVISIBLE : View.VISIBLE);
346        mCurrViewBottomGlow.setVisibility(mCurrViewBottomGlow.getAlpha() <= 0.0f ?
347                View.INVISIBLE : View.VISIBLE);
348    }
349
350    @Override
351    public boolean onInterceptTouchEvent(MotionEvent ev) {
352        final int action = ev.getAction();
353        if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
354                         " expanding=" + mExpanding +
355                         (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
356                         (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
357                         (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
358        // check for a spread-finger vertical pull gesture
359        mSGD.onTouchEvent(ev);
360        final int x = (int) mSGD.getFocusX();
361        final int y = (int) mSGD.getFocusY();
362
363        mInitialTouchFocusY = y;
364        mInitialTouchSpan = mSGD.getCurrentSpan();
365        mLastFocusY = mInitialTouchFocusY;
366        mLastSpanY = mInitialTouchSpan;
367        if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan);
368
369        if (mExpanding) {
370            return true;
371        } else {
372            if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
373                // we've begun Venetian blinds style expansion
374                return true;
375            }
376            final float xspan = mSGD.getCurrentSpanX();
377            if ((action == MotionEvent.ACTION_MOVE &&
378                    xspan > mPullGestureMinXSpan &&
379                    xspan > mSGD.getCurrentSpanY())) {
380                // detect a vertical pulling gesture with fingers somewhat separated
381                if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
382
383                final View underFocus = findView(x, y);
384                if (underFocus != null) {
385                    startExpanding(underFocus, PULL);
386                }
387                return true;
388            }
389            if (mScrollAdapter != null && !mScrollAdapter.isScrolledToTop()) {
390                return false;
391            }
392            // Now look for other gestures
393            switch (action & MotionEvent.ACTION_MASK) {
394            case MotionEvent.ACTION_MOVE: {
395                if (mWatchingForPull) {
396                    final int yDiff = y - mLastMotionY;
397                    if (yDiff > mTouchSlop) {
398                        if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
399                        mLastMotionY = y;
400                        final View underFocus = findView(x, y);
401                        if (underFocus != null) {
402                            startExpanding(underFocus, BLINDS);
403                            mInitialTouchY = mLastMotionY;
404                            mHasPopped = false;
405                        }
406                    }
407                }
408                break;
409            }
410
411            case MotionEvent.ACTION_DOWN:
412                mWatchingForPull = mScrollAdapter != null &&
413                        isInside(mScrollAdapter.getHostView(), x, y);
414                mLastMotionY = y;
415                break;
416
417            case MotionEvent.ACTION_CANCEL:
418            case MotionEvent.ACTION_UP:
419                if (DEBUG) Log.d(TAG, "up/cancel");
420                finishExpanding(false);
421                clearView();
422                break;
423            }
424            return mExpanding;
425        }
426    }
427
428    @Override
429    public boolean onTouchEvent(MotionEvent ev) {
430        final int action = ev.getActionMasked();
431        if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
432                " expanding=" + mExpanding +
433                (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
434                (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
435                (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
436
437        mSGD.onTouchEvent(ev);
438
439        switch (action) {
440            case MotionEvent.ACTION_MOVE: {
441                if (0 != (mExpansionStyle & BLINDS)) {
442                    final float rawHeight = ev.getY() - mInitialTouchY + mOldHeight;
443                    final float newHeight = clamp(rawHeight);
444                    final boolean wasClosed = (mOldHeight == mSmallSize);
445                    boolean isFinished = false;
446                    if (rawHeight > mNaturalHeight) {
447                        isFinished = true;
448                    }
449                    if (rawHeight < mSmallSize) {
450                        isFinished = true;
451                    }
452
453                    final float pull = Math.abs(ev.getY() - mInitialTouchY);
454                    if (mHasPopped || pull > mPopLimit) {
455                        if (!mHasPopped) {
456                            vibrate(mPopDuration);
457                            mHasPopped = true;
458                        }
459                    }
460
461                    if (mHasPopped) {
462                        mScaler.setHeight(newHeight);
463                        setGlow(GLOW_BASE);
464                    } else {
465                        setGlow(calculateGlow(4f * pull, 0f));
466                    }
467
468                    final int x = (int) mSGD.getFocusX();
469                    final int y = (int) mSGD.getFocusY();
470                    View underFocus = findView(x, y);
471                    if (isFinished && underFocus != null && underFocus != mCurrView) {
472                        finishExpanding(false); // @@@ needed?
473                        startExpanding(underFocus, BLINDS);
474                        mInitialTouchY = y;
475                        mHasPopped = false;
476                    }
477                    return true;
478                }
479
480                if (mExpanding) {
481                    updateExpansion();
482                    return true;
483                }
484
485                break;
486            }
487
488            case MotionEvent.ACTION_POINTER_UP:
489            case MotionEvent.ACTION_POINTER_DOWN:
490                if (DEBUG) Log.d(TAG, "pointer change");
491                mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
492                mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
493                break;
494
495            case MotionEvent.ACTION_UP:
496            case MotionEvent.ACTION_CANCEL:
497                if (DEBUG) Log.d(TAG, "up/cancel");
498                finishExpanding(false);
499                clearView();
500                break;
501        }
502        return true;
503    }
504
505    private void startExpanding(View v, int expandType) {
506        mExpansionStyle = expandType;
507        if (mExpanding &&  v == mCurrView) {
508            return;
509        }
510        mExpanding = true;
511        if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
512        mCallback.setUserLockedChild(v, true);
513        setView(v);
514        setGlow(GLOW_BASE);
515        mScaler.setView(v);
516        mOldHeight = mScaler.getHeight();
517        if (mCallback.canChildBeExpanded(v)) {
518            if (DEBUG) Log.d(TAG, "working on an expandable child");
519            mNaturalHeight = mScaler.getNaturalHeight(mLargeSize);
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        v.getParent().requestDisallowInterceptTouchEvent(true);
527    }
528
529    private void finishExpanding(boolean force) {
530        if (!mExpanding) return;
531
532        if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mCurrView);
533
534        float currentHeight = mScaler.getHeight();
535        float targetHeight = mSmallSize;
536        float h = mScaler.getHeight();
537        final boolean wasClosed = (mOldHeight == mSmallSize);
538        if (wasClosed) {
539            targetHeight = (force || currentHeight > mSmallSize) ? mNaturalHeight : mSmallSize;
540        } else {
541            targetHeight = (force || currentHeight < mNaturalHeight) ? mSmallSize : mNaturalHeight;
542        }
543        if (mScaleAnimation.isRunning()) {
544            mScaleAnimation.cancel();
545        }
546        setGlow(0f);
547        mCallback.setUserExpandedChild(mCurrView, h == mNaturalHeight);
548        if (targetHeight != currentHeight) {
549            mScaleAnimation.setFloatValues(targetHeight);
550            mScaleAnimation.setupStartValues();
551            mScaleAnimation.start();
552        }
553        mCallback.setUserLockedChild(mCurrView, false);
554
555        mExpanding = false;
556        mExpansionStyle = NONE;
557
558        if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed);
559        if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight);
560        if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize);
561        if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight);
562        if (DEBUG) Log.d(TAG, "scale was finished on view: " + mCurrView);
563    }
564
565    private void clearView() {
566        mCurrView = null;
567        mCurrViewTopGlow = null;
568        mCurrViewBottomGlow = null;
569    }
570
571    private void setView(View v) {
572        mCurrView = v;
573        if (v instanceof ViewGroup) {
574            ViewGroup g = (ViewGroup) v;
575            mCurrViewTopGlow = g.findViewById(R.id.top_glow);
576            mCurrViewBottomGlow = g.findViewById(R.id.bottom_glow);
577            if (DEBUG) {
578                String debugLog = "Looking for glows: " +
579                        (mCurrViewTopGlow != null ? "found top " : "didn't find top") +
580                        (mCurrViewBottomGlow != null ? "found bottom " : "didn't find bottom");
581                Log.v(TAG,  debugLog);
582            }
583        }
584    }
585
586    @Override
587    public void onClick(View v) {
588        startExpanding(v, STRETCH);
589        finishExpanding(true);
590        clearView();
591    }
592
593    /**
594     * Use this to abort any pending expansions in progress.
595     */
596    public void cancel() {
597        finishExpanding(true);
598        clearView();
599
600        // reset the gesture detector
601        mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
602    }
603
604    /**
605     * Triggers haptic feedback.
606     */
607    private synchronized void vibrate(long duration) {
608        if (mVibrator == null) {
609            mVibrator = (android.os.Vibrator)
610                    mContext.getSystemService(Context.VIBRATOR_SERVICE);
611        }
612        mVibrator.vibrate(duration, AudioManager.STREAM_SYSTEM);
613    }
614}
615
616