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