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