PanelView.java revision 9d09824a66e3a9785f3f05929bc9139da108efe4
1package com.android.systemui.statusbar.phone;
2
3import android.animation.ObjectAnimator;
4import android.animation.TimeAnimator;
5import android.animation.TimeAnimator.TimeListener;
6import android.content.Context;
7import android.content.res.Resources;
8import android.util.AttributeSet;
9import android.util.Slog;
10import android.view.MotionEvent;
11import android.view.VelocityTracker;
12import android.view.View;
13import android.widget.FrameLayout;
14
15import com.android.systemui.R;
16
17public class PanelView extends FrameLayout {
18    public static final boolean DEBUG = PanelBar.DEBUG;
19    public static final String TAG = PanelView.class.getSimpleName();
20    public final void LOG(String fmt, Object... args) {
21        if (!DEBUG) return;
22        Slog.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
23    }
24
25    public static final boolean BRAKES = false;
26    private static final boolean STRETCH_PAST_CONTENTS = true;
27
28    private float mSelfExpandVelocityPx; // classic value: 2000px/s
29    private float mSelfCollapseVelocityPx; // classic value: 2000px/s (will be negated to collapse "up")
30    private float mFlingExpandMinVelocityPx; // classic value: 200px/s
31    private float mFlingCollapseMinVelocityPx; // classic value: 200px/s
32    private float mCollapseMinDisplayFraction; // classic value: 0.08 (25px/min(320px,480px) on G1)
33    private float mExpandMinDisplayFraction; // classic value: 0.5 (drag open halfway to expand)
34    private float mFlingGestureMaxXVelocityPx; // classic value: 150px/s
35
36    private float mFlingGestureMinDistPx;
37
38    private float mExpandAccelPx; // classic value: 2000px/s/s
39    private float mCollapseAccelPx; // classic value: 2000px/s/s (will be negated to collapse "up")
40
41    private float mFlingGestureMaxOutputVelocityPx; // how fast can it really go? (should be a little
42                                                    // faster than mSelfCollapseVelocityPx)
43
44    private float mCollapseBrakingDistancePx = 200; // XXX Resource
45    private float mExpandBrakingDistancePx = 150; // XXX Resource
46    private float mBrakingSpeedPx = 150; // XXX Resource
47
48    private View mHandleView;
49    private float mPeekHeight;
50    private float mTouchOffset;
51    private float mExpandedFraction = 0;
52    private float mExpandedHeight = 0;
53    private boolean mJustPeeked;
54    private boolean mClosing;
55    private boolean mRubberbanding;
56    private boolean mTracking;
57
58    private TimeAnimator mTimeAnimator;
59    private ObjectAnimator mPeekAnimator;
60    private VelocityTracker mVelocityTracker;
61
62    private int[] mAbsPos = new int[2];
63    PanelBar mBar;
64
65    private final TimeListener mAnimationCallback = new TimeListener() {
66        @Override
67        public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
68            animationTick(deltaTime);
69        }
70    };
71
72    private final Runnable mStopAnimator = new Runnable() {
73        @Override
74        public void run() {
75            if (mTimeAnimator != null && mTimeAnimator.isStarted()) {
76                mTimeAnimator.end();
77                mRubberbanding = false;
78                mClosing = false;
79            }
80        }
81    };
82
83    private float mVel, mAccel;
84    private int mFullHeight = 0;
85    private String mViewName;
86    protected float mInitialTouchY;
87    protected float mFinalTouchY;
88
89    private void runPeekAnimation() {
90        if (DEBUG) LOG("peek to height=%.1f", mPeekHeight);
91        if (mTimeAnimator.isStarted()) {
92            return;
93        }
94        if (mPeekAnimator == null) {
95            mPeekAnimator = ObjectAnimator.ofFloat(this,
96                    "expandedHeight", mPeekHeight)
97                .setDuration(300);
98        }
99        mPeekAnimator.start();
100    }
101
102    private void animationTick(long dtms) {
103        if (!mTimeAnimator.isStarted()) {
104            // XXX HAX to work around bug in TimeAnimator.end() not resetting its last time
105            mTimeAnimator = new TimeAnimator();
106            mTimeAnimator.setTimeListener(mAnimationCallback);
107
108            if (mPeekAnimator != null) mPeekAnimator.cancel();
109
110            mTimeAnimator.start();
111
112            mRubberbanding = STRETCH_PAST_CONTENTS // is it enabled at all?
113                    && mExpandedHeight > getFullHeight() // are we past the end?
114                    && mVel >= -mFlingGestureMinDistPx; // was this not possibly a "close" gesture?
115            if (mRubberbanding) {
116                mClosing = true;
117            } else if (mVel == 0) {
118                // if the panel is less than halfway open, close it
119                mClosing = (mFinalTouchY / getFullHeight()) < 0.5f;
120            } else {
121                mClosing = mExpandedHeight > 0 && mVel < 0;
122            }
123        } else if (dtms > 0) {
124            final float dt = dtms * 0.001f;                  // ms -> s
125            if (DEBUG) LOG("tick: v=%.2fpx/s dt=%.4fs", mVel, dt);
126            if (DEBUG) LOG("tick: before: h=%d", (int) mExpandedHeight);
127
128            final float fh = getFullHeight();
129            boolean braking = false;
130            if (BRAKES) {
131                if (mClosing) {
132                    braking = mExpandedHeight <= mCollapseBrakingDistancePx;
133                    mAccel = braking ? 10*mCollapseAccelPx : -mCollapseAccelPx;
134                } else {
135                    braking = mExpandedHeight >= (fh-mExpandBrakingDistancePx);
136                    mAccel = braking ? 10*-mExpandAccelPx : mExpandAccelPx;
137                }
138            } else {
139                mAccel = mClosing ? -mCollapseAccelPx : mExpandAccelPx;
140            }
141
142            mVel += mAccel * dt;
143
144            if (braking) {
145                if (mClosing && mVel > -mBrakingSpeedPx) {
146                    mVel = -mBrakingSpeedPx;
147                } else if (!mClosing && mVel < mBrakingSpeedPx) {
148                    mVel = mBrakingSpeedPx;
149                }
150            } else {
151                if (mClosing && mVel > -mFlingCollapseMinVelocityPx) {
152                    mVel = -mFlingCollapseMinVelocityPx;
153                } else if (!mClosing && mVel > mFlingGestureMaxOutputVelocityPx) {
154                    mVel = mFlingGestureMaxOutputVelocityPx;
155                }
156            }
157
158            float h = mExpandedHeight + mVel * dt;
159
160            if (mRubberbanding && h < fh) {
161                h = fh;
162            }
163
164            if (DEBUG) LOG("tick: new h=%d closing=%s", (int) h, mClosing?"true":"false");
165
166            setExpandedHeightInternal(h);
167
168            mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
169
170            if (mVel == 0
171                    || (mClosing && mExpandedHeight == 0)
172                    || ((mRubberbanding || !mClosing) && mExpandedHeight == fh)) {
173                post(mStopAnimator);
174            }
175        }
176    }
177
178    public PanelView(Context context, AttributeSet attrs) {
179        super(context, attrs);
180
181        mTimeAnimator = new TimeAnimator();
182        mTimeAnimator.setTimeListener(mAnimationCallback);
183    }
184
185    private void loadDimens() {
186        final Resources res = getContext().getResources();
187
188        mSelfExpandVelocityPx = res.getDimension(R.dimen.self_expand_velocity);
189        mSelfCollapseVelocityPx = res.getDimension(R.dimen.self_collapse_velocity);
190        mFlingExpandMinVelocityPx = res.getDimension(R.dimen.fling_expand_min_velocity);
191        mFlingCollapseMinVelocityPx = res.getDimension(R.dimen.fling_collapse_min_velocity);
192
193        mFlingGestureMinDistPx = res.getDimension(R.dimen.fling_gesture_min_dist);
194
195        mCollapseMinDisplayFraction = res.getFraction(R.dimen.collapse_min_display_fraction, 1, 1);
196        mExpandMinDisplayFraction = res.getFraction(R.dimen.expand_min_display_fraction, 1, 1);
197
198        mExpandAccelPx = res.getDimension(R.dimen.expand_accel);
199        mCollapseAccelPx = res.getDimension(R.dimen.collapse_accel);
200
201        mFlingGestureMaxXVelocityPx = res.getDimension(R.dimen.fling_gesture_max_x_velocity);
202
203        mFlingGestureMaxOutputVelocityPx = res.getDimension(R.dimen.fling_gesture_max_output_velocity);
204
205        mPeekHeight = res.getDimension(R.dimen.close_handle_height)
206            + getPaddingBottom() // our window might have a dropshadow
207            - (mHandleView == null ? 0 : mHandleView.getPaddingTop()); // the handle might have a topshadow
208    }
209
210    private void trackMovement(MotionEvent event) {
211        // Add movement to velocity tracker using raw screen X and Y coordinates instead
212        // of window coordinates because the window frame may be moving at the same time.
213        float deltaX = event.getRawX() - event.getX();
214        float deltaY = event.getRawY() - event.getY();
215        event.offsetLocation(deltaX, deltaY);
216        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
217        event.offsetLocation(-deltaX, -deltaY);
218    }
219
220    // Pass all touches along to the handle, allowing the user to drag the panel closed from its interior
221    @Override
222    public boolean onTouchEvent(MotionEvent event) {
223        return mHandleView.dispatchTouchEvent(event);
224    }
225
226    @Override
227    protected void onFinishInflate() {
228        super.onFinishInflate();
229        mHandleView = findViewById(R.id.handle);
230
231        loadDimens();
232
233        if (DEBUG) LOG("handle view: " + mHandleView);
234        if (mHandleView != null) {
235            mHandleView.setOnTouchListener(new View.OnTouchListener() {
236                @Override
237                public boolean onTouch(View v, MotionEvent event) {
238                    final float y = event.getY();
239                    final float rawY = event.getRawY();
240                    if (DEBUG) LOG("handle.onTouch: a=%s y=%.1f rawY=%.1f off=%.1f",
241                            MotionEvent.actionToString(event.getAction()),
242                            y, rawY, mTouchOffset);
243                    PanelView.this.getLocationOnScreen(mAbsPos);
244
245                    switch (event.getAction()) {
246                        case MotionEvent.ACTION_DOWN:
247                            mTracking = true;
248                            mHandleView.setPressed(true);
249                            mInitialTouchY = y;
250                            mVelocityTracker = VelocityTracker.obtain();
251                            trackMovement(event);
252                            mTimeAnimator.cancel(); // end any outstanding animations
253                            mBar.onTrackingStarted(PanelView.this);
254                            mTouchOffset = (rawY - mAbsPos[1]) - PanelView.this.getExpandedHeight();
255                            if (mExpandedHeight == 0) {
256                                mJustPeeked = true;
257                                runPeekAnimation();
258                            }
259                            break;
260
261                        case MotionEvent.ACTION_MOVE:
262                            final float h = rawY - mAbsPos[1] - mTouchOffset;
263                            if (h > mPeekHeight) {
264                                if (mPeekAnimator != null && mPeekAnimator.isRunning()) {
265                                    mPeekAnimator.cancel();
266                                }
267                                mJustPeeked = false;
268                            }
269                            if (!mJustPeeked) {
270                                PanelView.this.setExpandedHeightInternal(h);
271                                mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
272                            }
273
274                            trackMovement(event);
275                            break;
276
277                        case MotionEvent.ACTION_UP:
278                        case MotionEvent.ACTION_CANCEL:
279                            mFinalTouchY = y;
280                            mTracking = false;
281                            mHandleView.setPressed(false);
282                            mBar.onTrackingStopped(PanelView.this);
283                            trackMovement(event);
284
285                            float vel = 0, yVel = 0, xVel = 0;
286                            boolean negative = false;
287
288                            if (mVelocityTracker != null) {
289                                // the velocitytracker might be null if we got a bad input stream
290                                mVelocityTracker.computeCurrentVelocity(1000);
291
292                                yVel = mVelocityTracker.getYVelocity();
293                                negative = yVel < 0;
294
295                                xVel = mVelocityTracker.getXVelocity();
296                                if (xVel < 0) {
297                                    xVel = -xVel;
298                                }
299                                if (xVel > mFlingGestureMaxXVelocityPx) {
300                                    xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis
301                                }
302
303                                vel = (float)Math.hypot(yVel, xVel);
304                                if (vel > mFlingGestureMaxOutputVelocityPx) {
305                                    vel = mFlingGestureMaxOutputVelocityPx;
306                                }
307
308                                mVelocityTracker.recycle();
309                                mVelocityTracker = null;
310                            }
311
312                            // if you've barely moved your finger, we treat the velocity as 0
313                            // preventing spurious flings due to touch screen jitter
314                            final float deltaY = Math.abs(mFinalTouchY - mInitialTouchY);
315                            if (deltaY < mFlingGestureMinDistPx
316                                    || vel < mFlingExpandMinVelocityPx
317                                    || mJustPeeked) {
318                                vel = 0;
319                            }
320
321                            if (negative) {
322                                vel = -vel;
323                            }
324
325                            if (DEBUG) LOG("gesture: dy=%f vraw=(%f,%f) vnorm=(%f,%f) vlinear=%f",
326                                    deltaY,
327                                    mVelocityTracker.getXVelocity(),
328                                    mVelocityTracker.getYVelocity(),
329                                    xVel, yVel,
330                                    vel);
331
332                            fling(vel, true);
333
334                            break;
335                    }
336                    return true;
337                }});
338        }
339    }
340
341    public void fling(float vel, boolean always) {
342        if (DEBUG) LOG("fling: vel=%.3f, this=%s", vel, this);
343        mVel = vel;
344
345        if (always||mVel != 0) {
346            animationTick(0); // begin the animation
347        }
348    }
349
350    @Override
351    protected void onAttachedToWindow() {
352        super.onAttachedToWindow();
353        mViewName = getResources().getResourceName(getId());
354    }
355
356    public String getName() {
357        return mViewName;
358    }
359
360    @Override
361    protected void onViewAdded(View child) {
362        if (DEBUG) LOG("onViewAdded: " + child);
363    }
364
365    public View getHandle() {
366        return mHandleView;
367    }
368
369    // Rubberbands the panel to hold its contents.
370    @Override
371    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
372        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
373
374        if (DEBUG) LOG("onMeasure(%d, %d) -> (%d, %d)",
375                widthMeasureSpec, heightMeasureSpec, getMeasuredWidth(), getMeasuredHeight());
376
377        // Did one of our children change size?
378        int newHeight = getMeasuredHeight();
379        if (newHeight != mFullHeight) {
380            mFullHeight = newHeight;
381            // If the user isn't actively poking us, let's rubberband to the content
382            if (!mTracking && !mRubberbanding && !mTimeAnimator.isStarted()
383                    && mExpandedHeight > 0 && mExpandedHeight != mFullHeight) {
384                mExpandedHeight = mFullHeight;
385            }
386        }
387        heightMeasureSpec = MeasureSpec.makeMeasureSpec(
388                    (int) mExpandedHeight, MeasureSpec.AT_MOST); // MeasureSpec.getMode(heightMeasureSpec));
389        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
390    }
391
392
393    public void setExpandedHeight(float height) {
394        if (DEBUG) LOG("setExpandedHeight(%.1f)", height);
395        mRubberbanding = false;
396        if (mTimeAnimator.isRunning()) {
397            post(mStopAnimator);
398        }
399        setExpandedHeightInternal(height);
400        mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
401    }
402
403    @Override
404    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
405        if (DEBUG) LOG("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom, (int)mExpandedHeight, mFullHeight);
406        super.onLayout(changed, left, top, right, bottom);
407    }
408
409    public void setExpandedHeightInternal(float h) {
410        float fh = getFullHeight();
411        if (fh == 0) {
412            // Hmm, full height hasn't been computed yet
413        }
414
415        if (h < 0) h = 0;
416        if (!(STRETCH_PAST_CONTENTS && (mTracking || mRubberbanding)) && h > fh) h = fh;
417        mExpandedHeight = h;
418
419        if (DEBUG) LOG("setExpansion: height=%.1f fh=%.1f tracking=%s rubber=%s", h, fh, mTracking?"T":"f", mRubberbanding?"T":"f");
420
421        requestLayout();
422//        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
423//        lp.height = (int) mExpandedHeight;
424//        setLayoutParams(lp);
425
426        mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : h / fh);
427    }
428
429    private float getFullHeight() {
430        if (mFullHeight <= 0) {
431            if (DEBUG) LOG("Forcing measure() since fullHeight=" + mFullHeight);
432            measure(MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY),
433                    MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY));
434        }
435        return mFullHeight;
436    }
437
438    public void setExpandedFraction(float frac) {
439        setExpandedHeight(getFullHeight() * frac);
440    }
441
442    public float getExpandedHeight() {
443        return mExpandedHeight;
444    }
445
446    public float getExpandedFraction() {
447        return mExpandedFraction;
448    }
449
450    public boolean isFullyExpanded() {
451        return mExpandedHeight >= getFullHeight();
452    }
453
454    public boolean isFullyCollapsed() {
455        return mExpandedHeight <= 0;
456    }
457
458    public boolean isCollapsing() {
459        return mClosing;
460    }
461
462    public void setBar(PanelBar panelBar) {
463        mBar = panelBar;
464    }
465
466    public void collapse() {
467        // TODO: abort animation or ongoing touch
468        if (DEBUG) LOG("collapse: " + this);
469        if (!isFullyCollapsed()) {
470            mTimeAnimator.cancel();
471            mClosing = true;
472            // collapse() should never be a rubberband, even if an animation is already running
473            mRubberbanding = false;
474            fling(-mSelfCollapseVelocityPx, /*always=*/ true);
475        }
476    }
477
478    public void expand() {
479        if (DEBUG) LOG("expand: " + this);
480        if (isFullyCollapsed()) {
481            mBar.startOpeningPanel(this);
482            fling(mSelfExpandVelocityPx, /*always=*/ true);
483        } else if (DEBUG) {
484            if (DEBUG) LOG("skipping expansion: is expanded");
485        }
486    }
487}
488