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