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