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