PanelView.java revision 42b3cf94d0f647a798085aaae2a7b8ff5203fc2e
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 java.io.FileDescriptor;
20import java.io.PrintWriter;
21import java.util.ArrayDeque;
22import java.util.ArrayList;
23import java.util.Iterator;
24
25import android.animation.ObjectAnimator;
26import android.animation.TimeAnimator;
27import android.animation.TimeAnimator.TimeListener;
28import android.content.Context;
29import android.content.res.Resources;
30import android.util.AttributeSet;
31import android.util.Slog;
32import android.view.MotionEvent;
33import android.view.VelocityTracker;
34import android.view.View;
35import android.widget.FrameLayout;
36
37import com.android.systemui.R;
38
39public class PanelView extends FrameLayout {
40    public static final boolean DEBUG = PanelBar.DEBUG;
41    public static final String TAG = PanelView.class.getSimpleName();
42    public final void LOG(String fmt, Object... args) {
43        if (!DEBUG) return;
44        Slog.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
45    }
46
47    public static final boolean BRAKES = false;
48    private boolean mRubberbandingEnabled = true;
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 View mHandleView;
71    private float mPeekHeight;
72    private float mTouchOffset;
73    private float mExpandedFraction = 0;
74    private float mExpandedHeight = 0;
75    private boolean mJustPeeked;
76    private boolean mClosing;
77    private boolean mRubberbanding;
78    private boolean mTracking;
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                Slog.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.descendingIterator();
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                        Slog.v("FlingTracker", String.format("   [%d] dx=%.1f dy=%.1f dt=%.0f vx=%.1f vy=%.1f",
130                                i,
131                                dx, dy, dt,
132                                (dx/dt),
133                                (dy/dt)
134                                ));
135                    }
136                    mVX += weight * dx / dt;
137                    mVY += weight * dy / dt;
138                    totalweight += weight;
139                    weight *= DECAY;
140                }
141                last = event;
142                i++;
143            }
144            mVX /= totalweight;
145            mVY /= totalweight;
146
147            if (FlingTracker.DEBUG) {
148                Slog.v("FlingTracker", "computed: vx=" + mVX + " vy=" + mVY);
149            }
150        }
151        public float getXVelocity() {
152            if (Float.isNaN(mVX)) {
153                Slog.v("FlingTracker", "warning: vx=NaN");
154                // XXX: should return 0
155            }
156            return mVX;
157        }
158        public float getYVelocity() {
159            if (Float.isNaN(mVY)) {
160                Slog.v("FlingTracker", "warning: vx=NaN");
161                // XXX: should return 0
162            }
163            return mVY;
164        }
165        public void recycle() {
166            mEventBuf.clear();
167        }
168
169        static FlingTracker sTracker;
170        static FlingTracker obtain() {
171            if (sTracker == null) {
172                sTracker = new FlingTracker();
173            }
174            return sTracker;
175        }
176    }
177
178    private int[] mAbsPos = new int[2];
179    PanelBar mBar;
180
181    private final TimeListener mAnimationCallback = new TimeListener() {
182        @Override
183        public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
184            animationTick(deltaTime);
185        }
186    };
187
188    private final Runnable mStopAnimator = new Runnable() {
189        @Override
190        public void run() {
191            if (mTimeAnimator != null && mTimeAnimator.isStarted()) {
192                mTimeAnimator.end();
193                mRubberbanding = false;
194                mClosing = false;
195            }
196        }
197    };
198
199    private float mVel, mAccel;
200    private int mFullHeight = 0;
201    private String mViewName;
202    protected float mInitialTouchY;
203    protected float mFinalTouchY;
204
205    public void setRubberbandingEnabled(boolean enable) {
206        mRubberbandingEnabled = enable;
207    }
208
209    private void runPeekAnimation() {
210        if (DEBUG) LOG("peek to height=%.1f", mPeekHeight);
211        if (mTimeAnimator.isStarted()) {
212            return;
213        }
214        if (mPeekAnimator == null) {
215            mPeekAnimator = ObjectAnimator.ofFloat(this,
216                    "expandedHeight", mPeekHeight)
217                .setDuration(250);
218        }
219        mPeekAnimator.start();
220    }
221
222    private void animationTick(long dtms) {
223        if (!mTimeAnimator.isStarted()) {
224            // XXX HAX to work around bug in TimeAnimator.end() not resetting its last time
225            mTimeAnimator = new TimeAnimator();
226            mTimeAnimator.setTimeListener(mAnimationCallback);
227
228            if (mPeekAnimator != null) mPeekAnimator.cancel();
229
230            mTimeAnimator.start();
231
232            mRubberbanding = mRubberbandingEnabled // is it enabled at all?
233                    && mExpandedHeight > getFullHeight() // are we past the end?
234                    && mVel >= -mFlingGestureMinDistPx; // was this not possibly a "close" gesture?
235            if (mRubberbanding) {
236                mClosing = true;
237            } else if (mVel == 0) {
238                // if the panel is less than halfway open, close it
239                mClosing = (mFinalTouchY / getFullHeight()) < 0.5f;
240            } else {
241                mClosing = mExpandedHeight > 0 && mVel < 0;
242            }
243        } else if (dtms > 0) {
244            final float dt = dtms * 0.001f;                  // ms -> s
245            if (DEBUG) LOG("tick: v=%.2fpx/s dt=%.4fs", mVel, dt);
246            if (DEBUG) LOG("tick: before: h=%d", (int) mExpandedHeight);
247
248            final float fh = getFullHeight();
249            boolean braking = false;
250            if (BRAKES) {
251                if (mClosing) {
252                    braking = mExpandedHeight <= mCollapseBrakingDistancePx;
253                    mAccel = braking ? 10*mCollapseAccelPx : -mCollapseAccelPx;
254                } else {
255                    braking = mExpandedHeight >= (fh-mExpandBrakingDistancePx);
256                    mAccel = braking ? 10*-mExpandAccelPx : mExpandAccelPx;
257                }
258            } else {
259                mAccel = mClosing ? -mCollapseAccelPx : mExpandAccelPx;
260            }
261
262            mVel += mAccel * dt;
263
264            if (braking) {
265                if (mClosing && mVel > -mBrakingSpeedPx) {
266                    mVel = -mBrakingSpeedPx;
267                } else if (!mClosing && mVel < mBrakingSpeedPx) {
268                    mVel = mBrakingSpeedPx;
269                }
270            } else {
271                if (mClosing && mVel > -mFlingCollapseMinVelocityPx) {
272                    mVel = -mFlingCollapseMinVelocityPx;
273                } else if (!mClosing && mVel > mFlingGestureMaxOutputVelocityPx) {
274                    mVel = mFlingGestureMaxOutputVelocityPx;
275                }
276            }
277
278            float h = mExpandedHeight + mVel * dt;
279
280            if (mRubberbanding && h < fh) {
281                h = fh;
282            }
283
284            if (DEBUG) LOG("tick: new h=%d closing=%s", (int) h, mClosing?"true":"false");
285
286            setExpandedHeightInternal(h);
287
288            mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
289
290            if (mVel == 0
291                    || (mClosing && mExpandedHeight == 0)
292                    || ((mRubberbanding || !mClosing) && mExpandedHeight == fh)) {
293                post(mStopAnimator);
294            }
295        } else {
296            Slog.v(TAG, "animationTick called with dtms=" + dtms + "; nothing to do (h="
297                    + mExpandedHeight + " v=" + mVel + ")");
298        }
299    }
300
301    public PanelView(Context context, AttributeSet attrs) {
302        super(context, attrs);
303
304        mTimeAnimator = new TimeAnimator();
305        mTimeAnimator.setTimeListener(mAnimationCallback);
306    }
307
308    private void loadDimens() {
309        final Resources res = getContext().getResources();
310
311        mSelfExpandVelocityPx = res.getDimension(R.dimen.self_expand_velocity);
312        mSelfCollapseVelocityPx = res.getDimension(R.dimen.self_collapse_velocity);
313        mFlingExpandMinVelocityPx = res.getDimension(R.dimen.fling_expand_min_velocity);
314        mFlingCollapseMinVelocityPx = res.getDimension(R.dimen.fling_collapse_min_velocity);
315
316        mFlingGestureMinDistPx = res.getDimension(R.dimen.fling_gesture_min_dist);
317
318        mCollapseMinDisplayFraction = res.getFraction(R.dimen.collapse_min_display_fraction, 1, 1);
319        mExpandMinDisplayFraction = res.getFraction(R.dimen.expand_min_display_fraction, 1, 1);
320
321        mExpandAccelPx = res.getDimension(R.dimen.expand_accel);
322        mCollapseAccelPx = res.getDimension(R.dimen.collapse_accel);
323
324        mFlingGestureMaxXVelocityPx = res.getDimension(R.dimen.fling_gesture_max_x_velocity);
325
326        mFlingGestureMaxOutputVelocityPx = res.getDimension(R.dimen.fling_gesture_max_output_velocity);
327
328        mPeekHeight = res.getDimension(R.dimen.peek_height)
329            + getPaddingBottom() // our window might have a dropshadow
330            - (mHandleView == null ? 0 : mHandleView.getPaddingTop()); // the handle might have a topshadow
331    }
332
333    private void trackMovement(MotionEvent event) {
334        // Add movement to velocity tracker using raw screen X and Y coordinates instead
335        // of window coordinates because the window frame may be moving at the same time.
336        float deltaX = event.getRawX() - event.getX();
337        float deltaY = event.getRawY() - event.getY();
338        event.offsetLocation(deltaX, deltaY);
339        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
340        event.offsetLocation(-deltaX, -deltaY);
341    }
342
343    // Pass all touches along to the handle, allowing the user to drag the panel closed from its interior
344    @Override
345    public boolean onTouchEvent(MotionEvent event) {
346        return mHandleView.dispatchTouchEvent(event);
347    }
348
349    @Override
350    protected void onFinishInflate() {
351        super.onFinishInflate();
352        mHandleView = findViewById(R.id.handle);
353
354        loadDimens();
355
356        if (DEBUG) LOG("handle view: " + mHandleView);
357        if (mHandleView != null) {
358            mHandleView.setOnTouchListener(new View.OnTouchListener() {
359                @Override
360                public boolean onTouch(View v, MotionEvent event) {
361                    final float y = event.getY();
362                    final float rawY = event.getRawY();
363                    if (DEBUG) LOG("handle.onTouch: a=%s y=%.1f rawY=%.1f off=%.1f",
364                            MotionEvent.actionToString(event.getAction()),
365                            y, rawY, mTouchOffset);
366                    PanelView.this.getLocationOnScreen(mAbsPos);
367
368                    switch (event.getAction()) {
369                        case MotionEvent.ACTION_DOWN:
370                            mTracking = true;
371                            mHandleView.setPressed(true);
372                            postInvalidate(); // catch the press state change
373                            mInitialTouchY = y;
374                            mVelocityTracker = FlingTracker.obtain();
375                            trackMovement(event);
376                            mTimeAnimator.cancel(); // end any outstanding animations
377                            mBar.onTrackingStarted(PanelView.this);
378                            mTouchOffset = (rawY - mAbsPos[1]) - PanelView.this.getExpandedHeight();
379                            if (mExpandedHeight == 0) {
380                                mJustPeeked = true;
381                                runPeekAnimation();
382                            }
383                            break;
384
385                        case MotionEvent.ACTION_MOVE:
386                            final float h = rawY - mAbsPos[1] - mTouchOffset;
387                            if (h > mPeekHeight) {
388                                if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
389                                    mPeekAnimator.cancel();
390                                }
391                                mJustPeeked = false;
392                            }
393                            if (!mJustPeeked) {
394                                PanelView.this.setExpandedHeightInternal(h);
395                                mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
396                            }
397
398                            trackMovement(event);
399                            break;
400
401                        case MotionEvent.ACTION_UP:
402                        case MotionEvent.ACTION_CANCEL:
403                            mFinalTouchY = y;
404                            mTracking = false;
405                            mHandleView.setPressed(false);
406                            postInvalidate(); // catch the press state change
407                            mBar.onTrackingStopped(PanelView.this);
408                            trackMovement(event);
409
410                            float vel = 0, yVel = 0, xVel = 0;
411                            boolean negative = false;
412
413                            if (mVelocityTracker != null) {
414                                // the velocitytracker might be null if we got a bad input stream
415                                mVelocityTracker.computeCurrentVelocity(1000);
416
417                                yVel = mVelocityTracker.getYVelocity();
418                                negative = yVel < 0;
419
420                                xVel = mVelocityTracker.getXVelocity();
421                                if (xVel < 0) {
422                                    xVel = -xVel;
423                                }
424                                if (xVel > mFlingGestureMaxXVelocityPx) {
425                                    xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis
426                                }
427
428                                vel = (float)Math.hypot(yVel, xVel);
429                                if (vel > mFlingGestureMaxOutputVelocityPx) {
430                                    vel = mFlingGestureMaxOutputVelocityPx;
431                                }
432
433                                mVelocityTracker.recycle();
434                                mVelocityTracker = null;
435                            }
436
437                            // if you've barely moved your finger, we treat the velocity as 0
438                            // preventing spurious flings due to touch screen jitter
439                            final float deltaY = Math.abs(mFinalTouchY - mInitialTouchY);
440                            if (deltaY < mFlingGestureMinDistPx
441                                    || vel < mFlingExpandMinVelocityPx
442                                    ) {
443                                vel = 0;
444                            }
445
446                            if (negative) {
447                                vel = -vel;
448                            }
449
450                            if (DEBUG) LOG("gesture: dy=%f vel=(%f,%f) vlinear=%f",
451                                    deltaY,
452                                    xVel, yVel,
453                                    vel);
454
455                            fling(vel, true);
456
457                            break;
458                    }
459                    return true;
460                }});
461        }
462    }
463
464    public void fling(float vel, boolean always) {
465        if (DEBUG) LOG("fling: vel=%.3f, this=%s", vel, this);
466        mVel = vel;
467
468        if (always||mVel != 0) {
469            animationTick(0); // begin the animation
470        }
471    }
472
473    @Override
474    protected void onAttachedToWindow() {
475        super.onAttachedToWindow();
476        mViewName = getResources().getResourceName(getId());
477    }
478
479    public String getName() {
480        return mViewName;
481    }
482
483    @Override
484    protected void onViewAdded(View child) {
485        if (DEBUG) LOG("onViewAdded: " + child);
486    }
487
488    public View getHandle() {
489        return mHandleView;
490    }
491
492    // Rubberbands the panel to hold its contents.
493    @Override
494    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
495        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
496
497        if (DEBUG) LOG("onMeasure(%d, %d) -> (%d, %d)",
498                widthMeasureSpec, heightMeasureSpec, getMeasuredWidth(), getMeasuredHeight());
499
500        // Did one of our children change size?
501        int newHeight = getMeasuredHeight();
502        if (newHeight != mFullHeight) {
503            mFullHeight = newHeight;
504            // If the user isn't actively poking us, let's rubberband to the content
505            if (!mTracking && !mRubberbanding && !mTimeAnimator.isStarted()
506                    && mExpandedHeight > 0 && mExpandedHeight != mFullHeight) {
507                mExpandedHeight = mFullHeight;
508            }
509        }
510        heightMeasureSpec = MeasureSpec.makeMeasureSpec(
511                    (int) mExpandedHeight, MeasureSpec.AT_MOST); // MeasureSpec.getMode(heightMeasureSpec));
512        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
513    }
514
515
516    public void setExpandedHeight(float height) {
517        if (DEBUG) LOG("setExpandedHeight(%.1f)", height);
518        mRubberbanding = false;
519        if (mTimeAnimator.isStarted()) {
520            post(mStopAnimator);
521        }
522        setExpandedHeightInternal(height);
523        mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
524    }
525
526    @Override
527    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
528        if (DEBUG) LOG("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom, (int)mExpandedHeight, mFullHeight);
529        super.onLayout(changed, left, top, right, bottom);
530    }
531
532    public void setExpandedHeightInternal(float h) {
533        if (Float.isNaN(h)) {
534            Slog.v(TAG, "setExpandedHeightInternal: warning: h=NaN");
535            // XXX: should set h to 0
536        }
537
538        float fh = getFullHeight();
539        if (fh == 0) {
540            // Hmm, full height hasn't been computed yet
541        }
542
543        if (h < 0) h = 0;
544        if (!(mRubberbandingEnabled && (mTracking || mRubberbanding)) && h > fh) h = fh;
545
546        mExpandedHeight = h;
547
548        if (DEBUG) LOG("setExpansion: height=%.1f fh=%.1f tracking=%s rubber=%s", h, fh, mTracking?"T":"f", mRubberbanding?"T":"f");
549
550        requestLayout();
551//        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
552//        lp.height = (int) mExpandedHeight;
553//        setLayoutParams(lp);
554
555        mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : h / fh);
556    }
557
558    private float getFullHeight() {
559        if (mFullHeight <= 0) {
560            if (DEBUG) LOG("Forcing measure() since fullHeight=" + mFullHeight);
561            measure(MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY),
562                    MeasureSpec.makeMeasureSpec(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, MeasureSpec.EXACTLY));
563        }
564        return mFullHeight;
565    }
566
567    public void setExpandedFraction(float frac) {
568        if (Float.isNaN(frac)) {
569            Slog.v(TAG, "setExpandedFraction: frac=NaN");
570            // XXX: set frac to 0 to defend
571        }
572        setExpandedHeight(getFullHeight() * frac);
573    }
574
575    public float getExpandedHeight() {
576        return mExpandedHeight;
577    }
578
579    public float getExpandedFraction() {
580        return mExpandedFraction;
581    }
582
583    public boolean isFullyExpanded() {
584        return mExpandedHeight >= getFullHeight();
585    }
586
587    public boolean isFullyCollapsed() {
588        return mExpandedHeight <= 0;
589    }
590
591    public boolean isCollapsing() {
592        return mClosing;
593    }
594
595    public void setBar(PanelBar panelBar) {
596        mBar = panelBar;
597    }
598
599    public void collapse() {
600        // TODO: abort animation or ongoing touch
601        if (DEBUG) LOG("collapse: " + this);
602        if (!isFullyCollapsed()) {
603            mTimeAnimator.cancel();
604            mClosing = true;
605            // collapse() should never be a rubberband, even if an animation is already running
606            mRubberbanding = false;
607            fling(-mSelfCollapseVelocityPx, /*always=*/ true);
608        }
609    }
610
611    public void expand() {
612        if (DEBUG) LOG("expand: " + this);
613        if (isFullyCollapsed()) {
614            mBar.startOpeningPanel(this);
615            fling(mSelfExpandVelocityPx, /*always=*/ true);
616        } else if (DEBUG) {
617            if (DEBUG) LOG("skipping expansion: is expanded");
618        }
619    }
620
621    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
622        pw.println(String.format("[PanelView(%s): expandedHeight=%f fullHeight=%f closing=%s"
623                + " tracking=%s rubberbanding=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s"
624                + "]",
625                this.getClass().getSimpleName(),
626                getExpandedHeight(),
627                getFullHeight(),
628                mClosing?"T":"f",
629                mTracking?"T":"f",
630                mRubberbanding?"T":"f",
631                mJustPeeked?"T":"f",
632                mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
633                mTimeAnimator, ((mTimeAnimator!=null && mTimeAnimator.isStarted())?" (started)":"")
634        ));
635    }
636}
637