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