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