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