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