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