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