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