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