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