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 java.io.FileDescriptor;
     20 import java.io.PrintWriter;
     21 import java.util.ArrayDeque;
     22 import java.util.Iterator;
     23 
     24 import android.animation.ObjectAnimator;
     25 import android.animation.TimeAnimator;
     26 import android.animation.TimeAnimator.TimeListener;
     27 import android.content.Context;
     28 import android.content.res.Resources;
     29 import android.util.AttributeSet;
     30 import android.util.Slog;
     31 import android.view.MotionEvent;
     32 import android.view.View;
     33 import android.widget.FrameLayout;
     34 
     35 import com.android.systemui.R;
     36 
     37 public class PanelView extends FrameLayout {
     38     public static final boolean DEBUG = PanelBar.DEBUG;
     39     public static final String TAG = PanelView.class.getSimpleName();
     40 
     41     public static final boolean DEBUG_NAN = true; // http://b/7686690
     42 
     43     public final void LOG(String fmt, Object... args) {
     44         if (!DEBUG) return;
     45         Slog.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
     46     }
     47 
     48     public static final boolean BRAKES = false;
     49     private boolean mRubberbandingEnabled = true;
     50 
     51     private float mSelfExpandVelocityPx; // classic value: 2000px/s
     52     private float mSelfCollapseVelocityPx; // classic value: 2000px/s (will be negated to collapse "up")
     53     private float mFlingExpandMinVelocityPx; // classic value: 200px/s
     54     private float mFlingCollapseMinVelocityPx; // classic value: 200px/s
     55     private float mCollapseMinDisplayFraction; // classic value: 0.08 (25px/min(320px,480px) on G1)
     56     private float mExpandMinDisplayFraction; // classic value: 0.5 (drag open halfway to expand)
     57     private float mFlingGestureMaxXVelocityPx; // classic value: 150px/s
     58 
     59     private float mFlingGestureMinDistPx;
     60 
     61     private float mExpandAccelPx; // classic value: 2000px/s/s
     62     private float mCollapseAccelPx; // classic value: 2000px/s/s (will be negated to collapse "up")
     63 
     64     private float mFlingGestureMaxOutputVelocityPx; // how fast can it really go? (should be a little
     65                                                     // faster than mSelfCollapseVelocityPx)
     66 
     67     private float mCollapseBrakingDistancePx = 200; // XXX Resource
     68     private float mExpandBrakingDistancePx = 150; // XXX Resource
     69     private float mBrakingSpeedPx = 150; // XXX Resource
     70 
     71     private View mHandleView;
     72     private float mPeekHeight;
     73     private float mTouchOffset;
     74     private float mExpandedFraction = 0;
     75     private float mExpandedHeight = 0;
     76     private boolean mJustPeeked;
     77     private boolean mClosing;
     78     private boolean mRubberbanding;
     79     private boolean mTracking;
     80 
     81     private TimeAnimator mTimeAnimator;
     82     private ObjectAnimator mPeekAnimator;
     83     private FlingTracker mVelocityTracker;
     84 
     85     /**
     86      * A very simple low-pass velocity filter for motion events; not nearly as sophisticated as
     87      * VelocityTracker but optimized for the kinds of gestures we expect to see in status bar
     88      * panels.
     89      */
     90     private static class FlingTracker {
     91         static final boolean DEBUG = false;
     92         final int MAX_EVENTS = 8;
     93         final float DECAY = 0.75f;
     94         ArrayDeque<MotionEventCopy> mEventBuf = new ArrayDeque<MotionEventCopy>(MAX_EVENTS);
     95         float mVX, mVY = 0;
     96         private static class MotionEventCopy {
     97             public MotionEventCopy(float x2, float y2, long eventTime) {
     98                 this.x = x2;
     99                 this.y = y2;
    100                 this.t = eventTime;
    101             }
    102             public float x, y;
    103             public long t;
    104         }
    105         public FlingTracker() {
    106         }
    107         public void addMovement(MotionEvent event) {
    108             if (mEventBuf.size() == MAX_EVENTS) {
    109                 mEventBuf.remove();
    110             }
    111             mEventBuf.add(new MotionEventCopy(event.getX(), event.getY(), event.getEventTime()));
    112         }
    113         public void computeCurrentVelocity(long timebase) {
    114             if (FlingTracker.DEBUG) {
    115                 Slog.v("FlingTracker", "computing velocities for " + mEventBuf.size() + " events");
    116             }
    117             mVX = mVY = 0;
    118             MotionEventCopy last = null;
    119             int i = 0;
    120             float totalweight = 0f;
    121             float weight = 10f;
    122             for (final Iterator<MotionEventCopy> iter = mEventBuf.descendingIterator();
    123                     iter.hasNext();) {
    124                 final MotionEventCopy event = iter.next();
    125                 if (last != null) {
    126                     final float dt = (float) (event.t - last.t) / timebase;
    127                     final float dx = (event.x - last.x);
    128                     final float dy = (event.y - last.y);
    129                     if (FlingTracker.DEBUG) {
    130                         Slog.v("FlingTracker", String.format("   [%d] dx=%.1f dy=%.1f dt=%.0f vx=%.1f vy=%.1f",
    131                                 i,
    132                                 dx, dy, dt,
    133                                 (dx/dt),
    134                                 (dy/dt)
    135                                 ));
    136                     }
    137                     mVX += weight * dx / dt;
    138                     mVY += weight * dy / dt;
    139                     totalweight += weight;
    140                     weight *= DECAY;
    141                 }
    142                 last = event;
    143                 i++;
    144             }
    145             if (totalweight > 0) {
    146                 mVX /= totalweight;
    147                 mVY /= totalweight;
    148             } else {
    149                 if (DEBUG_NAN) {
    150                     Slog.v("FlingTracker", "computeCurrentVelocity warning: totalweight=0",
    151                             new Throwable());
    152                 }
    153                 // so as not to contaminate the velocities with NaN
    154                 mVX = mVY = 0;
    155             }
    156 
    157             if (FlingTracker.DEBUG) {
    158                 Slog.v("FlingTracker", "computed: vx=" + mVX + " vy=" + mVY);
    159             }
    160         }
    161         public float getXVelocity() {
    162             if (Float.isNaN(mVX)) {
    163                 if (DEBUG_NAN) {
    164                     Slog.v("FlingTracker", "warning: vx=NaN");
    165                 }
    166                 mVX = 0;
    167             }
    168             return mVX;
    169         }
    170         public float getYVelocity() {
    171             if (Float.isNaN(mVY)) {
    172                 if (DEBUG_NAN) {
    173                     Slog.v("FlingTracker", "warning: vx=NaN");
    174                 }
    175                 mVY = 0;
    176             }
    177             return mVY;
    178         }
    179         public void recycle() {
    180             mEventBuf.clear();
    181         }
    182 
    183         static FlingTracker sTracker;
    184         static FlingTracker obtain() {
    185             if (sTracker == null) {
    186                 sTracker = new FlingTracker();
    187             }
    188             return sTracker;
    189         }
    190     }
    191 
    192     private int[] mAbsPos = new int[2];
    193     PanelBar mBar;
    194 
    195     private final TimeListener mAnimationCallback = new TimeListener() {
    196         @Override
    197         public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
    198             animationTick(deltaTime);
    199         }
    200     };
    201 
    202     private final Runnable mStopAnimator = new Runnable() {
    203         @Override
    204         public void run() {
    205             if (mTimeAnimator != null && mTimeAnimator.isStarted()) {
    206                 mTimeAnimator.end();
    207                 mRubberbanding = false;
    208                 mClosing = false;
    209             }
    210         }
    211     };
    212 
    213     private float mVel, mAccel;
    214     private int mFullHeight = 0;
    215     private String mViewName;
    216     protected float mInitialTouchY;
    217     protected float mFinalTouchY;
    218 
    219     public void setRubberbandingEnabled(boolean enable) {
    220         mRubberbandingEnabled = enable;
    221     }
    222 
    223     private void runPeekAnimation() {
    224         if (DEBUG) LOG("peek to height=%.1f", mPeekHeight);
    225         if (mTimeAnimator.isStarted()) {
    226             return;
    227         }
    228         if (mPeekAnimator == null) {
    229             mPeekAnimator = ObjectAnimator.ofFloat(this,
    230                     "expandedHeight", mPeekHeight)
    231                 .setDuration(250);
    232         }
    233         mPeekAnimator.start();
    234     }
    235 
    236     private void animationTick(long dtms) {
    237         if (!mTimeAnimator.isStarted()) {
    238             // XXX HAX to work around bug in TimeAnimator.end() not resetting its last time
    239             mTimeAnimator = new TimeAnimator();
    240             mTimeAnimator.setTimeListener(mAnimationCallback);
    241 
    242             if (mPeekAnimator != null) mPeekAnimator.cancel();
    243 
    244             mTimeAnimator.start();
    245 
    246             mRubberbanding = mRubberbandingEnabled // is it enabled at all?
    247                     && mExpandedHeight > getFullHeight() // are we past the end?
    248                     && mVel >= -mFlingGestureMinDistPx; // was this not possibly a "close" gesture?
    249             if (mRubberbanding) {
    250                 mClosing = true;
    251             } else if (mVel == 0) {
    252                 // if the panel is less than halfway open, close it
    253                 mClosing = (mFinalTouchY / getFullHeight()) < 0.5f;
    254             } else {
    255                 mClosing = mExpandedHeight > 0 && mVel < 0;
    256             }
    257         } else if (dtms > 0) {
    258             final float dt = dtms * 0.001f;                  // ms -> s
    259             if (DEBUG) LOG("tick: v=%.2fpx/s dt=%.4fs", mVel, dt);
    260             if (DEBUG) LOG("tick: before: h=%d", (int) mExpandedHeight);
    261 
    262             final float fh = getFullHeight();
    263             boolean braking = false;
    264             if (BRAKES) {
    265                 if (mClosing) {
    266                     braking = mExpandedHeight <= mCollapseBrakingDistancePx;
    267                     mAccel = braking ? 10*mCollapseAccelPx : -mCollapseAccelPx;
    268                 } else {
    269                     braking = mExpandedHeight >= (fh-mExpandBrakingDistancePx);
    270                     mAccel = braking ? 10*-mExpandAccelPx : mExpandAccelPx;
    271                 }
    272             } else {
    273                 mAccel = mClosing ? -mCollapseAccelPx : mExpandAccelPx;
    274             }
    275 
    276             mVel += mAccel * dt;
    277 
    278             if (braking) {
    279                 if (mClosing && mVel > -mBrakingSpeedPx) {
    280                     mVel = -mBrakingSpeedPx;
    281                 } else if (!mClosing && mVel < mBrakingSpeedPx) {
    282                     mVel = mBrakingSpeedPx;
    283                 }
    284             } else {
    285                 if (mClosing && mVel > -mFlingCollapseMinVelocityPx) {
    286                     mVel = -mFlingCollapseMinVelocityPx;
    287                 } else if (!mClosing && mVel > mFlingGestureMaxOutputVelocityPx) {
    288                     mVel = mFlingGestureMaxOutputVelocityPx;
    289                 }
    290             }
    291 
    292             float h = mExpandedHeight + mVel * dt;
    293 
    294             if (mRubberbanding && h < fh) {
    295                 h = fh;
    296             }
    297 
    298             if (DEBUG) LOG("tick: new h=%d closing=%s", (int) h, mClosing?"true":"false");
    299 
    300             setExpandedHeightInternal(h);
    301 
    302             mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
    303 
    304             if (mVel == 0
    305                     || (mClosing && mExpandedHeight == 0)
    306                     || ((mRubberbanding || !mClosing) && mExpandedHeight == fh)) {
    307                 post(mStopAnimator);
    308             }
    309         } else {
    310             Slog.v(TAG, "animationTick called with dtms=" + dtms + "; nothing to do (h="
    311                     + mExpandedHeight + " v=" + mVel + ")");
    312         }
    313     }
    314 
    315     public PanelView(Context context, AttributeSet attrs) {
    316         super(context, attrs);
    317 
    318         mTimeAnimator = new TimeAnimator();
    319         mTimeAnimator.setTimeListener(mAnimationCallback);
    320     }
    321 
    322     private void loadDimens() {
    323         final Resources res = getContext().getResources();
    324 
    325         mSelfExpandVelocityPx = res.getDimension(R.dimen.self_expand_velocity);
    326         mSelfCollapseVelocityPx = res.getDimension(R.dimen.self_collapse_velocity);
    327         mFlingExpandMinVelocityPx = res.getDimension(R.dimen.fling_expand_min_velocity);
    328         mFlingCollapseMinVelocityPx = res.getDimension(R.dimen.fling_collapse_min_velocity);
    329 
    330         mFlingGestureMinDistPx = res.getDimension(R.dimen.fling_gesture_min_dist);
    331 
    332         mCollapseMinDisplayFraction = res.getFraction(R.dimen.collapse_min_display_fraction, 1, 1);
    333         mExpandMinDisplayFraction = res.getFraction(R.dimen.expand_min_display_fraction, 1, 1);
    334 
    335         mExpandAccelPx = res.getDimension(R.dimen.expand_accel);
    336         mCollapseAccelPx = res.getDimension(R.dimen.collapse_accel);
    337 
    338         mFlingGestureMaxXVelocityPx = res.getDimension(R.dimen.fling_gesture_max_x_velocity);
    339 
    340         mFlingGestureMaxOutputVelocityPx = res.getDimension(R.dimen.fling_gesture_max_output_velocity);
    341 
    342         mPeekHeight = res.getDimension(R.dimen.peek_height)
    343             + getPaddingBottom() // our window might have a dropshadow
    344             - (mHandleView == null ? 0 : mHandleView.getPaddingTop()); // the handle might have a topshadow
    345     }
    346 
    347     private void trackMovement(MotionEvent event) {
    348         // Add movement to velocity tracker using raw screen X and Y coordinates instead
    349         // of window coordinates because the window frame may be moving at the same time.
    350         float deltaX = event.getRawX() - event.getX();
    351         float deltaY = event.getRawY() - event.getY();
    352         event.offsetLocation(deltaX, deltaY);
    353         if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
    354         event.offsetLocation(-deltaX, -deltaY);
    355     }
    356 
    357     // Pass all touches along to the handle, allowing the user to drag the panel closed from its interior
    358     @Override
    359     public boolean onTouchEvent(MotionEvent event) {
    360         return mHandleView.dispatchTouchEvent(event);
    361     }
    362 
    363     @Override
    364     protected void onFinishInflate() {
    365         super.onFinishInflate();
    366         mHandleView = findViewById(R.id.handle);
    367 
    368         loadDimens();
    369 
    370         if (DEBUG) LOG("handle view: " + mHandleView);
    371         if (mHandleView != null) {
    372             mHandleView.setOnTouchListener(new View.OnTouchListener() {
    373                 @Override
    374                 public boolean onTouch(View v, MotionEvent event) {
    375                     final float y = event.getY();
    376                     final float rawY = event.getRawY();
    377                     if (DEBUG) LOG("handle.onTouch: a=%s y=%.1f rawY=%.1f off=%.1f",
    378                             MotionEvent.actionToString(event.getAction()),
    379                             y, rawY, mTouchOffset);
    380                     PanelView.this.getLocationOnScreen(mAbsPos);
    381 
    382                     switch (event.getAction()) {
    383                         case MotionEvent.ACTION_DOWN:
    384                             mTracking = true;
    385                             mHandleView.setPressed(true);
    386                             postInvalidate(); // catch the press state change
    387                             mInitialTouchY = y;
    388                             mVelocityTracker = FlingTracker.obtain();
    389                             trackMovement(event);
    390                             mTimeAnimator.cancel(); // end any outstanding animations
    391                             mBar.onTrackingStarted(PanelView.this);
    392                             mTouchOffset = (rawY - mAbsPos[1]) - PanelView.this.getExpandedHeight();
    393                             if (mExpandedHeight == 0) {
    394                                 mJustPeeked = true;
    395                                 runPeekAnimation();
    396                             }
    397                             break;
    398 
    399                         case MotionEvent.ACTION_MOVE:
    400                             final float h = rawY - mAbsPos[1] - mTouchOffset;
    401                             if (h > mPeekHeight) {
    402                                 if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
    403                                     mPeekAnimator.cancel();
    404                                 }
    405                                 mJustPeeked = false;
    406                             }
    407                             if (!mJustPeeked) {
    408                                 PanelView.this.setExpandedHeightInternal(h);
    409                                 mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
    410                             }
    411 
    412                             trackMovement(event);
    413                             break;
    414 
    415                         case MotionEvent.ACTION_UP:
    416                         case MotionEvent.ACTION_CANCEL:
    417                             mFinalTouchY = y;
    418                             mTracking = false;
    419                             mHandleView.setPressed(false);
    420                             postInvalidate(); // catch the press state change
    421                             mBar.onTrackingStopped(PanelView.this);
    422                             trackMovement(event);
    423 
    424                             float vel = 0, yVel = 0, xVel = 0;
    425                             boolean negative = false;
    426 
    427                             if (mVelocityTracker != null) {
    428                                 // the velocitytracker might be null if we got a bad input stream
    429                                 mVelocityTracker.computeCurrentVelocity(1000);
    430 
    431                                 yVel = mVelocityTracker.getYVelocity();
    432                                 negative = yVel < 0;
    433 
    434                                 xVel = mVelocityTracker.getXVelocity();
    435                                 if (xVel < 0) {
    436                                     xVel = -xVel;
    437                                 }
    438                                 if (xVel > mFlingGestureMaxXVelocityPx) {
    439                                     xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis
    440                                 }
    441 
    442                                 vel = (float)Math.hypot(yVel, xVel);
    443                                 if (vel > mFlingGestureMaxOutputVelocityPx) {
    444                                     vel = mFlingGestureMaxOutputVelocityPx;
    445                                 }
    446 
    447                                 mVelocityTracker.recycle();
    448                                 mVelocityTracker = null;
    449                             }
    450 
    451                             // if you've barely moved your finger, we treat the velocity as 0
    452                             // preventing spurious flings due to touch screen jitter
    453                             final float deltaY = Math.abs(mFinalTouchY - mInitialTouchY);
    454                             if (deltaY < mFlingGestureMinDistPx
    455                                     || vel < mFlingExpandMinVelocityPx
    456                                     ) {
    457                                 vel = 0;
    458                             }
    459 
    460                             if (negative) {
    461                                 vel = -vel;
    462                             }
    463 
    464                             if (DEBUG) LOG("gesture: dy=%f vel=(%f,%f) vlinear=%f",
    465                                     deltaY,
    466                                     xVel, yVel,
    467                                     vel);
    468 
    469                             fling(vel, true);
    470 
    471                             break;
    472                     }
    473                     return true;
    474                 }});
    475         }
    476     }
    477 
    478     public void fling(float vel, boolean always) {
    479         if (DEBUG) LOG("fling: vel=%.3f, this=%s", vel, this);
    480         mVel = vel;
    481 
    482         if (always||mVel != 0) {
    483             animationTick(0); // begin the animation
    484         }
    485     }
    486 
    487     @Override
    488     protected void onAttachedToWindow() {
    489         super.onAttachedToWindow();
    490         mViewName = getResources().getResourceName(getId());
    491     }
    492 
    493     public String getName() {
    494         return mViewName;
    495     }
    496 
    497     @Override
    498     protected void onViewAdded(View child) {
    499         if (DEBUG) LOG("onViewAdded: " + child);
    500     }
    501 
    502     public View getHandle() {
    503         return mHandleView;
    504     }
    505 
    506     // Rubberbands the panel to hold its contents.
    507     @Override
    508     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    509         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    510 
    511         if (DEBUG) LOG("onMeasure(%d, %d) -> (%d, %d)",
    512                 widthMeasureSpec, heightMeasureSpec, getMeasuredWidth(), getMeasuredHeight());
    513 
    514         // Did one of our children change size?
    515         int newHeight = getMeasuredHeight();
    516         if (newHeight != mFullHeight) {
    517             mFullHeight = newHeight;
    518             // If the user isn't actively poking us, let's rubberband to the content
    519             if (!mTracking && !mRubberbanding && !mTimeAnimator.isStarted()
    520                     && mExpandedHeight > 0 && mExpandedHeight != mFullHeight) {
    521                 mExpandedHeight = mFullHeight;
    522             }
    523         }
    524         heightMeasureSpec = MeasureSpec.makeMeasureSpec(
    525                     (int) mExpandedHeight, MeasureSpec.AT_MOST); // MeasureSpec.getMode(heightMeasureSpec));
    526         setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    527     }
    528 
    529 
    530     public void setExpandedHeight(float height) {
    531         if (DEBUG) LOG("setExpandedHeight(%.1f)", height);
    532         mRubberbanding = false;
    533         if (mTimeAnimator.isStarted()) {
    534             post(mStopAnimator);
    535         }
    536         setExpandedHeightInternal(height);
    537         mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
    538     }
    539 
    540     @Override
    541     protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
    542         if (DEBUG) LOG("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom, (int)mExpandedHeight, mFullHeight);
    543         super.onLayout(changed, left, top, right, bottom);
    544     }
    545 
    546     public void setExpandedHeightInternal(float h) {
    547         if (Float.isNaN(h)) {
    548             // If a NaN gets in here, it will freeze the Animators.
    549             if (DEBUG_NAN) {
    550                 Slog.v(TAG, "setExpandedHeightInternal: warning: h=NaN, using 0 instead",
    551                         new Throwable());
    552             }
    553             h = 0;
    554         }
    555 
    556         float fh = getFullHeight();
    557         if (fh == 0) {
    558             // Hmm, full height hasn't been computed yet
    559         }
    560 
    561         if (h < 0) h = 0;
    562         if (!(mRubberbandingEnabled && (mTracking || mRubberbanding)) && h > fh) h = fh;
    563 
    564         mExpandedHeight = h;
    565 
    566         if (DEBUG) LOG("setExpansion: height=%.1f fh=%.1f tracking=%s rubber=%s", h, fh, mTracking?"T":"f", mRubberbanding?"T":"f");
    567 
    568         requestLayout();
    569 //        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
    570 //        lp.height = (int) mExpandedHeight;
    571 //        setLayoutParams(lp);
    572 
    573         mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : h / fh);
    574     }
    575 
    576     private float getFullHeight() {
    577         if (mFullHeight <= 0) {
    578             if (DEBUG) LOG("Forcing measure() since fullHeight=" + mFullHeight);
    579             measure(MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY),
    580                     MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY));
    581         }
    582         return mFullHeight;
    583     }
    584 
    585     public void setExpandedFraction(float frac) {
    586         if (Float.isNaN(frac)) {
    587             // If a NaN gets in here, it will freeze the Animators.
    588             if (DEBUG_NAN) {
    589                 Slog.v(TAG, "setExpandedFraction: frac=NaN, using 0 instead",
    590                         new Throwable());
    591             }
    592             frac = 0;
    593         }
    594         setExpandedHeight(getFullHeight() * frac);
    595     }
    596 
    597     public float getExpandedHeight() {
    598         return mExpandedHeight;
    599     }
    600 
    601     public float getExpandedFraction() {
    602         return mExpandedFraction;
    603     }
    604 
    605     public boolean isFullyExpanded() {
    606         return mExpandedHeight >= getFullHeight();
    607     }
    608 
    609     public boolean isFullyCollapsed() {
    610         return mExpandedHeight <= 0;
    611     }
    612 
    613     public boolean isCollapsing() {
    614         return mClosing;
    615     }
    616 
    617     public void setBar(PanelBar panelBar) {
    618         mBar = panelBar;
    619     }
    620 
    621     public void collapse() {
    622         // TODO: abort animation or ongoing touch
    623         if (DEBUG) LOG("collapse: " + this);
    624         if (!isFullyCollapsed()) {
    625             mTimeAnimator.cancel();
    626             mClosing = true;
    627             // collapse() should never be a rubberband, even if an animation is already running
    628             mRubberbanding = false;
    629             fling(-mSelfCollapseVelocityPx, /*always=*/ true);
    630         }
    631     }
    632 
    633     public void expand() {
    634         if (DEBUG) LOG("expand: " + this);
    635         if (isFullyCollapsed()) {
    636             mBar.startOpeningPanel(this);
    637             fling(mSelfExpandVelocityPx, /*always=*/ true);
    638         } else if (DEBUG) {
    639             if (DEBUG) LOG("skipping expansion: is expanded");
    640         }
    641     }
    642 
    643     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    644         pw.println(String.format("[PanelView(%s): expandedHeight=%f fullHeight=%f closing=%s"
    645                 + " tracking=%s rubberbanding=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s"
    646                 + "]",
    647                 this.getClass().getSimpleName(),
    648                 getExpandedHeight(),
    649                 getFullHeight(),
    650                 mClosing?"T":"f",
    651                 mTracking?"T":"f",
    652                 mRubberbanding?"T":"f",
    653                 mJustPeeked?"T":"f",
    654                 mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
    655                 mTimeAnimator, ((mTimeAnimator!=null && mTimeAnimator.isStarted())?" (started)":"")
    656         ));
    657     }
    658 }
    659