PanelView.java revision 91c39ef7f2fb1b678658a037f2e055062ee2d9c3
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.view.ViewConfiguration;
29import android.widget.FrameLayout;
30
31import com.android.systemui.R;
32
33import java.io.FileDescriptor;
34import java.io.PrintWriter;
35import java.util.ArrayDeque;
36import java.util.Iterator;
37
38public class PanelView extends FrameLayout {
39    public static final boolean DEBUG = PanelBar.DEBUG;
40    public static final String TAG = PanelView.class.getSimpleName();
41
42    public static final boolean DEBUG_NAN = true; // http://b/7686690
43
44    private final void logf(String fmt, Object... args) {
45        Log.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 mInitialOffsetOnTouch;
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    private int mTrackingPointer;
81    private int mTouchSlop;
82
83    private TimeAnimator mTimeAnimator;
84    private ObjectAnimator mPeekAnimator;
85    private FlingTracker mVelocityTracker;
86
87    /**
88     * A very simple low-pass velocity filter for motion events; not nearly as sophisticated as
89     * VelocityTracker but optimized for the kinds of gestures we expect to see in status bar
90     * panels.
91     */
92    private static class FlingTracker {
93        static final boolean DEBUG = false;
94        final int MAX_EVENTS = 8;
95        final float DECAY = 0.75f;
96        ArrayDeque<MotionEventCopy> mEventBuf = new ArrayDeque<MotionEventCopy>(MAX_EVENTS);
97        float mVX, mVY = 0;
98        private static class MotionEventCopy {
99            public MotionEventCopy(float x2, float y2, long eventTime) {
100                this.x = x2;
101                this.y = y2;
102                this.t = eventTime;
103            }
104            public float x, y;
105            public long t;
106        }
107        public FlingTracker() {
108        }
109        public void addMovement(MotionEvent event) {
110            if (mEventBuf.size() == MAX_EVENTS) {
111                mEventBuf.remove();
112            }
113            mEventBuf.add(new MotionEventCopy(event.getX(), event.getY(), event.getEventTime()));
114        }
115        public void computeCurrentVelocity(long timebase) {
116            if (FlingTracker.DEBUG) {
117                Log.v("FlingTracker", "computing velocities for " + mEventBuf.size() + " events");
118            }
119            mVX = mVY = 0;
120            MotionEventCopy last = null;
121            int i = 0;
122            float totalweight = 0f;
123            float weight = 10f;
124            for (final Iterator<MotionEventCopy> iter = mEventBuf.iterator();
125                    iter.hasNext();) {
126                final MotionEventCopy event = iter.next();
127                if (last != null) {
128                    final float dt = (float) (event.t - last.t) / timebase;
129                    final float dx = (event.x - last.x);
130                    final float dy = (event.y - last.y);
131                    if (FlingTracker.DEBUG) {
132                        Log.v("FlingTracker", String.format(
133                                "   [%d] (t=%d %.1f,%.1f) dx=%.1f dy=%.1f dt=%f vx=%.1f vy=%.1f",
134                                i, event.t, event.x, event.y,
135                                dx, dy, dt,
136                                (dx/dt),
137                                (dy/dt)
138                                ));
139                    }
140                    if (event.t == last.t) {
141                        // Really not sure what to do with events that happened at the same time,
142                        // so we'll skip subsequent events.
143                        if (DEBUG_NAN) {
144                            Log.v("FlingTracker", "skipping simultaneous event at t=" + event.t);
145                        }
146                        continue;
147                    }
148                    mVX += weight * dx / dt;
149                    mVY += weight * dy / dt;
150                    totalweight += weight;
151                    weight *= DECAY;
152                }
153                last = event;
154                i++;
155            }
156            if (totalweight > 0) {
157                mVX /= totalweight;
158                mVY /= totalweight;
159            } else {
160                if (DEBUG_NAN) {
161                    Log.v("FlingTracker", "computeCurrentVelocity warning: totalweight=0",
162                            new Throwable());
163                }
164                // so as not to contaminate the velocities with NaN
165                mVX = mVY = 0;
166            }
167
168            if (FlingTracker.DEBUG) {
169                Log.v("FlingTracker", "computed: vx=" + mVX + " vy=" + mVY);
170            }
171        }
172        public float getXVelocity() {
173            if (Float.isNaN(mVX) || Float.isInfinite(mVX)) {
174                if (DEBUG_NAN) {
175                    Log.v("FlingTracker", "warning: vx=" + mVX);
176                }
177                mVX = 0;
178            }
179            return mVX;
180        }
181        public float getYVelocity() {
182            if (Float.isNaN(mVY) || Float.isInfinite(mVX)) {
183                if (DEBUG_NAN) {
184                    Log.v("FlingTracker", "warning: vx=" + mVY);
185                }
186                mVY = 0;
187            }
188            return mVY;
189        }
190        public void recycle() {
191            mEventBuf.clear();
192        }
193
194        static FlingTracker sTracker;
195        static FlingTracker obtain() {
196            if (sTracker == null) {
197                sTracker = new FlingTracker();
198            }
199            return sTracker;
200        }
201    }
202
203    PanelBar mBar;
204
205    private final TimeListener mAnimationCallback = new TimeListener() {
206        @Override
207        public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
208            animationTick(deltaTime);
209        }
210    };
211
212    private final Runnable mStopAnimator = new Runnable() {
213        @Override
214        public void run() {
215            if (mTimeAnimator != null && mTimeAnimator.isStarted()) {
216                mTimeAnimator.end();
217                mRubberbanding = false;
218                mClosing = false;
219            }
220        }
221    };
222
223    private float mVel, mAccel;
224    protected int mMaxPanelHeight = 0;
225    private String mViewName;
226    protected float mInitialTouchY;
227    protected float mFinalTouchY;
228
229    public void setRubberbandingEnabled(boolean enable) {
230        mRubberbandingEnabled = enable;
231    }
232
233    private void runPeekAnimation() {
234        if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
235        if (mTimeAnimator.isStarted()) {
236            return;
237        }
238        if (mPeekAnimator == null) {
239            mPeekAnimator = ObjectAnimator.ofFloat(this,
240                    "expandedHeight", mPeekHeight)
241                .setDuration(250);
242        }
243        mPeekAnimator.start();
244    }
245
246    private void animationTick(long dtms) {
247        if (!mTimeAnimator.isStarted()) {
248            // XXX HAX to work around bug in TimeAnimator.end() not resetting its last time
249            mTimeAnimator = new TimeAnimator();
250            mTimeAnimator.setTimeListener(mAnimationCallback);
251
252            if (mPeekAnimator != null) mPeekAnimator.cancel();
253
254            mTimeAnimator.start();
255
256            mRubberbanding = mRubberbandingEnabled // is it enabled at all?
257                    && mExpandedHeight > getMaxPanelHeight() // are we past the end?
258                    && mVel >= -mFlingGestureMinDistPx; // was this not possibly a "close" gesture?
259            if (mRubberbanding) {
260                mClosing = true;
261            } else if (mVel == 0) {
262                // if the panel is less than halfway open, close it
263                mClosing = (mFinalTouchY / getMaxPanelHeight()) < 0.5f;
264            } else {
265                mClosing = mExpandedHeight > 0 && mVel < 0;
266            }
267        } else if (dtms > 0) {
268            final float dt = dtms * 0.001f;                  // ms -> s
269            if (DEBUG) logf("tick: v=%.2fpx/s dt=%.4fs", mVel, dt);
270            if (DEBUG) logf("tick: before: h=%d", (int) mExpandedHeight);
271
272            final float fh = getMaxPanelHeight();
273            boolean braking = false;
274            if (BRAKES) {
275                if (mClosing) {
276                    braking = mExpandedHeight <= mCollapseBrakingDistancePx;
277                    mAccel = braking ? 10*mCollapseAccelPx : -mCollapseAccelPx;
278                } else {
279                    braking = mExpandedHeight >= (fh-mExpandBrakingDistancePx);
280                    mAccel = braking ? 10*-mExpandAccelPx : mExpandAccelPx;
281                }
282            } else {
283                mAccel = mClosing ? -mCollapseAccelPx : mExpandAccelPx;
284            }
285
286            mVel += mAccel * dt;
287
288            if (braking) {
289                if (mClosing && mVel > -mBrakingSpeedPx) {
290                    mVel = -mBrakingSpeedPx;
291                } else if (!mClosing && mVel < mBrakingSpeedPx) {
292                    mVel = mBrakingSpeedPx;
293                }
294            } else {
295                if (mClosing && mVel > -mFlingCollapseMinVelocityPx) {
296                    mVel = -mFlingCollapseMinVelocityPx;
297                } else if (!mClosing && mVel > mFlingGestureMaxOutputVelocityPx) {
298                    mVel = mFlingGestureMaxOutputVelocityPx;
299                }
300            }
301
302            float h = mExpandedHeight + mVel * dt;
303
304            if (mRubberbanding && h < fh) {
305                h = fh;
306            }
307
308            if (DEBUG) logf("tick: new h=%d closing=%s", (int) h, mClosing?"true":"false");
309
310            setExpandedHeightInternal(h);
311
312            mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
313
314            if (mVel == 0
315                    || (mClosing && mExpandedHeight == 0)
316                    || ((mRubberbanding || !mClosing) && mExpandedHeight == fh)) {
317                post(mStopAnimator);
318            }
319        } else {
320            Log.v(TAG, "animationTick called with dtms=" + dtms + "; nothing to do (h="
321                    + mExpandedHeight + " v=" + mVel + ")");
322        }
323    }
324
325    public PanelView(Context context, AttributeSet attrs) {
326        super(context, attrs);
327
328        mTimeAnimator = new TimeAnimator();
329        mTimeAnimator.setTimeListener(mAnimationCallback);
330    }
331
332    private void loadDimens() {
333        final Resources res = getContext().getResources();
334
335        mSelfExpandVelocityPx = res.getDimension(R.dimen.self_expand_velocity);
336        mSelfCollapseVelocityPx = res.getDimension(R.dimen.self_collapse_velocity);
337        mFlingExpandMinVelocityPx = res.getDimension(R.dimen.fling_expand_min_velocity);
338        mFlingCollapseMinVelocityPx = res.getDimension(R.dimen.fling_collapse_min_velocity);
339
340        mFlingGestureMinDistPx = res.getDimension(R.dimen.fling_gesture_min_dist);
341
342        mCollapseMinDisplayFraction = res.getFraction(R.dimen.collapse_min_display_fraction, 1, 1);
343        mExpandMinDisplayFraction = res.getFraction(R.dimen.expand_min_display_fraction, 1, 1);
344
345        mExpandAccelPx = res.getDimension(R.dimen.expand_accel);
346        mCollapseAccelPx = res.getDimension(R.dimen.collapse_accel);
347
348        mFlingGestureMaxXVelocityPx = res.getDimension(R.dimen.fling_gesture_max_x_velocity);
349
350        mFlingGestureMaxOutputVelocityPx = res.getDimension(R.dimen.fling_gesture_max_output_velocity);
351
352        mPeekHeight = res.getDimension(R.dimen.peek_height)
353            + getPaddingBottom() // our window might have a dropshadow
354            - (mHandleView == null ? 0 : mHandleView.getPaddingTop()); // the handle might have a topshadow
355
356        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
357        mTouchSlop = configuration.getScaledTouchSlop();
358    }
359
360    private void trackMovement(MotionEvent event) {
361        // Add movement to velocity tracker using raw screen X and Y coordinates instead
362        // of window coordinates because the window frame may be moving at the same time.
363        float deltaX = event.getRawX() - event.getX();
364        float deltaY = event.getRawY() - event.getY();
365        event.offsetLocation(deltaX, deltaY);
366        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
367        event.offsetLocation(-deltaX, -deltaY);
368    }
369
370    @Override
371    public boolean onTouchEvent(MotionEvent event) {
372
373        /*
374         * We capture touch events here and update the expand height here in case according to
375         * the users fingers. This also handles multi-touch.
376         *
377         * If the user just clicks shortly, we give him a quick peek of the shade.
378         *
379         * Flinging is also enabled in order to open or close the shade.
380         */
381
382        int pointerIndex = event.findPointerIndex(mTrackingPointer);
383        if (pointerIndex < 0) {
384            pointerIndex = 0;
385            mTrackingPointer = event.getPointerId(pointerIndex);
386        }
387        final float y = event.getY(pointerIndex);
388
389        switch (event.getActionMasked()) {
390            case MotionEvent.ACTION_DOWN:
391                mTracking = true;
392                if (mHandleView != null) {
393                    mHandleView.setPressed(true);
394                    postInvalidate(); // catch the press state change
395                }
396
397                mInitialTouchY = y;
398                initVelocityTracker();
399                trackMovement(event);
400                mTimeAnimator.cancel(); // end any outstanding animations
401                mBar.onTrackingStarted(PanelView.this);
402                mInitialOffsetOnTouch = mExpandedHeight;
403                if (mExpandedHeight == 0) {
404                    mJustPeeked = true;
405                    runPeekAnimation();
406                }
407                break;
408
409            case MotionEvent.ACTION_POINTER_UP:
410                final int upPointer = event.getPointerId(event.getActionIndex());
411                if (mTrackingPointer == upPointer) {
412                    // gesture is ongoing, find a new pointer to track
413                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
414                    final float newY = event.getY(newIndex);
415                    mTrackingPointer = event.getPointerId(newIndex);
416                    mInitialOffsetOnTouch = mExpandedHeight;
417                    mInitialTouchY = newY;
418                }
419                break;
420
421            case MotionEvent.ACTION_MOVE:
422                final float h = y - mInitialTouchY + mInitialOffsetOnTouch;
423                if (h > mPeekHeight) {
424                    if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
425                        mPeekAnimator.cancel();
426                    }
427                    mJustPeeked = false;
428                }
429                if (!mJustPeeked) {
430                    setExpandedHeightInternal(h);
431                    mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
432                }
433
434                trackMovement(event);
435                break;
436
437            case MotionEvent.ACTION_UP:
438            case MotionEvent.ACTION_CANCEL:
439                mFinalTouchY = y;
440                mTracking = false;
441                mTrackingPointer = -1;
442                if (mHandleView != null) {
443                    mHandleView.setPressed(false);
444                    postInvalidate(); // catch the press state change
445                }
446                mBar.onTrackingStopped(PanelView.this);
447                trackMovement(event);
448
449                float vel = getCurrentVelocity();
450                fling(vel, true);
451
452                if (mVelocityTracker != null) {
453                    mVelocityTracker.recycle();
454                    mVelocityTracker = null;
455                }
456                break;
457        }
458        return true;
459    }
460
461    private float getCurrentVelocity() {
462        float vel = 0;
463        float yVel = 0, xVel = 0;
464        boolean negative = false;
465
466        // the velocitytracker might be null if we got a bad input stream
467        if (mVelocityTracker == null) {
468            return 0;
469        }
470
471        mVelocityTracker.computeCurrentVelocity(1000);
472
473        yVel = mVelocityTracker.getYVelocity();
474        negative = yVel < 0;
475
476        xVel = mVelocityTracker.getXVelocity();
477        if (xVel < 0) {
478            xVel = -xVel;
479        }
480        if (xVel > mFlingGestureMaxXVelocityPx) {
481            xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis
482        }
483
484        vel = (float) Math.hypot(yVel, xVel);
485        if (vel > mFlingGestureMaxOutputVelocityPx) {
486            vel = mFlingGestureMaxOutputVelocityPx;
487        }
488
489        // if you've barely moved your finger, we treat the velocity as 0
490        // preventing spurious flings due to touch screen jitter
491        final float deltaY = Math.abs(mFinalTouchY - mInitialTouchY);
492        if (deltaY < mFlingGestureMinDistPx
493                || vel < mFlingExpandMinVelocityPx
494                ) {
495            vel = 0;
496        }
497
498        if (negative) {
499            vel = -vel;
500        }
501
502        if (DEBUG) {
503            logf("gesture: dy=%f vel=(%f,%f) vlinear=%f",
504                    deltaY,
505                    xVel, yVel,
506                    vel);
507        }
508        return vel;
509    }
510
511    @Override
512    public boolean onInterceptTouchEvent(MotionEvent event) {
513
514        /*
515         * If the user drags anywhere inside the panel we intercept it if he moves his finger
516         * upwards. This allows closing the shade from anywhere inside the panel.
517         *
518         * We only do this if the current content is scrolled to the bottom,
519         * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
520         * possible.
521         */
522        int pointerIndex = event.findPointerIndex(mTrackingPointer);
523        if (pointerIndex < 0) {
524            pointerIndex = 0;
525            mTrackingPointer = event.getPointerId(pointerIndex);
526        }
527        final float y = event.getY(pointerIndex);
528        boolean scrolledToBottom = isScrolledToBottom();
529
530        switch (event.getActionMasked()) {
531            case MotionEvent.ACTION_DOWN:
532                if (mHandleView != null) {
533                    mHandleView.setPressed(true);
534                    // catch the press state change
535                    postInvalidate();
536                }
537                mInitialTouchY = y;
538                initVelocityTracker();
539                trackMovement(event);
540                mTimeAnimator.cancel(); // end any outstanding animations
541                if (mExpandedHeight == 0 || y > getContentHeight()) {
542                    return true;
543                }
544                break;
545            case MotionEvent.ACTION_POINTER_UP:
546                final int upPointer = event.getPointerId(event.getActionIndex());
547                if (mTrackingPointer == upPointer) {
548                    // gesture is ongoing, find a new pointer to track
549                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
550                    mTrackingPointer = event.getPointerId(newIndex);
551                    final float newY = event.getY(newIndex);
552                    mInitialTouchY = newY;
553                }
554                break;
555
556            case MotionEvent.ACTION_MOVE:
557                final float h = y - mInitialTouchY;
558                trackMovement(event);
559                if (scrolledToBottom) {
560                    if (h < -mTouchSlop) {
561                        mInitialOffsetOnTouch = mExpandedHeight;
562                        mInitialTouchY = y;
563                        mTracking = true;
564                        return true;
565                    }
566                }
567                break;
568        }
569        return false;
570    }
571
572    private void initVelocityTracker() {
573        if (mVelocityTracker != null) {
574            mVelocityTracker.recycle();
575        }
576        mVelocityTracker = FlingTracker.obtain();
577    }
578
579    protected boolean isScrolledToBottom() {
580        return false;
581    }
582
583    protected float getContentHeight() {
584        return mExpandedHeight;
585    }
586
587    @Override
588    protected void onFinishInflate() {
589        super.onFinishInflate();
590        mHandleView = findViewById(R.id.handle);
591
592        loadDimens();
593    }
594
595    public void fling(float vel, boolean always) {
596        if (DEBUG) logf("fling: vel=%.3f, this=%s", vel, this);
597        mVel = vel;
598
599        if (always||mVel != 0) {
600            animationTick(0); // begin the animation
601        }
602    }
603
604    @Override
605    protected void onAttachedToWindow() {
606        super.onAttachedToWindow();
607        mViewName = getResources().getResourceName(getId());
608    }
609
610    public String getName() {
611        return mViewName;
612    }
613
614    @Override
615    protected void onViewAdded(View child) {
616        if (DEBUG) logf("onViewAdded: " + child);
617    }
618
619    public View getHandle() {
620        return mHandleView;
621    }
622
623    // Rubberbands the panel to hold its contents.
624    @Override
625    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
626        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
627
628        if (DEBUG) logf("onMeasure(%d, %d) -> (%d, %d)",
629                widthMeasureSpec, heightMeasureSpec, getMeasuredWidth(), getMeasuredHeight());
630
631        // Did one of our children change size?
632        int newHeight = getMeasuredHeight();
633        if (newHeight != mMaxPanelHeight) {
634            mMaxPanelHeight = newHeight;
635        }
636        heightMeasureSpec = MeasureSpec.makeMeasureSpec(
637                    getDesiredMeasureHeight(), MeasureSpec.AT_MOST);
638        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
639    }
640
641    protected int getDesiredMeasureHeight() {
642        return (int) mExpandedHeight;
643    }
644
645
646    public void setExpandedHeight(float height) {
647        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
648        mRubberbanding = false;
649        if (mTimeAnimator.isStarted()) {
650            post(mStopAnimator);
651        }
652        setExpandedHeightInternal(height);
653        mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
654    }
655
656    @Override
657    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
658        if (DEBUG) logf("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom,
659                (int)mExpandedHeight, mMaxPanelHeight);
660        super.onLayout(changed, left, top, right, bottom);
661        requestPanelHeightUpdate();
662    }
663
664    protected void requestPanelHeightUpdate() {
665        float currentMaxPanelHeight = getMaxPanelHeight();
666
667        // If the user isn't actively poking us, let's update the height
668        if (!mTracking && !mRubberbanding && !mTimeAnimator.isStarted()
669                && mExpandedHeight > 0 && currentMaxPanelHeight != mExpandedHeight) {
670            setExpandedHeightInternal(currentMaxPanelHeight);
671        }
672    }
673
674    public void setExpandedHeightInternal(float h) {
675        if (Float.isNaN(h)) {
676            // If a NaN gets in here, it will freeze the Animators.
677            if (DEBUG_NAN) {
678                Log.v(TAG, "setExpandedHeightInternal: warning: h=NaN, using 0 instead",
679                        new Throwable());
680            }
681            h = 0;
682        }
683
684        float fh = getMaxPanelHeight();
685        if (fh == 0) {
686            // Hmm, full height hasn't been computed yet
687        }
688
689        if (h < 0) h = 0;
690        if (!(mRubberbandingEnabled && (mTracking || mRubberbanding)) && h > fh) h = fh;
691
692        mExpandedHeight = h;
693
694        if (DEBUG) {
695            logf("setExpansion: height=%.1f fh=%.1f tracking=%s rubber=%s", h, fh,
696                    mTracking ? "T" : "f", mRubberbanding ? "T" : "f");
697        }
698
699        onHeightUpdated(mExpandedHeight);
700
701//        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
702//        lp.height = (int) mExpandedHeight;
703//        setLayoutParams(lp);
704
705        mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : h / fh);
706    }
707
708    protected void onHeightUpdated(float expandedHeight) {
709        requestLayout();
710    }
711
712    /**
713     * This returns the maximum height of the panel. Children should override this if their
714     * desired height is not the full height.
715     *
716     * @return the default implementation simply returns the maximum height.
717     */
718    protected int getMaxPanelHeight() {
719        if (mMaxPanelHeight <= 0) {
720            if (DEBUG) logf("Forcing measure() since mMaxPanelHeight=" + mMaxPanelHeight);
721            measure(MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY),
722                    MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY));
723        }
724        return mMaxPanelHeight;
725    }
726
727    public void setExpandedFraction(float frac) {
728        if (Float.isNaN(frac)) {
729            // If a NaN gets in here, it will freeze the Animators.
730            if (DEBUG_NAN) {
731                Log.v(TAG, "setExpandedFraction: frac=NaN, using 0 instead",
732                        new Throwable());
733            }
734            frac = 0;
735        }
736        setExpandedHeight(getMaxPanelHeight() * frac);
737    }
738
739    public float getExpandedHeight() {
740        return mExpandedHeight;
741    }
742
743    public float getExpandedFraction() {
744        return mExpandedFraction;
745    }
746
747    public boolean isFullyExpanded() {
748        return mExpandedHeight >= getMaxPanelHeight();
749    }
750
751    public boolean isFullyCollapsed() {
752        return mExpandedHeight <= 0;
753    }
754
755    public boolean isCollapsing() {
756        return mClosing;
757    }
758
759    public boolean isTracking() {
760        return mTracking;
761    }
762
763    public void setBar(PanelBar panelBar) {
764        mBar = panelBar;
765    }
766
767    public void collapse() {
768        // TODO: abort animation or ongoing touch
769        if (DEBUG) logf("collapse: " + this);
770        if (!isFullyCollapsed()) {
771            mTimeAnimator.cancel();
772            mClosing = true;
773            // collapse() should never be a rubberband, even if an animation is already running
774            mRubberbanding = false;
775            fling(-mSelfCollapseVelocityPx, /*always=*/ true);
776        }
777    }
778
779    public void expand() {
780        if (DEBUG) logf("expand: " + this);
781        if (isFullyCollapsed()) {
782            mBar.startOpeningPanel(this);
783            fling(mSelfExpandVelocityPx, /*always=*/ true);
784        } else if (DEBUG) {
785            if (DEBUG) logf("skipping expansion: is expanded");
786        }
787    }
788
789    public void cancelPeek() {
790        if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
791            mPeekAnimator.cancel();
792        }
793    }
794
795    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
796        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
797                + " tracking=%s rubberbanding=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s"
798                + "]",
799                this.getClass().getSimpleName(),
800                getExpandedHeight(),
801                getMaxPanelHeight(),
802                mClosing?"T":"f",
803                mTracking?"T":"f",
804                mRubberbanding?"T":"f",
805                mJustPeeked?"T":"f",
806                mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
807                mTimeAnimator, ((mTimeAnimator!=null && mTimeAnimator.isStarted())?" (started)":"")
808        ));
809    }
810}
811