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