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