PanelView.java revision 9012958742c7a66b37ba5f2196f9086bb1980e6b
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
17package com.android.systemui.statusbar.phone;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.MotionEvent;
29import android.view.ViewConfiguration;
30import android.view.animation.AnimationUtils;
31import android.view.animation.Interpolator;
32import android.widget.FrameLayout;
33
34import com.android.systemui.R;
35import com.android.systemui.statusbar.FlingAnimationUtils;
36import com.android.systemui.statusbar.StatusBarState;
37
38import java.io.FileDescriptor;
39import java.io.PrintWriter;
40
41public abstract class PanelView extends FrameLayout {
42    public static final boolean DEBUG = PanelBar.DEBUG;
43    public static final String TAG = PanelView.class.getSimpleName();
44    protected float mOverExpansion;
45
46    private final void logf(String fmt, Object... args) {
47        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
48    }
49
50    protected PhoneStatusBar mStatusBar;
51    private float mPeekHeight;
52    private float mHintDistance;
53    private float mInitialOffsetOnTouch;
54    private float mExpandedFraction = 0;
55    private float mExpandedHeight = 0;
56    private boolean mJustPeeked;
57    private boolean mClosing;
58    private boolean mTracking;
59    private boolean mTouchSlopExceeded;
60    private int mTrackingPointer;
61    protected int mTouchSlop;
62
63    private ValueAnimator mHeightAnimator;
64    private ObjectAnimator mPeekAnimator;
65    private VelocityTrackerInterface mVelocityTracker;
66    private FlingAnimationUtils mFlingAnimationUtils;
67
68    PanelBar mBar;
69
70    protected int mMaxPanelHeight = -1;
71    private String mViewName;
72    private float mInitialTouchY;
73    private float mInitialTouchX;
74
75    private Interpolator mLinearOutSlowInInterpolator;
76    private Interpolator mBounceInterpolator;
77
78    protected void onExpandingFinished() {
79        mBar.onExpandingFinished();
80    }
81
82    protected void onExpandingStarted() {
83    }
84
85    private void runPeekAnimation() {
86        if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
87        if (mHeightAnimator != null) {
88            return;
89        }
90        if (mPeekAnimator == null) {
91            mPeekAnimator = ObjectAnimator.ofFloat(this,
92                    "expandedHeight", mPeekHeight)
93                .setDuration(250);
94        }
95        mPeekAnimator.start();
96    }
97
98    public PanelView(Context context, AttributeSet attrs) {
99        super(context, attrs);
100        mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f);
101        mLinearOutSlowInInterpolator =
102                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
103        mBounceInterpolator = new BounceInterpolator();
104    }
105
106    protected void loadDimens() {
107        final Resources res = getContext().getResources();
108        mPeekHeight = res.getDimension(R.dimen.peek_height)
109            + getPaddingBottom(); // our window might have a dropshadow
110
111        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
112        mTouchSlop = configuration.getScaledTouchSlop();
113        mHintDistance = res.getDimension(R.dimen.hint_move_distance);
114    }
115
116    private void trackMovement(MotionEvent event) {
117        // Add movement to velocity tracker using raw screen X and Y coordinates instead
118        // of window coordinates because the window frame may be moving at the same time.
119        float deltaX = event.getRawX() - event.getX();
120        float deltaY = event.getRawY() - event.getY();
121        event.offsetLocation(deltaX, deltaY);
122        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
123        event.offsetLocation(-deltaX, -deltaY);
124    }
125
126    @Override
127    public boolean onTouchEvent(MotionEvent event) {
128
129        /*
130         * We capture touch events here and update the expand height here in case according to
131         * the users fingers. This also handles multi-touch.
132         *
133         * If the user just clicks shortly, we give him a quick peek of the shade.
134         *
135         * Flinging is also enabled in order to open or close the shade.
136         */
137
138        int pointerIndex = event.findPointerIndex(mTrackingPointer);
139        if (pointerIndex < 0) {
140            pointerIndex = 0;
141            mTrackingPointer = event.getPointerId(pointerIndex);
142        }
143        final float y = event.getY(pointerIndex);
144        final float x = event.getX(pointerIndex);
145
146        boolean waitForTouchSlop = hasConflictingGestures();
147
148        switch (event.getActionMasked()) {
149            case MotionEvent.ACTION_DOWN:
150
151                mInitialTouchY = y;
152                mInitialTouchX = x;
153                mInitialOffsetOnTouch = mExpandedHeight;
154                mTouchSlopExceeded = false;
155                if (mVelocityTracker == null) {
156                    initVelocityTracker();
157                }
158                trackMovement(event);
159                if (!waitForTouchSlop || mHeightAnimator != null) {
160                    if (mHeightAnimator != null) {
161                        mHeightAnimator.cancel(); // end any outstanding animations
162                    }
163                    onTrackingStarted();
164                }
165                if (mExpandedHeight == 0) {
166                    mJustPeeked = true;
167                    runPeekAnimation();
168                }
169                break;
170
171            case MotionEvent.ACTION_POINTER_UP:
172                final int upPointer = event.getPointerId(event.getActionIndex());
173                if (mTrackingPointer == upPointer) {
174                    // gesture is ongoing, find a new pointer to track
175                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
176                    final float newY = event.getY(newIndex);
177                    final float newX = event.getX(newIndex);
178                    mTrackingPointer = event.getPointerId(newIndex);
179                    mInitialOffsetOnTouch = mExpandedHeight;
180                    mInitialTouchY = newY;
181                    mInitialTouchX = newX;
182                }
183                break;
184
185            case MotionEvent.ACTION_MOVE:
186                float h = y - mInitialTouchY;
187                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
188                    mTouchSlopExceeded = true;
189                    if (waitForTouchSlop && !mTracking) {
190                        mInitialOffsetOnTouch = mExpandedHeight;
191                        mInitialTouchX = x;
192                        mInitialTouchY = y;
193                        if (mHeightAnimator != null) {
194                            mHeightAnimator.cancel(); // end any outstanding animations
195                        }
196                        onTrackingStarted();
197                        h = 0;
198                    }
199                }
200                final float newHeight = h + mInitialOffsetOnTouch;
201                if (newHeight > mPeekHeight) {
202                    if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
203                        mPeekAnimator.cancel();
204                    }
205                    mJustPeeked = false;
206                }
207                if (!mJustPeeked && (!waitForTouchSlop || mTracking)) {
208                    setExpandedHeightInternal(newHeight);
209                    mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
210                }
211
212                trackMovement(event);
213                break;
214
215            case MotionEvent.ACTION_UP:
216            case MotionEvent.ACTION_CANCEL:
217                mTrackingPointer = -1;
218                trackMovement(event);
219                if (mTracking && mTouchSlopExceeded) {
220                    float vel = getCurrentVelocity();
221                    boolean expand = flingExpands(vel);
222                    onTrackingStopped(expand);
223                    fling(vel, expand);
224                } else {
225                    boolean expands = onEmptySpaceClick();
226                    onTrackingStopped(expands);
227                }
228                if (mVelocityTracker != null) {
229                    mVelocityTracker.recycle();
230                    mVelocityTracker = null;
231                }
232                break;
233        }
234        return !waitForTouchSlop || mTracking;
235    }
236
237    protected abstract boolean hasConflictingGestures();
238
239    protected void onTrackingStopped(boolean expand) {
240        mTracking = false;
241        mBar.onTrackingStopped(PanelView.this, expand);
242    }
243
244    protected void onTrackingStarted() {
245        mTracking = true;
246        mBar.onTrackingStarted(PanelView.this);
247        onExpandingStarted();
248    }
249
250    private float getCurrentVelocity() {
251
252        // the velocitytracker might be null if we got a bad input stream
253        if (mVelocityTracker == null) {
254            return 0;
255        }
256        mVelocityTracker.computeCurrentVelocity(1000);
257        return mVelocityTracker.getYVelocity();
258    }
259
260    @Override
261    public boolean onInterceptTouchEvent(MotionEvent event) {
262
263        /*
264         * If the user drags anywhere inside the panel we intercept it if he moves his finger
265         * upwards. This allows closing the shade from anywhere inside the panel.
266         *
267         * We only do this if the current content is scrolled to the bottom,
268         * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
269         * possible.
270         */
271        int pointerIndex = event.findPointerIndex(mTrackingPointer);
272        if (pointerIndex < 0) {
273            pointerIndex = 0;
274            mTrackingPointer = event.getPointerId(pointerIndex);
275        }
276        final float x = event.getX(pointerIndex);
277        final float y = event.getY(pointerIndex);
278        boolean scrolledToBottom = isScrolledToBottom();
279
280        switch (event.getActionMasked()) {
281            case MotionEvent.ACTION_DOWN:
282                if (mHeightAnimator != null) {
283                    mHeightAnimator.cancel(); // end any outstanding animations
284                    return true;
285                }
286                mInitialTouchY = y;
287                mInitialTouchX = x;
288                mTouchSlopExceeded = false;
289                initVelocityTracker();
290                trackMovement(event);
291                break;
292            case MotionEvent.ACTION_POINTER_UP:
293                final int upPointer = event.getPointerId(event.getActionIndex());
294                if (mTrackingPointer == upPointer) {
295                    // gesture is ongoing, find a new pointer to track
296                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
297                    mTrackingPointer = event.getPointerId(newIndex);
298                    mInitialTouchX = event.getX(newIndex);
299                    mInitialTouchY = event.getY(newIndex);
300                }
301                break;
302
303            case MotionEvent.ACTION_MOVE:
304                final float h = y - mInitialTouchY;
305                trackMovement(event);
306                if (scrolledToBottom) {
307                    if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) {
308                        mInitialOffsetOnTouch = mExpandedHeight;
309                        mInitialTouchY = y;
310                        mInitialTouchX = x;
311                        mTracking = true;
312                        mTouchSlopExceeded = true;
313                        onTrackingStarted();
314                        return true;
315                    }
316                }
317                break;
318        }
319        return false;
320    }
321
322    private void initVelocityTracker() {
323        if (mVelocityTracker != null) {
324            mVelocityTracker.recycle();
325        }
326        mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
327    }
328
329    protected boolean isScrolledToBottom() {
330        return true;
331    }
332
333    protected float getContentHeight() {
334        return mExpandedHeight;
335    }
336
337    @Override
338    protected void onFinishInflate() {
339        super.onFinishInflate();
340        loadDimens();
341    }
342
343    @Override
344    protected void onConfigurationChanged(Configuration newConfig) {
345        super.onConfigurationChanged(newConfig);
346        loadDimens();
347        mMaxPanelHeight = -1;
348    }
349
350    /**
351     * @param vel the current velocity of the motion
352     * @return whether a fling should expands the panel; contracts otherwise
353     */
354    private boolean flingExpands(float vel) {
355        if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
356            return getExpandedFraction() > 0.5f;
357        } else {
358            return vel > 0;
359        }
360    }
361
362    protected void fling(float vel, boolean expand) {
363        cancelPeek();
364        float target = expand ? getMaxPanelHeight() : 0.0f;
365        if (target == mExpandedHeight) {
366            onExpandingFinished();
367            mBar.panelExpansionChanged(this, mExpandedFraction);
368            return;
369        }
370        ValueAnimator animator = createHeightAnimator(target);
371        if (expand) {
372            mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
373        } else {
374            mFlingAnimationUtils.applyDismissing(animator, mExpandedHeight, target, vel,
375                    getHeight());
376
377            // Make it shorter if we run a canned animation
378            if (vel == 0) {
379                animator.setDuration((long) (animator.getDuration() / 1.75f));
380            }
381        }
382        animator.addListener(new AnimatorListenerAdapter() {
383            @Override
384            public void onAnimationEnd(Animator animation) {
385                mHeightAnimator = null;
386                onExpandingFinished();
387            }
388        });
389        animator.start();
390        mHeightAnimator = animator;
391    }
392
393    @Override
394    protected void onAttachedToWindow() {
395        super.onAttachedToWindow();
396        mViewName = getResources().getResourceName(getId());
397    }
398
399    public String getName() {
400        return mViewName;
401    }
402
403    @Override
404    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
405        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
406
407        if (DEBUG) logf("onMeasure(%d, %d) -> (%d, %d)",
408                widthMeasureSpec, heightMeasureSpec, getMeasuredWidth(), getMeasuredHeight());
409
410        // Did one of our children change size?
411        int newHeight = getMeasuredHeight();
412        if (newHeight > mMaxPanelHeight) {
413            // we only adapt the max height if it's bigger
414            mMaxPanelHeight = newHeight;
415            // If the user isn't actively poking us, let's rubberband to the content
416            if (!mTracking && mHeightAnimator == null
417                    && mExpandedHeight > 0 && mExpandedHeight != mMaxPanelHeight
418                    && mMaxPanelHeight > 0) {
419                mExpandedHeight = mMaxPanelHeight;
420            }
421        }
422    }
423
424    public void setExpandedHeight(float height) {
425        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
426        setExpandedHeightInternal(height);
427        mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
428    }
429
430    @Override
431    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
432        if (DEBUG) logf("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom,
433                (int)mExpandedHeight, mMaxPanelHeight);
434        super.onLayout(changed, left, top, right, bottom);
435        requestPanelHeightUpdate();
436    }
437
438    protected void requestPanelHeightUpdate() {
439        float currentMaxPanelHeight = getMaxPanelHeight();
440
441        // If the user isn't actively poking us, let's update the height
442        if (!mTracking && mHeightAnimator == null
443                && mExpandedHeight > 0 && currentMaxPanelHeight != mExpandedHeight) {
444            setExpandedHeightInternal(currentMaxPanelHeight);
445        }
446    }
447
448    public void setExpandedHeightInternal(float h) {
449        float fh = getMaxPanelHeight();
450        mExpandedHeight = Math.max(0, Math.min(fh, h));
451        float overExpansion = h - fh;
452        overExpansion = Math.max(0, overExpansion);
453        if (overExpansion != mOverExpansion) {
454            onOverExpansionChanged(overExpansion);
455        }
456
457        if (DEBUG) {
458            logf("setExpansion: height=%.1f fh=%.1f tracking=%s", h, fh, mTracking ? "T" : "f");
459        }
460
461        onHeightUpdated(mExpandedHeight);
462        mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : mExpandedHeight / fh);
463    }
464
465    protected void onOverExpansionChanged(float overExpansion) {
466        mOverExpansion = overExpansion;
467    }
468
469    protected abstract void onHeightUpdated(float expandedHeight);
470
471    /**
472     * This returns the maximum height of the panel. Children should override this if their
473     * desired height is not the full height.
474     *
475     * @return the default implementation simply returns the maximum height.
476     */
477    protected int getMaxPanelHeight() {
478        mMaxPanelHeight = Math.max(mMaxPanelHeight, getHeight());
479        return mMaxPanelHeight;
480    }
481
482    public void setExpandedFraction(float frac) {
483        setExpandedHeight(getMaxPanelHeight() * frac);
484    }
485
486    public float getExpandedHeight() {
487        return mExpandedHeight;
488    }
489
490    public float getExpandedFraction() {
491        return mExpandedFraction;
492    }
493
494    public boolean isFullyExpanded() {
495        return mExpandedHeight >= getMaxPanelHeight();
496    }
497
498    public boolean isFullyCollapsed() {
499        return mExpandedHeight <= 0;
500    }
501
502    public boolean isCollapsing() {
503        return mClosing;
504    }
505
506    public boolean isTracking() {
507        return mTracking;
508    }
509
510    public void setBar(PanelBar panelBar) {
511        mBar = panelBar;
512    }
513
514    public void collapse() {
515        // TODO: abort animation or ongoing touch
516        if (DEBUG) logf("collapse: " + this);
517        if (!isFullyCollapsed()) {
518            if (mHeightAnimator != null) {
519                mHeightAnimator.cancel();
520            }
521            mClosing = true;
522            onExpandingStarted();
523            fling(0, false /* expand */);
524        }
525    }
526
527    public void expand() {
528        if (DEBUG) logf("expand: " + this);
529        if (isFullyCollapsed()) {
530            mBar.startOpeningPanel(this);
531            onExpandingStarted();
532            fling(0, true /* expand */);
533        } else if (DEBUG) {
534            if (DEBUG) logf("skipping expansion: is expanded");
535        }
536    }
537
538    public void cancelPeek() {
539        if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
540            mPeekAnimator.cancel();
541        }
542    }
543
544    protected void startUnlockHintAnimation() {
545
546        // We don't need to hint the user if an animation is already running or the user is changing
547        // the expansion.
548        if (mHeightAnimator != null || mTracking) {
549            return;
550        }
551        cancelPeek();
552        onExpandingStarted();
553        startUnlockHintAnimationPhase1();
554        mStatusBar.onUnlockHintStarted();
555    }
556
557    /**
558     * Phase 1: Move everything upwards.
559     */
560    private void startUnlockHintAnimationPhase1() {
561        float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
562        ValueAnimator animator = createHeightAnimator(target);
563        animator.setDuration(250);
564        animator.setInterpolator(mLinearOutSlowInInterpolator);
565        animator.addListener(new AnimatorListenerAdapter() {
566            private boolean mCancelled;
567
568            @Override
569            public void onAnimationCancel(Animator animation) {
570                mCancelled = true;
571            }
572
573            @Override
574            public void onAnimationEnd(Animator animation) {
575                if (mCancelled) {
576                    mHeightAnimator = null;
577                    onExpandingFinished();
578                    mStatusBar.onUnlockHintFinished();
579                } else {
580                    startUnlockHintAnimationPhase2();
581                }
582            }
583        });
584        animator.start();
585        mHeightAnimator = animator;
586    }
587
588    /**
589     * Phase 2: Bounce down.
590     */
591    private void startUnlockHintAnimationPhase2() {
592        ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
593        animator.setDuration(450);
594        animator.setInterpolator(mBounceInterpolator);
595        animator.addListener(new AnimatorListenerAdapter() {
596            @Override
597            public void onAnimationEnd(Animator animation) {
598                mHeightAnimator = null;
599                onExpandingFinished();
600                mStatusBar.onUnlockHintFinished();
601            }
602        });
603        animator.start();
604        mHeightAnimator = animator;
605    }
606
607    private ValueAnimator createHeightAnimator(float targetHeight) {
608        ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
609        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
610            @Override
611            public void onAnimationUpdate(ValueAnimator animation) {
612                setExpandedHeight((Float) animation.getAnimatedValue());
613            }
614        });
615        return animator;
616    }
617
618    /**
619     * Gets called when the user performs a click anywhere in the empty area of the panel.
620     *
621     * @return whether the panel will be expanded after the action performed by this method
622     */
623    private boolean onEmptySpaceClick() {
624        switch (mStatusBar.getBarState()) {
625            case StatusBarState.KEYGUARD:
626                startUnlockHintAnimation();
627                return true;
628            case StatusBarState.SHADE_LOCKED:
629                // TODO: Go to Keyguard again.
630                return true;
631            case StatusBarState.SHADE:
632                collapse();
633                return false;
634            default:
635                return true;
636        }
637    }
638
639    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
640        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
641                + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s"
642                + "]",
643                this.getClass().getSimpleName(),
644                getExpandedHeight(),
645                getMaxPanelHeight(),
646                mClosing?"T":"f",
647                mTracking?"T":"f",
648                mJustPeeked?"T":"f",
649                mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
650                mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":"")
651        ));
652    }
653
654    public abstract void resetViews();
655}
656