PanelView.java revision 79d79c455beecf7a70460575ab21d9bf49767e2c
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.ViewTreeObserver;
31import android.view.animation.AnimationUtils;
32import android.view.animation.Interpolator;
33import android.widget.FrameLayout;
34
35import com.android.systemui.EventLogConstants;
36import com.android.systemui.EventLogTags;
37import com.android.systemui.R;
38import com.android.systemui.doze.DozeLog;
39import com.android.systemui.statusbar.FlingAnimationUtils;
40import com.android.systemui.statusbar.StatusBarState;
41import com.android.systemui.statusbar.policy.HeadsUpManager;
42
43import java.io.FileDescriptor;
44import java.io.PrintWriter;
45
46public abstract class PanelView extends FrameLayout {
47    public static final boolean DEBUG = PanelBar.DEBUG;
48    public static final String TAG = PanelView.class.getSimpleName();
49
50    private final void logf(String fmt, Object... args) {
51        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
52    }
53
54    protected PhoneStatusBar mStatusBar;
55    protected HeadsUpManager mHeadsUpManager;
56
57    private float mPeekHeight;
58    private float mHintDistance;
59    private int mEdgeTapAreaWidth;
60    private float mInitialOffsetOnTouch;
61    private boolean mCollapsedAndHeadsUpOnDown;
62    private float mExpandedFraction = 0;
63    protected float mExpandedHeight = 0;
64    private boolean mPanelClosedOnDown;
65    private boolean mHasLayoutedSinceDown;
66    private float mUpdateFlingVelocity;
67    private boolean mUpdateFlingOnLayout;
68    private boolean mPeekTouching;
69    private boolean mJustPeeked;
70    private boolean mClosing;
71    protected boolean mTracking;
72    private boolean mTouchSlopExceeded;
73    private int mTrackingPointer;
74    protected int mTouchSlop;
75    protected boolean mHintAnimationRunning;
76    private boolean mOverExpandedBeforeFling;
77    private boolean mTouchAboveFalsingThreshold;
78    private int mUnlockFalsingThreshold;
79    private boolean mTouchStartedInEmptyArea;
80    private boolean mMotionAborted;
81    private boolean mUpwardsWhenTresholdReached;
82    private boolean mAnimatingOnDown;
83
84    private ValueAnimator mHeightAnimator;
85    private ObjectAnimator mPeekAnimator;
86    private VelocityTrackerInterface mVelocityTracker;
87    private FlingAnimationUtils mFlingAnimationUtils;
88
89    /**
90     * Whether an instant expand request is currently pending and we are just waiting for layout.
91     */
92    private boolean mInstantExpanding;
93
94    PanelBar mBar;
95
96    private String mViewName;
97    private float mInitialTouchY;
98    private float mInitialTouchX;
99    private boolean mTouchDisabled;
100
101    private Interpolator mLinearOutSlowInInterpolator;
102    private Interpolator mFastOutSlowInInterpolator;
103    private Interpolator mBounceInterpolator;
104    protected KeyguardBottomAreaView mKeyguardBottomArea;
105
106    private boolean mPeekPending;
107    private boolean mCollapseAfterPeek;
108
109    /**
110     * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
111     */
112    private float mNextCollapseSpeedUpFactor = 1.0f;
113
114    private boolean mExpanding;
115    private boolean mGestureWaitForTouchSlop;
116    private boolean mIgnoreXTouchSlop;
117    private Runnable mPeekRunnable = new Runnable() {
118        @Override
119        public void run() {
120            mPeekPending = false;
121            runPeekAnimation();
122        }
123    };
124
125    protected void onExpandingFinished() {
126        endClosing();
127        mBar.onExpandingFinished();
128    }
129
130    protected void onExpandingStarted() {
131    }
132
133    private void notifyExpandingStarted() {
134        if (!mExpanding) {
135            mExpanding = true;
136            onExpandingStarted();
137        }
138    }
139
140    private void notifyExpandingFinished() {
141        if (mExpanding) {
142            mExpanding = false;
143            onExpandingFinished();
144        }
145    }
146
147    private void schedulePeek() {
148        mPeekPending = true;
149        long timeout = ViewConfiguration.getTapTimeout();
150        postOnAnimationDelayed(mPeekRunnable, timeout);
151        notifyBarPanelExpansionChanged();
152    }
153
154    private void runPeekAnimation() {
155        mPeekHeight = getPeekHeight();
156        if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
157        if (mHeightAnimator != null) {
158            return;
159        }
160        mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight)
161                .setDuration(250);
162        mPeekAnimator.setInterpolator(mLinearOutSlowInInterpolator);
163        mPeekAnimator.addListener(new AnimatorListenerAdapter() {
164            private boolean mCancelled;
165
166            @Override
167            public void onAnimationCancel(Animator animation) {
168                mCancelled = true;
169            }
170
171            @Override
172            public void onAnimationEnd(Animator animation) {
173                mPeekAnimator = null;
174                if (mCollapseAfterPeek && !mCancelled) {
175                    postOnAnimation(new Runnable() {
176                        @Override
177                        public void run() {
178                            collapse(false /* delayed */, 1.0f /* speedUpFactor */);
179                        }
180                    });
181                }
182                mCollapseAfterPeek = false;
183            }
184        });
185        notifyExpandingStarted();
186        mPeekAnimator.start();
187        mJustPeeked = true;
188    }
189
190    public PanelView(Context context, AttributeSet attrs) {
191        super(context, attrs);
192        mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f);
193        mFastOutSlowInInterpolator =
194                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
195        mLinearOutSlowInInterpolator =
196                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
197        mBounceInterpolator = new BounceInterpolator();
198    }
199
200    protected void loadDimens() {
201        final Resources res = getContext().getResources();
202        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
203        mTouchSlop = configuration.getScaledTouchSlop();
204        mHintDistance = res.getDimension(R.dimen.hint_move_distance);
205        mEdgeTapAreaWidth = res.getDimensionPixelSize(R.dimen.edge_tap_area_width);
206        mUnlockFalsingThreshold = res.getDimensionPixelSize(R.dimen.unlock_falsing_threshold);
207    }
208
209    private void trackMovement(MotionEvent event) {
210        // Add movement to velocity tracker using raw screen X and Y coordinates instead
211        // of window coordinates because the window frame may be moving at the same time.
212        float deltaX = event.getRawX() - event.getX();
213        float deltaY = event.getRawY() - event.getY();
214        event.offsetLocation(deltaX, deltaY);
215        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
216        event.offsetLocation(-deltaX, -deltaY);
217    }
218
219    public void setTouchDisabled(boolean disabled) {
220        mTouchDisabled = disabled;
221    }
222
223    @Override
224    public boolean onTouchEvent(MotionEvent event) {
225        if (mInstantExpanding || mTouchDisabled
226                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
227            return false;
228        }
229
230        /*
231         * We capture touch events here and update the expand height here in case according to
232         * the users fingers. This also handles multi-touch.
233         *
234         * If the user just clicks shortly, we give him a quick peek of the shade.
235         *
236         * Flinging is also enabled in order to open or close the shade.
237         */
238
239        int pointerIndex = event.findPointerIndex(mTrackingPointer);
240        if (pointerIndex < 0) {
241            pointerIndex = 0;
242            mTrackingPointer = event.getPointerId(pointerIndex);
243        }
244        final float x = event.getX(pointerIndex);
245        final float y = event.getY(pointerIndex);
246
247        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
248            mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();
249            mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
250        }
251
252        switch (event.getActionMasked()) {
253            case MotionEvent.ACTION_DOWN:
254                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
255                mJustPeeked = false;
256                mPanelClosedOnDown = isFullyCollapsed();
257                mHasLayoutedSinceDown = false;
258                mUpdateFlingOnLayout = false;
259                mMotionAborted = false;
260                mPeekTouching = mPanelClosedOnDown;
261                mTouchAboveFalsingThreshold = false;
262                mCollapsedAndHeadsUpOnDown = isFullyCollapsed()
263                        && mHeadsUpManager.hasPinnedHeadsUp();
264                if (mVelocityTracker == null) {
265                    initVelocityTracker();
266                }
267                trackMovement(event);
268                if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) ||
269                        mPeekPending || mPeekAnimator != null) {
270                    cancelHeightAnimator();
271                    cancelPeek();
272                    mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
273                            || mPeekPending || mPeekAnimator != null;
274                    onTrackingStarted();
275                }
276                if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) {
277                    schedulePeek();
278                }
279                break;
280
281            case MotionEvent.ACTION_POINTER_UP:
282                final int upPointer = event.getPointerId(event.getActionIndex());
283                if (mTrackingPointer == upPointer) {
284                    // gesture is ongoing, find a new pointer to track
285                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
286                    final float newY = event.getY(newIndex);
287                    final float newX = event.getX(newIndex);
288                    mTrackingPointer = event.getPointerId(newIndex);
289                    startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
290                }
291                break;
292            case MotionEvent.ACTION_POINTER_DOWN:
293                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
294                    mMotionAborted = true;
295                    endMotionEvent(event, x, y, true /* forceCancel */);
296                    return false;
297                }
298                break;
299            case MotionEvent.ACTION_MOVE:
300                float h = y - mInitialTouchY;
301
302                // If the panel was collapsed when touching, we only need to check for the
303                // y-component of the gesture, as we have no conflicting horizontal gesture.
304                if (Math.abs(h) > mTouchSlop
305                        && (Math.abs(h) > Math.abs(x - mInitialTouchX)
306                                || mIgnoreXTouchSlop)) {
307                    mTouchSlopExceeded = true;
308                    if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
309                        if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
310                            startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
311                            h = 0;
312                        }
313                        cancelHeightAnimator();
314                        removeCallbacks(mPeekRunnable);
315                        mPeekPending = false;
316                        onTrackingStarted();
317                    }
318                }
319                final float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
320                if (newHeight > mPeekHeight) {
321                    if (mPeekAnimator != null) {
322                        mPeekAnimator.cancel();
323                    }
324                    mJustPeeked = false;
325                }
326                if (-h >= getFalsingThreshold()) {
327                    mTouchAboveFalsingThreshold = true;
328                    mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);
329                }
330                if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
331                    setExpandedHeightInternal(newHeight);
332                }
333
334                trackMovement(event);
335                break;
336
337            case MotionEvent.ACTION_UP:
338            case MotionEvent.ACTION_CANCEL:
339                trackMovement(event);
340                endMotionEvent(event, x, y, false /* forceCancel */);
341                break;
342        }
343        return !mGestureWaitForTouchSlop || mTracking;
344    }
345
346    /**
347     * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
348     * horizontal direction
349     */
350    private boolean isDirectionUpwards(float x, float y) {
351        float xDiff = x - mInitialTouchX;
352        float yDiff = y - mInitialTouchY;
353        if (yDiff >= 0) {
354            return false;
355        }
356        return Math.abs(yDiff) >= Math.abs(xDiff);
357    }
358
359    protected void startExpandMotion(float newX, float newY, boolean startTracking,
360            float expandedHeight) {
361        mInitialOffsetOnTouch = expandedHeight;
362        mInitialTouchY = newY;
363        mInitialTouchX = newX;
364        if (startTracking) {
365            mTouchSlopExceeded = true;
366            onTrackingStarted();
367        }
368    }
369
370    private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
371        mTrackingPointer = -1;
372        if ((mTracking && mTouchSlopExceeded)
373                || Math.abs(x - mInitialTouchX) > mTouchSlop
374                || Math.abs(y - mInitialTouchY) > mTouchSlop
375                || event.getActionMasked() == MotionEvent.ACTION_CANCEL
376                || forceCancel) {
377            float vel = 0f;
378            float vectorVel = 0f;
379            if (mVelocityTracker != null) {
380                mVelocityTracker.computeCurrentVelocity(1000);
381                vel = mVelocityTracker.getYVelocity();
382                vectorVel = (float) Math.hypot(
383                        mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
384            }
385            boolean expand = flingExpands(vel, vectorVel, x, y)
386                    || event.getActionMasked() == MotionEvent.ACTION_CANCEL
387                    || forceCancel;
388            DozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
389                    mStatusBar.isFalsingThresholdNeeded(),
390                    mStatusBar.isScreenOnComingFromTouch());
391                    // Log collapse gesture if on lock screen.
392                    if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
393                        float displayDensity = mStatusBar.getDisplayDensity();
394                        int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity);
395                        int velocityDp = (int) Math.abs(vel / displayDensity);
396                        EventLogTags.writeSysuiLockscreenGesture(
397                                EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_UP_UNLOCK,
398                                heightDp, velocityDp);
399                    }
400            fling(vel, expand, isFalseTouch(x, y));
401            onTrackingStopped(expand);
402            mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
403            if (mUpdateFlingOnLayout) {
404                mUpdateFlingVelocity = vel;
405            }
406        } else {
407            boolean expands = onEmptySpaceClick(mInitialTouchX);
408            onTrackingStopped(expands);
409        }
410
411        if (mVelocityTracker != null) {
412            mVelocityTracker.recycle();
413            mVelocityTracker = null;
414        }
415        mPeekTouching = false;
416    }
417
418    private int getFalsingThreshold() {
419        float factor = mStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f;
420        return (int) (mUnlockFalsingThreshold * factor);
421    }
422
423    protected abstract boolean hasConflictingGestures();
424
425    protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y);
426
427    protected void onTrackingStopped(boolean expand) {
428        mTracking = false;
429        mBar.onTrackingStopped(PanelView.this, expand);
430        notifyBarPanelExpansionChanged();
431    }
432
433    protected void onTrackingStarted() {
434        endClosing();
435        mTracking = true;
436        mCollapseAfterPeek = false;
437        mBar.onTrackingStarted(PanelView.this);
438        notifyExpandingStarted();
439        notifyBarPanelExpansionChanged();
440    }
441
442    @Override
443    public boolean onInterceptTouchEvent(MotionEvent event) {
444        if (mInstantExpanding
445                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
446            return false;
447        }
448
449        /*
450         * If the user drags anywhere inside the panel we intercept it if he moves his finger
451         * upwards. This allows closing the shade from anywhere inside the panel.
452         *
453         * We only do this if the current content is scrolled to the bottom,
454         * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
455         * possible.
456         */
457        int pointerIndex = event.findPointerIndex(mTrackingPointer);
458        if (pointerIndex < 0) {
459            pointerIndex = 0;
460            mTrackingPointer = event.getPointerId(pointerIndex);
461        }
462        final float x = event.getX(pointerIndex);
463        final float y = event.getY(pointerIndex);
464        boolean scrolledToBottom = isScrolledToBottom();
465
466        switch (event.getActionMasked()) {
467            case MotionEvent.ACTION_DOWN:
468                mStatusBar.userActivity();
469                mAnimatingOnDown = mHeightAnimator != null;
470                if (mAnimatingOnDown && mClosing && !mHintAnimationRunning || mPeekPending || mPeekAnimator != null) {
471                    cancelHeightAnimator();
472                    cancelPeek();
473                    mTouchSlopExceeded = true;
474                    return true;
475                }
476                mInitialTouchY = y;
477                mInitialTouchX = x;
478                mTouchStartedInEmptyArea = !isInContentBounds(x, y);
479                mTouchSlopExceeded = false;
480                mJustPeeked = false;
481                mMotionAborted = false;
482                mPanelClosedOnDown = isFullyCollapsed();
483                mCollapsedAndHeadsUpOnDown = false;
484                mHasLayoutedSinceDown = false;
485                mUpdateFlingOnLayout = false;
486                mTouchAboveFalsingThreshold = false;
487                initVelocityTracker();
488                trackMovement(event);
489                break;
490            case MotionEvent.ACTION_POINTER_UP:
491                final int upPointer = event.getPointerId(event.getActionIndex());
492                if (mTrackingPointer == upPointer) {
493                    // gesture is ongoing, find a new pointer to track
494                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
495                    mTrackingPointer = event.getPointerId(newIndex);
496                    mInitialTouchX = event.getX(newIndex);
497                    mInitialTouchY = event.getY(newIndex);
498                }
499                break;
500            case MotionEvent.ACTION_POINTER_DOWN:
501                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
502                    mMotionAborted = true;
503                    if (mVelocityTracker != null) {
504                        mVelocityTracker.recycle();
505                        mVelocityTracker = null;
506                    }
507                }
508                break;
509            case MotionEvent.ACTION_MOVE:
510                final float h = y - mInitialTouchY;
511                trackMovement(event);
512                if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) {
513                    float hAbs = Math.abs(h);
514                    if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop))
515                            && hAbs > Math.abs(x - mInitialTouchX)) {
516                        cancelHeightAnimator();
517                        startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
518                        return true;
519                    }
520                }
521                break;
522            case MotionEvent.ACTION_CANCEL:
523            case MotionEvent.ACTION_UP:
524                if (mVelocityTracker != null) {
525                    mVelocityTracker.recycle();
526                    mVelocityTracker = null;
527                }
528                break;
529        }
530        return false;
531    }
532
533    /**
534     * @return Whether a pair of coordinates are inside the visible view content bounds.
535     */
536    protected abstract boolean isInContentBounds(float x, float y);
537
538    private void cancelHeightAnimator() {
539        if (mHeightAnimator != null) {
540            mHeightAnimator.cancel();
541        }
542        endClosing();
543    }
544
545    private void endClosing() {
546        if (mClosing) {
547            mClosing = false;
548            onClosingFinished();
549        }
550    }
551
552    private void initVelocityTracker() {
553        if (mVelocityTracker != null) {
554            mVelocityTracker.recycle();
555        }
556        mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
557    }
558
559    protected boolean isScrolledToBottom() {
560        return true;
561    }
562
563    protected float getContentHeight() {
564        return mExpandedHeight;
565    }
566
567    @Override
568    protected void onFinishInflate() {
569        super.onFinishInflate();
570        loadDimens();
571    }
572
573    @Override
574    protected void onConfigurationChanged(Configuration newConfig) {
575        super.onConfigurationChanged(newConfig);
576        loadDimens();
577    }
578
579    /**
580     * @param vel the current vertical velocity of the motion
581     * @param vectorVel the length of the vectorial velocity
582     * @return whether a fling should expands the panel; contracts otherwise
583     */
584    protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
585        if (isFalseTouch(x, y)) {
586            return true;
587        }
588        if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
589            return getExpandedFraction() > 0.5f;
590        } else {
591            return vel > 0;
592        }
593    }
594
595    /**
596     * @param x the final x-coordinate when the finger was lifted
597     * @param y the final y-coordinate when the finger was lifted
598     * @return whether this motion should be regarded as a false touch
599     */
600    private boolean isFalseTouch(float x, float y) {
601        if (!mStatusBar.isFalsingThresholdNeeded()) {
602            return false;
603        }
604        if (!mTouchAboveFalsingThreshold) {
605            return true;
606        }
607        if (mUpwardsWhenTresholdReached) {
608            return false;
609        }
610        return !isDirectionUpwards(x, y);
611    }
612
613    protected void fling(float vel, boolean expand) {
614        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
615    }
616
617    protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
618        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
619    }
620
621    protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
622            boolean expandBecauseOfFalsing) {
623        cancelPeek();
624        float target = expand ? getMaxPanelHeight() : 0.0f;
625        if (!expand) {
626            mClosing = true;
627        }
628        flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
629    }
630
631    protected void flingToHeight(float vel, boolean expand, float target,
632            float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
633        // Hack to make the expand transition look nice when clear all button is visible - we make
634        // the animation only to the last notification, and then jump to the maximum panel height so
635        // clear all just fades in and the decelerating motion is towards the last notification.
636        final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible()
637                && mExpandedHeight < getMaxPanelHeight() - getClearAllHeight()
638                && !isClearAllVisible();
639        if (clearAllExpandHack) {
640            target = getMaxPanelHeight() - getClearAllHeight();
641        }
642        if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {
643            notifyExpandingFinished();
644            return;
645        }
646        mOverExpandedBeforeFling = getOverExpansionAmount() > 0f;
647        ValueAnimator animator = createHeightAnimator(target);
648        if (expand) {
649            if (expandBecauseOfFalsing) {
650                vel = 0;
651            }
652            mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
653            if (expandBecauseOfFalsing) {
654                animator.setDuration(350);
655            }
656        } else {
657            mFlingAnimationUtils.applyDismissing(animator, mExpandedHeight, target, vel,
658                    getHeight());
659
660            // Make it shorter if we run a canned animation
661            if (vel == 0) {
662                animator.setDuration((long)
663                        (animator.getDuration() * getCannedFlingDurationFactor()
664                                / collapseSpeedUpFactor));
665            }
666        }
667        animator.addListener(new AnimatorListenerAdapter() {
668            private boolean mCancelled;
669
670            @Override
671            public void onAnimationCancel(Animator animation) {
672                mCancelled = true;
673            }
674
675            @Override
676            public void onAnimationEnd(Animator animation) {
677                if (clearAllExpandHack && !mCancelled) {
678                    setExpandedHeightInternal(getMaxPanelHeight());
679                }
680                mHeightAnimator = null;
681                if (!mCancelled) {
682                    notifyExpandingFinished();
683                }
684                notifyBarPanelExpansionChanged();
685            }
686        });
687        mHeightAnimator = animator;
688        animator.start();
689    }
690
691    @Override
692    protected void onAttachedToWindow() {
693        super.onAttachedToWindow();
694        mViewName = getResources().getResourceName(getId());
695    }
696
697    public String getName() {
698        return mViewName;
699    }
700
701    public void setExpandedHeight(float height) {
702        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
703        setExpandedHeightInternal(height + getOverExpansionPixels());
704    }
705
706    @Override
707    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
708        super.onLayout(changed, left, top, right, bottom);
709        requestPanelHeightUpdate();
710        mHasLayoutedSinceDown = true;
711        if (mUpdateFlingOnLayout) {
712            abortAnimations();
713            fling(mUpdateFlingVelocity, true /* expands */);
714            mUpdateFlingOnLayout = false;
715        }
716    }
717
718    protected void requestPanelHeightUpdate() {
719        float currentMaxPanelHeight = getMaxPanelHeight();
720
721        // If the user isn't actively poking us, let's update the height
722        if ((!mTracking || isTrackingBlocked())
723                && mHeightAnimator == null
724                && !isFullyCollapsed()
725                && currentMaxPanelHeight != mExpandedHeight
726                && !mPeekPending
727                && mPeekAnimator == null
728                && !mPeekTouching) {
729            setExpandedHeight(currentMaxPanelHeight);
730        }
731    }
732
733    public void setExpandedHeightInternal(float h) {
734        float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
735        if (mHeightAnimator == null) {
736            float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
737            if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
738                setOverExpansion(overExpansionPixels, true /* isPixels */);
739            }
740            mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
741        } else {
742            mExpandedHeight = h;
743            if (mOverExpandedBeforeFling) {
744                setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
745            }
746        }
747
748        mExpandedHeight = Math.max(0, mExpandedHeight);
749        mExpandedFraction = Math.min(1f, fhWithoutOverExpansion == 0
750                ? 0
751                : mExpandedHeight / fhWithoutOverExpansion);
752        onHeightUpdated(mExpandedHeight);
753        notifyBarPanelExpansionChanged();
754    }
755
756    /**
757     * @return true if the panel tracking should be temporarily blocked; this is used when a
758     *         conflicting gesture (opening QS) is happening
759     */
760    protected abstract boolean isTrackingBlocked();
761
762    protected abstract void setOverExpansion(float overExpansion, boolean isPixels);
763
764    protected abstract void onHeightUpdated(float expandedHeight);
765
766    protected abstract float getOverExpansionAmount();
767
768    protected abstract float getOverExpansionPixels();
769
770    /**
771     * This returns the maximum height of the panel. Children should override this if their
772     * desired height is not the full height.
773     *
774     * @return the default implementation simply returns the maximum height.
775     */
776    protected abstract int getMaxPanelHeight();
777
778    public void setExpandedFraction(float frac) {
779        setExpandedHeight(getMaxPanelHeight() * frac);
780    }
781
782    public float getExpandedHeight() {
783        return mExpandedHeight;
784    }
785
786    public float getExpandedFraction() {
787        return mExpandedFraction;
788    }
789
790    public boolean isFullyExpanded() {
791        return mExpandedHeight >= getMaxPanelHeight();
792    }
793
794    public boolean isFullyCollapsed() {
795        return mExpandedHeight <= 0;
796    }
797
798    public boolean isCollapsing() {
799        return mClosing;
800    }
801
802    public boolean isTracking() {
803        return mTracking;
804    }
805
806    public void setBar(PanelBar panelBar) {
807        mBar = panelBar;
808    }
809
810    public void collapse(boolean delayed, float speedUpFactor) {
811        if (DEBUG) logf("collapse: " + this);
812        if (mPeekPending || mPeekAnimator != null) {
813            mCollapseAfterPeek = true;
814            if (mPeekPending) {
815
816                // We know that the whole gesture is just a peek triggered by a simple click, so
817                // better start it now.
818                removeCallbacks(mPeekRunnable);
819                mPeekRunnable.run();
820            }
821        } else if (!isFullyCollapsed() && !mTracking && !mClosing) {
822            cancelHeightAnimator();
823            mClosing = true;
824            notifyExpandingStarted();
825            if (delayed) {
826                mNextCollapseSpeedUpFactor = speedUpFactor;
827                postDelayed(mFlingCollapseRunnable, 120);
828            } else {
829                fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
830            }
831        }
832    }
833
834    private final Runnable mFlingCollapseRunnable = new Runnable() {
835        @Override
836        public void run() {
837            fling(0, false /* expand */, mNextCollapseSpeedUpFactor,
838                    false /* expandBecauseOfFalsing */);
839        }
840    };
841
842    public void expand() {
843        if (DEBUG) logf("expand: " + this);
844        if (isFullyCollapsed()) {
845            mBar.startOpeningPanel(this);
846            notifyExpandingStarted();
847            fling(0, true /* expand */);
848        } else if (DEBUG) {
849            if (DEBUG) logf("skipping expansion: is expanded");
850        }
851    }
852
853    public void cancelPeek() {
854        if (mPeekAnimator != null) {
855            mPeekAnimator.cancel();
856        }
857        removeCallbacks(mPeekRunnable);
858        mPeekPending = false;
859
860        // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also
861        // notify mBar that we might have closed ourselves.
862        notifyBarPanelExpansionChanged();
863    }
864
865    public void instantExpand() {
866        mInstantExpanding = true;
867        mUpdateFlingOnLayout = false;
868        abortAnimations();
869        cancelPeek();
870        if (mTracking) {
871            onTrackingStopped(true /* expands */); // The panel is expanded after this call.
872        }
873        if (mExpanding) {
874            notifyExpandingFinished();
875        }
876        notifyBarPanelExpansionChanged();
877
878        // Wait for window manager to pickup the change, so we know the maximum height of the panel
879        // then.
880        getViewTreeObserver().addOnGlobalLayoutListener(
881                new ViewTreeObserver.OnGlobalLayoutListener() {
882                    @Override
883                    public void onGlobalLayout() {
884                        if (mStatusBar.getStatusBarWindow().getHeight()
885                                != mStatusBar.getStatusBarHeight()) {
886                            getViewTreeObserver().removeOnGlobalLayoutListener(this);
887                            setExpandedFraction(1f);
888                            mInstantExpanding = false;
889                        }
890                    }
891                });
892
893        // Make sure a layout really happens.
894        requestLayout();
895    }
896
897    public void instantCollapse() {
898        abortAnimations();
899        setExpandedFraction(0f);
900        if (mExpanding) {
901            notifyExpandingFinished();
902        }
903    }
904
905    private void abortAnimations() {
906        cancelPeek();
907        cancelHeightAnimator();
908        removeCallbacks(mPostCollapseRunnable);
909        removeCallbacks(mFlingCollapseRunnable);
910    }
911
912    protected void onClosingFinished() {
913        mBar.onClosingFinished();
914    }
915
916
917    protected void startUnlockHintAnimation() {
918
919        // We don't need to hint the user if an animation is already running or the user is changing
920        // the expansion.
921        if (mHeightAnimator != null || mTracking) {
922            return;
923        }
924        cancelPeek();
925        notifyExpandingStarted();
926        startUnlockHintAnimationPhase1(new Runnable() {
927            @Override
928            public void run() {
929                notifyExpandingFinished();
930                mStatusBar.onHintFinished();
931                mHintAnimationRunning = false;
932            }
933        });
934        mStatusBar.onUnlockHintStarted();
935        mHintAnimationRunning = true;
936    }
937
938    /**
939     * Phase 1: Move everything upwards.
940     */
941    private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
942        float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
943        ValueAnimator animator = createHeightAnimator(target);
944        animator.setDuration(250);
945        animator.setInterpolator(mFastOutSlowInInterpolator);
946        animator.addListener(new AnimatorListenerAdapter() {
947            private boolean mCancelled;
948
949            @Override
950            public void onAnimationCancel(Animator animation) {
951                mCancelled = true;
952            }
953
954            @Override
955            public void onAnimationEnd(Animator animation) {
956                if (mCancelled) {
957                    mHeightAnimator = null;
958                    onAnimationFinished.run();
959                } else {
960                    startUnlockHintAnimationPhase2(onAnimationFinished);
961                }
962            }
963        });
964        animator.start();
965        mHeightAnimator = animator;
966        mKeyguardBottomArea.getIndicationView().animate()
967                .translationY(-mHintDistance)
968                .setDuration(250)
969                .setInterpolator(mFastOutSlowInInterpolator)
970                .withEndAction(new Runnable() {
971                    @Override
972                    public void run() {
973                        mKeyguardBottomArea.getIndicationView().animate()
974                                .translationY(0)
975                                .setDuration(450)
976                                .setInterpolator(mBounceInterpolator)
977                                .start();
978                    }
979                })
980                .start();
981    }
982
983    /**
984     * Phase 2: Bounce down.
985     */
986    private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
987        ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
988        animator.setDuration(450);
989        animator.setInterpolator(mBounceInterpolator);
990        animator.addListener(new AnimatorListenerAdapter() {
991            @Override
992            public void onAnimationEnd(Animator animation) {
993                mHeightAnimator = null;
994                onAnimationFinished.run();
995                notifyBarPanelExpansionChanged();
996            }
997        });
998        animator.start();
999        mHeightAnimator = animator;
1000    }
1001
1002    private ValueAnimator createHeightAnimator(float targetHeight) {
1003        ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
1004        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1005            @Override
1006            public void onAnimationUpdate(ValueAnimator animation) {
1007                setExpandedHeightInternal((Float) animation.getAnimatedValue());
1008            }
1009        });
1010        return animator;
1011    }
1012
1013    protected void notifyBarPanelExpansionChanged() {
1014        mBar.panelExpansionChanged(this, mExpandedFraction, mExpandedFraction > 0f || mPeekPending
1015                || mPeekAnimator != null || mInstantExpanding || mHeadsUpManager.hasPinnedHeadsUp()
1016                || mTracking || mHeightAnimator != null);
1017    }
1018
1019    /**
1020     * Gets called when the user performs a click anywhere in the empty area of the panel.
1021     *
1022     * @return whether the panel will be expanded after the action performed by this method
1023     */
1024    protected boolean onEmptySpaceClick(float x) {
1025        if (mHintAnimationRunning) {
1026            return true;
1027        }
1028        return onMiddleClicked();
1029    }
1030
1031    protected final Runnable mPostCollapseRunnable = new Runnable() {
1032        @Override
1033        public void run() {
1034            collapse(false /* delayed */, 1.0f /* speedUpFactor */);
1035        }
1036    };
1037
1038    protected abstract boolean onMiddleClicked();
1039
1040    protected abstract boolean isDozing();
1041
1042    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1043        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
1044                + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s touchDisabled=%s"
1045                + "]",
1046                this.getClass().getSimpleName(),
1047                getExpandedHeight(),
1048                getMaxPanelHeight(),
1049                mClosing?"T":"f",
1050                mTracking?"T":"f",
1051                mJustPeeked?"T":"f",
1052                mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
1053                mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":""),
1054                mTouchDisabled?"T":"f"
1055        ));
1056    }
1057
1058    public abstract void resetViews();
1059
1060    protected abstract float getPeekHeight();
1061
1062    protected abstract float getCannedFlingDurationFactor();
1063
1064    /**
1065     * @return whether "Clear all" button will be visible when the panel is fully expanded
1066     */
1067    protected abstract boolean fullyExpandedClearAllVisible();
1068
1069    protected abstract boolean isClearAllVisible();
1070
1071    /**
1072     * @return the height of the clear all button, in pixels
1073     */
1074    protected abstract int getClearAllHeight();
1075
1076    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
1077        mHeadsUpManager = headsUpManager;
1078    }
1079}
1080