PanelView.java revision e29b2dbc762bfa66093d76f5a65f55328d8753c9
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.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.MotionEvent;
29import android.view.ViewConfiguration;
30import android.widget.FrameLayout;
31
32import com.android.systemui.R;
33import com.android.systemui.statusbar.FlingAnimationUtils;
34
35import java.io.FileDescriptor;
36import java.io.PrintWriter;
37
38public abstract class PanelView extends FrameLayout {
39    public static final boolean DEBUG = PanelBar.DEBUG;
40    public static final String TAG = PanelView.class.getSimpleName();
41    protected float mOverExpansion;
42
43    private final void logf(String fmt, Object... args) {
44        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
45    }
46
47    private float mPeekHeight;
48    private float mInitialOffsetOnTouch;
49    private float mExpandedFraction = 0;
50    private float mExpandedHeight = 0;
51    private boolean mJustPeeked;
52    private boolean mClosing;
53    private boolean mTracking;
54    private int mTrackingPointer;
55    protected int mTouchSlop;
56
57    private ValueAnimator mHeightAnimator;
58    private ObjectAnimator mPeekAnimator;
59    private VelocityTrackerInterface mVelocityTracker;
60    private FlingAnimationUtils mFlingAnimationUtils;
61
62    PanelBar mBar;
63
64    protected int mMaxPanelHeight = -1;
65    private String mViewName;
66    private float mInitialTouchY;
67    private float mInitialTouchX;
68
69    protected void onExpandingFinished() {
70        mBar.onExpandingFinished();
71    }
72
73    protected void onExpandingStarted() {
74    }
75
76    private void runPeekAnimation() {
77        if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
78        if (mHeightAnimator != null) {
79            return;
80        }
81        if (mPeekAnimator == null) {
82            mPeekAnimator = ObjectAnimator.ofFloat(this,
83                    "expandedHeight", mPeekHeight)
84                .setDuration(250);
85        }
86        mPeekAnimator.start();
87    }
88
89    public PanelView(Context context, AttributeSet attrs) {
90        super(context, attrs);
91        mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f);
92    }
93
94    protected void loadDimens() {
95        final Resources res = getContext().getResources();
96        mPeekHeight = res.getDimension(R.dimen.peek_height)
97            + getPaddingBottom(); // our window might have a dropshadow
98
99        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
100        mTouchSlop = configuration.getScaledTouchSlop();
101    }
102
103    private void trackMovement(MotionEvent event) {
104        // Add movement to velocity tracker using raw screen X and Y coordinates instead
105        // of window coordinates because the window frame may be moving at the same time.
106        float deltaX = event.getRawX() - event.getX();
107        float deltaY = event.getRawY() - event.getY();
108        event.offsetLocation(deltaX, deltaY);
109        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
110        event.offsetLocation(-deltaX, -deltaY);
111    }
112
113    @Override
114    public boolean onTouchEvent(MotionEvent event) {
115
116        /*
117         * We capture touch events here and update the expand height here in case according to
118         * the users fingers. This also handles multi-touch.
119         *
120         * If the user just clicks shortly, we give him a quick peek of the shade.
121         *
122         * Flinging is also enabled in order to open or close the shade.
123         */
124
125        int pointerIndex = event.findPointerIndex(mTrackingPointer);
126        if (pointerIndex < 0) {
127            pointerIndex = 0;
128            mTrackingPointer = event.getPointerId(pointerIndex);
129        }
130        final float y = event.getY(pointerIndex);
131        final float x = event.getX(pointerIndex);
132
133        boolean waitForTouchSlop = hasConflictingGestures();
134
135        switch (event.getActionMasked()) {
136            case MotionEvent.ACTION_DOWN:
137
138                mInitialTouchY = y;
139                mInitialTouchX = x;
140                mInitialOffsetOnTouch = mExpandedHeight;
141                if (mVelocityTracker == null) {
142                    initVelocityTracker();
143                }
144                trackMovement(event);
145                if (!waitForTouchSlop || mHeightAnimator != null) {
146                    if (mHeightAnimator != null) {
147                        mHeightAnimator.cancel(); // end any outstanding animations
148                    }
149                    onTrackingStarted();
150                }
151                if (mExpandedHeight == 0) {
152                    mJustPeeked = true;
153                    runPeekAnimation();
154                }
155                break;
156
157            case MotionEvent.ACTION_POINTER_UP:
158                final int upPointer = event.getPointerId(event.getActionIndex());
159                if (mTrackingPointer == upPointer) {
160                    // gesture is ongoing, find a new pointer to track
161                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
162                    final float newY = event.getY(newIndex);
163                    final float newX = event.getX(newIndex);
164                    mTrackingPointer = event.getPointerId(newIndex);
165                    mInitialOffsetOnTouch = mExpandedHeight;
166                    mInitialTouchY = newY;
167                    mInitialTouchX = newX;
168                }
169                break;
170
171            case MotionEvent.ACTION_MOVE:
172                float h = y - mInitialTouchY;
173                if (waitForTouchSlop && !mTracking && Math.abs(h) > mTouchSlop
174                        && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
175                    mInitialOffsetOnTouch = mExpandedHeight;
176                    mInitialTouchX = x;
177                    mInitialTouchY = y;
178                    if (mHeightAnimator != null) {
179                        mHeightAnimator.cancel(); // end any outstanding animations
180                    }
181                    onTrackingStarted();
182                    h = 0;
183                }
184                final float newHeight = h + mInitialOffsetOnTouch;
185                if (newHeight > mPeekHeight) {
186                    if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
187                        mPeekAnimator.cancel();
188                    }
189                    mJustPeeked = false;
190                }
191                if (!mJustPeeked && (!waitForTouchSlop || mTracking)) {
192                    setExpandedHeightInternal(newHeight);
193                    mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
194                }
195
196                trackMovement(event);
197                break;
198
199            case MotionEvent.ACTION_UP:
200            case MotionEvent.ACTION_CANCEL:
201                mTrackingPointer = -1;
202                trackMovement(event);
203                float vel = getCurrentVelocity();
204                boolean expand = flingExpands(vel);
205                onTrackingStopped(expand);
206                fling(vel, expand);
207                if (mVelocityTracker != null) {
208                    mVelocityTracker.recycle();
209                    mVelocityTracker = null;
210                }
211                break;
212        }
213        return !waitForTouchSlop || mTracking;
214    }
215
216    protected abstract boolean hasConflictingGestures();
217
218    protected void onTrackingStopped(boolean expand) {
219        mTracking = false;
220        mBar.onTrackingStopped(PanelView.this, expand);
221    }
222
223    protected void onTrackingStarted() {
224        mTracking = true;
225        mBar.onTrackingStarted(PanelView.this);
226        onExpandingStarted();
227    }
228
229    private float getCurrentVelocity() {
230
231        // the velocitytracker might be null if we got a bad input stream
232        if (mVelocityTracker == null) {
233            return 0;
234        }
235        mVelocityTracker.computeCurrentVelocity(1000);
236        return mVelocityTracker.getYVelocity();
237    }
238
239    @Override
240    public boolean onInterceptTouchEvent(MotionEvent event) {
241
242        /*
243         * If the user drags anywhere inside the panel we intercept it if he moves his finger
244         * upwards. This allows closing the shade from anywhere inside the panel.
245         *
246         * We only do this if the current content is scrolled to the bottom,
247         * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
248         * possible.
249         */
250        int pointerIndex = event.findPointerIndex(mTrackingPointer);
251        if (pointerIndex < 0) {
252            pointerIndex = 0;
253            mTrackingPointer = event.getPointerId(pointerIndex);
254        }
255        final float x = event.getX(pointerIndex);
256        final float y = event.getY(pointerIndex);
257        boolean scrolledToBottom = isScrolledToBottom();
258
259        switch (event.getActionMasked()) {
260            case MotionEvent.ACTION_DOWN:
261                if (mHeightAnimator != null) {
262                    mHeightAnimator.cancel(); // end any outstanding animations
263                    return true;
264                }
265                mInitialTouchY = y;
266                mInitialTouchX = x;
267                initVelocityTracker();
268                trackMovement(event);
269                break;
270            case MotionEvent.ACTION_POINTER_UP:
271                final int upPointer = event.getPointerId(event.getActionIndex());
272                if (mTrackingPointer == upPointer) {
273                    // gesture is ongoing, find a new pointer to track
274                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
275                    mTrackingPointer = event.getPointerId(newIndex);
276                    mInitialTouchX = event.getX(newIndex);
277                    mInitialTouchY = event.getY(newIndex);
278                }
279                break;
280
281            case MotionEvent.ACTION_MOVE:
282                final float h = y - mInitialTouchY;
283                trackMovement(event);
284                if (scrolledToBottom) {
285                    if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) {
286                        mInitialOffsetOnTouch = mExpandedHeight;
287                        mInitialTouchY = y;
288                        mInitialTouchX = x;
289                        mTracking = true;
290                        onTrackingStarted();
291                        return true;
292                    }
293                }
294                break;
295        }
296        return false;
297    }
298
299    private void initVelocityTracker() {
300        if (mVelocityTracker != null) {
301            mVelocityTracker.recycle();
302        }
303        mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
304    }
305
306    protected boolean isScrolledToBottom() {
307        return true;
308    }
309
310    protected float getContentHeight() {
311        return mExpandedHeight;
312    }
313
314    @Override
315    protected void onFinishInflate() {
316        super.onFinishInflate();
317        loadDimens();
318    }
319
320    @Override
321    protected void onConfigurationChanged(Configuration newConfig) {
322        super.onConfigurationChanged(newConfig);
323        loadDimens();
324        mMaxPanelHeight = -1;
325    }
326
327    /**
328     * @param vel the current velocity of the motion
329     * @return whether a fling should expands the panel; contracts otherwise
330     */
331    private boolean flingExpands(float vel) {
332        if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
333            return getExpandedFraction() > 0.5f;
334        } else {
335            return vel > 0;
336        }
337    }
338
339    protected void fling(float vel, boolean expand) {
340        cancelPeek();
341        float target = expand ? getMaxPanelHeight() : 0.0f;
342        if (target == mExpandedHeight) {
343            onExpandingFinished();
344            mBar.panelExpansionChanged(this, mExpandedFraction);
345            return;
346        }
347        ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, target);
348        if (expand) {
349            mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
350        } else {
351            mFlingAnimationUtils.applyDismissing(animator, mExpandedHeight, target, vel,
352                    getHeight());
353
354            // Make it shorter if we run a canned animation
355            if (vel == 0) {
356                animator.setDuration((long) (animator.getDuration() / 1.75f));
357            }
358        }
359        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
360            @Override
361            public void onAnimationUpdate(ValueAnimator animation) {
362                setExpandedHeight((Float) animation.getAnimatedValue());
363            }
364        });
365        animator.addListener(new AnimatorListenerAdapter() {
366            @Override
367            public void onAnimationEnd(Animator animation) {
368                mHeightAnimator = null;
369                onExpandingFinished();
370            }
371        });
372        animator.start();
373        mHeightAnimator = animator;
374    }
375
376    @Override
377    protected void onAttachedToWindow() {
378        super.onAttachedToWindow();
379        mViewName = getResources().getResourceName(getId());
380    }
381
382    public String getName() {
383        return mViewName;
384    }
385
386    @Override
387    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
388        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
389
390        if (DEBUG) logf("onMeasure(%d, %d) -> (%d, %d)",
391                widthMeasureSpec, heightMeasureSpec, getMeasuredWidth(), getMeasuredHeight());
392
393        // Did one of our children change size?
394        int newHeight = getMeasuredHeight();
395        if (newHeight > mMaxPanelHeight) {
396            // we only adapt the max height if it's bigger
397            mMaxPanelHeight = newHeight;
398            // If the user isn't actively poking us, let's rubberband to the content
399            if (!mTracking && mHeightAnimator == null
400                    && mExpandedHeight > 0 && mExpandedHeight != mMaxPanelHeight
401                    && mMaxPanelHeight > 0) {
402                mExpandedHeight = mMaxPanelHeight;
403            }
404        }
405    }
406
407    public void setExpandedHeight(float height) {
408        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
409        setExpandedHeightInternal(height);
410        mBar.panelExpansionChanged(PanelView.this, mExpandedFraction);
411    }
412
413    @Override
414    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
415        if (DEBUG) logf("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom,
416                (int)mExpandedHeight, mMaxPanelHeight);
417        super.onLayout(changed, left, top, right, bottom);
418        requestPanelHeightUpdate();
419    }
420
421    protected void requestPanelHeightUpdate() {
422        float currentMaxPanelHeight = getMaxPanelHeight();
423
424        // If the user isn't actively poking us, let's update the height
425        if (!mTracking && mHeightAnimator == null
426                && mExpandedHeight > 0 && currentMaxPanelHeight != mExpandedHeight) {
427            setExpandedHeightInternal(currentMaxPanelHeight);
428        }
429    }
430
431    public void setExpandedHeightInternal(float h) {
432        float fh = getMaxPanelHeight();
433        mExpandedHeight = Math.max(0, Math.min(fh, h));
434        float overExpansion = h - fh;
435        overExpansion = Math.max(0, overExpansion);
436        if (overExpansion != mOverExpansion) {
437            onOverExpansionChanged(overExpansion);
438        }
439
440        if (DEBUG) {
441            logf("setExpansion: height=%.1f fh=%.1f tracking=%s", h, fh, mTracking ? "T" : "f");
442        }
443
444        onHeightUpdated(mExpandedHeight);
445        mExpandedFraction = Math.min(1f, (fh == 0) ? 0 : mExpandedHeight / fh);
446    }
447
448    protected void onOverExpansionChanged(float overExpansion) {
449        mOverExpansion = overExpansion;
450    }
451
452    protected void onHeightUpdated(float expandedHeight) {
453        requestLayout();
454    }
455
456    /**
457     * This returns the maximum height of the panel. Children should override this if their
458     * desired height is not the full height.
459     *
460     * @return the default implementation simply returns the maximum height.
461     */
462    protected int getMaxPanelHeight() {
463        mMaxPanelHeight = Math.max(mMaxPanelHeight, getHeight());
464        return mMaxPanelHeight;
465    }
466
467    public void setExpandedFraction(float frac) {
468        setExpandedHeight(getMaxPanelHeight() * frac);
469    }
470
471    public float getExpandedHeight() {
472        return mExpandedHeight;
473    }
474
475    public float getExpandedFraction() {
476        return mExpandedFraction;
477    }
478
479    public boolean isFullyExpanded() {
480        return mExpandedHeight >= getMaxPanelHeight();
481    }
482
483    public boolean isFullyCollapsed() {
484        return mExpandedHeight <= 0;
485    }
486
487    public boolean isCollapsing() {
488        return mClosing;
489    }
490
491    public boolean isTracking() {
492        return mTracking;
493    }
494
495    public void setBar(PanelBar panelBar) {
496        mBar = panelBar;
497    }
498
499    public void collapse() {
500        // TODO: abort animation or ongoing touch
501        if (DEBUG) logf("collapse: " + this);
502        if (!isFullyCollapsed()) {
503            if (mHeightAnimator != null) {
504                mHeightAnimator.cancel();
505            }
506            mClosing = true;
507            onExpandingStarted();
508            fling(0, false /* expand */);
509        }
510    }
511
512    public void expand() {
513        if (DEBUG) logf("expand: " + this);
514        if (isFullyCollapsed()) {
515            mBar.startOpeningPanel(this);
516            onExpandingStarted();
517            fling(0, true /* expand */);
518        } else if (DEBUG) {
519            if (DEBUG) logf("skipping expansion: is expanded");
520        }
521    }
522
523    public void cancelPeek() {
524        if (mPeekAnimator != null && mPeekAnimator.isStarted()) {
525            mPeekAnimator.cancel();
526        }
527    }
528
529    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
530        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
531                + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s"
532                + "]",
533                this.getClass().getSimpleName(),
534                getExpandedHeight(),
535                getMaxPanelHeight(),
536                mClosing?"T":"f",
537                mTracking?"T":"f",
538                mJustPeeked?"T":"f",
539                mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
540                mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":"")
541        ));
542    }
543
544    public abstract void resetViews();
545}
546