1 package com.android.contacts.widget; 2 3 import com.android.contacts.R; 4 import com.android.contacts.quickcontact.ExpandingEntryCardView; 5 import com.android.contacts.test.NeededForReflection; 6 import com.android.contacts.util.SchedulingUtils; 7 8 import android.animation.Animator; 9 import android.animation.Animator.AnimatorListener; 10 import android.animation.AnimatorListenerAdapter; 11 import android.animation.ObjectAnimator; 12 import android.animation.ValueAnimator; 13 import android.animation.ValueAnimator.AnimatorUpdateListener; 14 import android.content.Context; 15 import android.content.res.TypedArray; 16 import android.graphics.Canvas; 17 import android.graphics.Color; 18 import android.graphics.ColorMatrix; 19 import android.graphics.ColorMatrixColorFilter; 20 import android.graphics.drawable.GradientDrawable; 21 import android.hardware.display.DisplayManager; 22 import android.os.Trace; 23 import android.util.AttributeSet; 24 import android.util.TypedValue; 25 import android.view.Display; 26 import android.view.Gravity; 27 import android.view.MotionEvent; 28 import android.view.VelocityTracker; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.ViewConfiguration; 32 import android.view.animation.AnimationUtils; 33 import android.view.animation.Interpolator; 34 import android.view.animation.PathInterpolator; 35 import android.widget.EdgeEffect; 36 import android.widget.FrameLayout; 37 import android.widget.LinearLayout; 38 import android.widget.Scroller; 39 import android.widget.ScrollView; 40 import android.widget.TextView; 41 import android.widget.Toolbar; 42 43 /** 44 * A custom {@link ViewGroup} that operates similarly to a {@link ScrollView}, except with multiple 45 * subviews. These subviews are scrolled or shrinked one at a time, until each reaches their 46 * minimum or maximum value. 47 * 48 * MultiShrinkScroller is designed for a specific problem. As such, this class is designed to be 49 * used with a specific layout file: quickcontact_activity.xml. MultiShrinkScroller expects subviews 50 * with specific ID values. 51 * 52 * MultiShrinkScroller's code is heavily influenced by ScrollView. Nonetheless, several ScrollView 53 * features are missing. For example: handling of KEYCODES, OverScroll bounce and saving 54 * scroll state in savedInstanceState bundles. 55 * 56 * Before copying this approach to nested scrolling, consider whether something simpler & less 57 * customized will work for you. For example, see the re-usable StickyHeaderListView used by 58 * WifiSetupActivity (very nice). Alternatively, check out Google+'s cover photo scrolling or 59 * Android L's built in nested scrolling support. I thought I needed a more custom ViewGroup in 60 * order to track velocity, modify EdgeEffect color & perform the originally specified animations. 61 * As a result this ViewGroup has non-standard talkback and keyboard support. 62 */ 63 public class MultiShrinkScroller extends FrameLayout { 64 65 /** 66 * 1000 pixels per millisecond. Ie, 1 pixel per second. 67 */ 68 private static final int PIXELS_PER_SECOND = 1000; 69 70 /** 71 * Length of the acceleration animations. This value was taken from ValueAnimator.java. 72 */ 73 private static final int EXIT_FLING_ANIMATION_DURATION_MS = 250; 74 75 /** 76 * In portrait mode, the height:width ratio of the photo's starting height. 77 */ 78 private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.6f; 79 80 /** 81 * Color blending will only be performed on the contact photo once the toolbar is compressed 82 * to this ratio of its full height. 83 */ 84 private static final float COLOR_BLENDING_START_RATIO = 0.5f; 85 86 private static final float SPRING_DAMPENING_FACTOR = 0.01f; 87 88 /** 89 * When displaying a letter tile drawable, this alpha value should be used at the intermediate 90 * toolbar height. 91 */ 92 private static final float DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA = 0.8f; 93 94 private float[] mLastEventPosition = { 0, 0 }; 95 private VelocityTracker mVelocityTracker; 96 private boolean mIsBeingDragged = false; 97 private boolean mReceivedDown = false; 98 /** 99 * Did the current downwards fling/scroll-animation start while we were fullscreen? 100 */ 101 private boolean mIsFullscreenDownwardsFling = false; 102 103 private ScrollView mScrollView; 104 private View mScrollViewChild; 105 private View mToolbar; 106 private QuickContactImageView mPhotoView; 107 private View mPhotoViewContainer; 108 private View mTransparentView; 109 private MultiShrinkScrollerListener mListener; 110 private TextView mLargeTextView; 111 private View mPhotoTouchInterceptOverlay; 112 /** Contains desired size & vertical offset of the title, once the header is fully compressed */ 113 private TextView mInvisiblePlaceholderTextView; 114 private View mTitleGradientView; 115 private View mActionBarGradientView; 116 private View mStartColumn; 117 private int mHeaderTintColor; 118 private int mMaximumHeaderHeight; 119 private int mMinimumHeaderHeight; 120 /** 121 * When the contact photo is tapped, it is resized to max size or this size. This value also 122 * sometimes represents the maximum achievable header size achieved by scrolling. To enforce 123 * this maximum in scrolling logic, always access this value via 124 * {@link #getMaximumScrollableHeaderHeight}. 125 */ 126 private int mIntermediateHeaderHeight; 127 /** 128 * If true, regular scrolling can expand the header beyond mIntermediateHeaderHeight. The 129 * header, that contains the contact photo, can expand to a height equal its width. 130 */ 131 private boolean mIsOpenContactSquare; 132 private int mMaximumHeaderTextSize; 133 private int mCollapsedTitleBottomMargin; 134 private int mCollapsedTitleStartMargin; 135 private int mMinimumPortraitHeaderHeight; 136 private int mMaximumPortraitHeaderHeight; 137 /** 138 * True once the header has touched the top of the screen at least once. 139 */ 140 private boolean mHasEverTouchedTheTop; 141 private boolean mIsTouchDisabledForDismissAnimation; 142 private boolean mIsTouchDisabledForSuppressLayout; 143 144 private final Scroller mScroller; 145 private final EdgeEffect mEdgeGlowBottom; 146 private final EdgeEffect mEdgeGlowTop; 147 private final int mTouchSlop; 148 private final int mMaximumVelocity; 149 private final int mMinimumVelocity; 150 private final int mDismissDistanceOnScroll; 151 private final int mDismissDistanceOnRelease; 152 private final int mSnapToTopSlopHeight; 153 private final int mTransparentStartHeight; 154 private final int mMaximumTitleMargin; 155 private final float mToolbarElevation; 156 private final boolean mIsTwoPanel; 157 private final float mLandscapePhotoRatio; 158 private final int mActionBarSize; 159 160 // Objects used to perform color filtering on the header. These are stored as fields for 161 // the sole purpose of avoiding "new" operations inside animation loops. 162 private final ColorMatrix mWhitenessColorMatrix = new ColorMatrix(); 163 private final ColorMatrix mColorMatrix = new ColorMatrix(); 164 private final float[] mAlphaMatrixValues = { 165 0, 0, 0, 0, 0, 166 0, 0, 0, 0, 0, 167 0, 0, 0, 0, 0, 168 0, 0, 0, 1, 0 169 }; 170 private final ColorMatrix mMultiplyBlendMatrix = new ColorMatrix(); 171 private final float[] mMultiplyBlendMatrixValues = { 172 0, 0, 0, 0, 0, 173 0, 0, 0, 0, 0, 174 0, 0, 0, 0, 0, 175 0, 0, 0, 1, 0 176 }; 177 178 private final PathInterpolator mTextSizePathInterpolator 179 = new PathInterpolator(0.16f, 0.4f, 0.2f, 1); 180 181 private final int[] mGradientColors = new int[] {0,0x88000000}; 182 private GradientDrawable mTitleGradientDrawable = new GradientDrawable( 183 GradientDrawable.Orientation.TOP_BOTTOM, mGradientColors); 184 private GradientDrawable mActionBarGradientDrawable = new GradientDrawable( 185 GradientDrawable.Orientation.BOTTOM_TOP, mGradientColors); 186 187 public interface MultiShrinkScrollerListener { 188 void onScrolledOffBottom(); 189 190 void onStartScrollOffBottom(); 191 192 void onTransparentViewHeightChange(float ratio); 193 194 void onEntranceAnimationDone(); 195 196 void onEnterFullscreen(); 197 198 void onExitFullscreen(); 199 } 200 201 private final AnimatorListener mSnapToBottomListener = new AnimatorListenerAdapter() { 202 @Override 203 public void onAnimationEnd(Animator animation) { 204 if (getScrollUntilOffBottom() > 0 && mListener != null) { 205 // Due to a rounding error, after the animation finished we haven't fully scrolled 206 // off the screen. Lie to the listener: tell it that we did scroll off the screen. 207 mListener.onScrolledOffBottom(); 208 // No other messages need to be sent to the listener. 209 mListener = null; 210 } 211 } 212 }; 213 214 /** 215 * Interpolator from android.support.v4.view.ViewPager. Snappier and more elastic feeling 216 * than the default interpolator. 217 */ 218 private static final Interpolator sInterpolator = new Interpolator() { 219 220 /** 221 * {@inheritDoc} 222 */ 223 @Override 224 public float getInterpolation(float t) { 225 t -= 1.0f; 226 return t * t * t * t * t + 1.0f; 227 } 228 }; 229 230 public MultiShrinkScroller(Context context) { 231 this(context, null); 232 } 233 234 public MultiShrinkScroller(Context context, AttributeSet attrs) { 235 this(context, attrs, 0); 236 } 237 238 public MultiShrinkScroller(Context context, AttributeSet attrs, int defStyleAttr) { 239 super(context, attrs, defStyleAttr); 240 241 final ViewConfiguration configuration = ViewConfiguration.get(context); 242 setFocusable(false); 243 // Drawing must be enabled in order to support EdgeEffect 244 setWillNotDraw(/* willNotDraw = */ false); 245 246 mEdgeGlowBottom = new EdgeEffect(context); 247 mEdgeGlowTop = new EdgeEffect(context); 248 mScroller = new Scroller(context, sInterpolator); 249 mTouchSlop = configuration.getScaledTouchSlop(); 250 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 251 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 252 mTransparentStartHeight = (int) getResources().getDimension( 253 R.dimen.quickcontact_starting_empty_height); 254 mToolbarElevation = getResources().getDimension( 255 R.dimen.quick_contact_toolbar_elevation); 256 mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel); 257 mMaximumTitleMargin = (int) getResources().getDimension( 258 R.dimen.quickcontact_title_initial_margin); 259 260 mDismissDistanceOnScroll = (int) getResources().getDimension( 261 R.dimen.quickcontact_dismiss_distance_on_scroll); 262 mDismissDistanceOnRelease = (int) getResources().getDimension( 263 R.dimen.quickcontact_dismiss_distance_on_release); 264 mSnapToTopSlopHeight = (int) getResources().getDimension( 265 R.dimen.quickcontact_snap_to_top_slop_height); 266 267 final TypedValue photoRatio = new TypedValue(); 268 getResources().getValue(R.dimen.quickcontact_landscape_photo_ratio, photoRatio, 269 /* resolveRefs = */ true); 270 mLandscapePhotoRatio = photoRatio.getFloat(); 271 272 final TypedArray attributeArray = context.obtainStyledAttributes( 273 new int[]{android.R.attr.actionBarSize}); 274 mActionBarSize = attributeArray.getDimensionPixelSize(0, 0); 275 mMinimumHeaderHeight = mActionBarSize; 276 // This value is approximately equal to the portrait ActionBar size. It isn't exactly the 277 // same, since the landscape and portrait ActionBar sizes can be different. 278 mMinimumPortraitHeaderHeight = mMinimumHeaderHeight; 279 attributeArray.recycle(); 280 } 281 282 /** 283 * This method must be called inside the Activity's OnCreate. 284 */ 285 public void initialize(MultiShrinkScrollerListener listener, boolean isOpenContactSquare) { 286 mScrollView = (ScrollView) findViewById(R.id.content_scroller); 287 mScrollViewChild = findViewById(R.id.card_container); 288 mToolbar = findViewById(R.id.toolbar_parent); 289 mPhotoViewContainer = findViewById(R.id.toolbar_parent); 290 mTransparentView = findViewById(R.id.transparent_view); 291 mLargeTextView = (TextView) findViewById(R.id.large_title); 292 mInvisiblePlaceholderTextView = (TextView) findViewById(R.id.placeholder_textview); 293 mStartColumn = findViewById(R.id.empty_start_column); 294 // Touching the empty space should close the card 295 if (mStartColumn != null) { 296 mStartColumn.setOnClickListener(new OnClickListener() { 297 @Override 298 public void onClick(View v) { 299 scrollOffBottom(); 300 } 301 }); 302 findViewById(R.id.empty_end_column).setOnClickListener(new OnClickListener() { 303 @Override 304 public void onClick(View v) { 305 scrollOffBottom(); 306 } 307 }); 308 } 309 mListener = listener; 310 mIsOpenContactSquare = isOpenContactSquare; 311 312 mPhotoView = (QuickContactImageView) findViewById(R.id.photo); 313 314 mTitleGradientView = findViewById(R.id.title_gradient); 315 mTitleGradientView.setBackground(mTitleGradientDrawable); 316 mActionBarGradientView = findViewById(R.id.action_bar_gradient); 317 mActionBarGradientView.setBackground(mActionBarGradientDrawable); 318 mCollapsedTitleStartMargin = ((Toolbar) findViewById(R.id.toolbar)).getContentInsetStart(); 319 320 mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay); 321 if (!mIsTwoPanel) { 322 mPhotoTouchInterceptOverlay.setOnClickListener(new OnClickListener() { 323 @Override 324 public void onClick(View v) { 325 expandHeader(); 326 } 327 }); 328 } 329 330 SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ false, new Runnable() { 331 @Override 332 public void run() { 333 if (!mIsTwoPanel) { 334 // We never want the height of the photo view to exceed its width. 335 mMaximumHeaderHeight = mPhotoViewContainer.getWidth(); 336 mIntermediateHeaderHeight = (int) (mMaximumHeaderHeight 337 * INTERMEDIATE_HEADER_HEIGHT_RATIO); 338 } 339 mMaximumPortraitHeaderHeight = mIsTwoPanel ? getHeight() 340 : mPhotoViewContainer.getWidth(); 341 setHeaderHeight(getMaximumScrollableHeaderHeight()); 342 mMaximumHeaderTextSize = mLargeTextView.getHeight(); 343 if (mIsTwoPanel) { 344 mMaximumHeaderHeight = getHeight(); 345 mMinimumHeaderHeight = mMaximumHeaderHeight; 346 mIntermediateHeaderHeight = mMaximumHeaderHeight; 347 348 // Permanently set photo width and height. 349 final ViewGroup.LayoutParams photoLayoutParams 350 = mPhotoViewContainer.getLayoutParams(); 351 photoLayoutParams.height = mMaximumHeaderHeight; 352 photoLayoutParams.width = (int) (mMaximumHeaderHeight * mLandscapePhotoRatio); 353 mPhotoViewContainer.setLayoutParams(photoLayoutParams); 354 355 // Permanently set title width and margin. 356 final FrameLayout.LayoutParams largeTextLayoutParams 357 = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams(); 358 largeTextLayoutParams.width = photoLayoutParams.width - 359 largeTextLayoutParams.leftMargin - largeTextLayoutParams.rightMargin; 360 largeTextLayoutParams.gravity = Gravity.BOTTOM | Gravity.START; 361 mLargeTextView.setLayoutParams(largeTextLayoutParams); 362 } else { 363 // Set the width of mLargeTextView as if it was nested inside 364 // mPhotoViewContainer. 365 mLargeTextView.setWidth(mPhotoViewContainer.getWidth() 366 - 2 * mMaximumTitleMargin); 367 } 368 369 calculateCollapsedLargeTitlePadding(); 370 updateHeaderTextSizeAndMargin(); 371 configureGradientViewHeights(); 372 } 373 }); 374 } 375 376 private void configureGradientViewHeights() { 377 final FrameLayout.LayoutParams actionBarGradientLayoutParams 378 = (FrameLayout.LayoutParams) mActionBarGradientView.getLayoutParams(); 379 actionBarGradientLayoutParams.height = mActionBarSize; 380 mActionBarGradientView.setLayoutParams(actionBarGradientLayoutParams); 381 final FrameLayout.LayoutParams titleGradientLayoutParams 382 = (FrameLayout.LayoutParams) mTitleGradientView.getLayoutParams(); 383 final float TITLE_GRADIENT_SIZE_COEFFICIENT = 1.25f; 384 final FrameLayout.LayoutParams largeTextLayoutParms 385 = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams(); 386 titleGradientLayoutParams.height = (int) ((mLargeTextView.getHeight() 387 + largeTextLayoutParms.bottomMargin) * TITLE_GRADIENT_SIZE_COEFFICIENT); 388 mTitleGradientView.setLayoutParams(titleGradientLayoutParams); 389 } 390 391 public void setTitle(String title) { 392 mLargeTextView.setText(title); 393 mPhotoTouchInterceptOverlay.setContentDescription(title); 394 } 395 396 @Override 397 public boolean onInterceptTouchEvent(MotionEvent event) { 398 if (mVelocityTracker == null) { 399 mVelocityTracker = VelocityTracker.obtain(); 400 } 401 mVelocityTracker.addMovement(event); 402 403 // The only time we want to intercept touch events is when we are being dragged. 404 return shouldStartDrag(event); 405 } 406 407 private boolean shouldStartDrag(MotionEvent event) { 408 if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return false; 409 410 411 if (mIsBeingDragged) { 412 mIsBeingDragged = false; 413 return false; 414 } 415 416 switch (event.getAction()) { 417 // If we are in the middle of a fling and there is a down event, we'll steal it and 418 // start a drag. 419 case MotionEvent.ACTION_DOWN: 420 updateLastEventPosition(event); 421 if (!mScroller.isFinished()) { 422 startDrag(); 423 return true; 424 } else { 425 mReceivedDown = true; 426 } 427 break; 428 429 // Otherwise, we will start a drag if there is enough motion in the direction we are 430 // capable of scrolling. 431 case MotionEvent.ACTION_MOVE: 432 if (motionShouldStartDrag(event)) { 433 updateLastEventPosition(event); 434 startDrag(); 435 return true; 436 } 437 break; 438 } 439 440 return false; 441 } 442 443 @Override 444 public boolean onTouchEvent(MotionEvent event) { 445 if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return true; 446 447 final int action = event.getAction(); 448 449 if (mVelocityTracker == null) { 450 mVelocityTracker = VelocityTracker.obtain(); 451 } 452 mVelocityTracker.addMovement(event); 453 454 if (!mIsBeingDragged) { 455 if (shouldStartDrag(event)) { 456 return true; 457 } 458 459 if (action == MotionEvent.ACTION_UP && mReceivedDown) { 460 mReceivedDown = false; 461 return performClick(); 462 } 463 return true; 464 } 465 466 switch (action) { 467 case MotionEvent.ACTION_MOVE: 468 final float delta = updatePositionAndComputeDelta(event); 469 scrollTo(0, getScroll() + (int) delta); 470 mReceivedDown = false; 471 472 if (mIsBeingDragged) { 473 final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll(); 474 if (delta > distanceFromMaxScrolling) { 475 // The ScrollView is being pulled upwards while there is no more 476 // content offscreen, and the view port is already fully expanded. 477 mEdgeGlowBottom.onPull(delta / getHeight(), 1 - event.getX() / getWidth()); 478 } 479 480 if (!mEdgeGlowBottom.isFinished()) { 481 postInvalidateOnAnimation(); 482 } 483 484 if (shouldDismissOnScroll()) { 485 scrollOffBottom(); 486 } 487 488 } 489 break; 490 491 case MotionEvent.ACTION_UP: 492 case MotionEvent.ACTION_CANCEL: 493 stopDrag(action == MotionEvent.ACTION_CANCEL); 494 mReceivedDown = false; 495 break; 496 } 497 498 return true; 499 } 500 501 public void setHeaderTintColor(int color) { 502 mHeaderTintColor = color; 503 updatePhotoTintAndDropShadow(); 504 // We want to use the same amount of alpha on the new tint color as the previous tint color. 505 final int edgeEffectAlpha = Color.alpha(mEdgeGlowBottom.getColor()); 506 mEdgeGlowBottom.setColor((color & 0xffffff) | Color.argb(edgeEffectAlpha, 0, 0, 0)); 507 mEdgeGlowTop.setColor(mEdgeGlowBottom.getColor()); 508 } 509 510 /** 511 * Expand to maximum size. 512 */ 513 private void expandHeader() { 514 if (getHeaderHeight() != mMaximumHeaderHeight) { 515 final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight", 516 mMaximumHeaderHeight); 517 animator.setDuration(ExpandingEntryCardView.DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 518 animator.start(); 519 // Scroll nested scroll view to its top 520 if (mScrollView.getScrollY() != 0) { 521 ObjectAnimator.ofInt(mScrollView, "scrollY", -mScrollView.getScrollY()).start(); 522 } 523 } 524 } 525 526 private void startDrag() { 527 mIsBeingDragged = true; 528 mScroller.abortAnimation(); 529 } 530 531 private void stopDrag(boolean cancelled) { 532 mIsBeingDragged = false; 533 if (!cancelled && getChildCount() > 0) { 534 final float velocity = getCurrentVelocity(); 535 if (velocity > mMinimumVelocity || velocity < -mMinimumVelocity) { 536 fling(-velocity); 537 onDragFinished(mScroller.getFinalY() - mScroller.getStartY()); 538 } else { 539 onDragFinished(/* flingDelta = */ 0); 540 } 541 } else { 542 onDragFinished(/* flingDelta = */ 0); 543 } 544 545 if (mVelocityTracker != null) { 546 mVelocityTracker.recycle(); 547 mVelocityTracker = null; 548 } 549 550 mEdgeGlowBottom.onRelease(); 551 } 552 553 private void onDragFinished(int flingDelta) { 554 if (getTransparentViewHeight() <= 0) { 555 // Don't perform any snapping if quick contacts is full screen. 556 return; 557 } 558 if (!snapToTopOnDragFinished(flingDelta)) { 559 // The drag/fling won't result in the content at the top of the Window. Consider 560 // snapping the content to the bottom of the window. 561 snapToBottomOnDragFinished(); 562 } 563 } 564 565 /** 566 * If needed, snap the subviews to the top of the Window. 567 * 568 * @return TRUE if QuickContacts will snap/fling to to top after this method call. 569 */ 570 private boolean snapToTopOnDragFinished(int flingDelta) { 571 if (!mHasEverTouchedTheTop) { 572 // If the current fling is predicted to scroll past the top, then we don't need to snap 573 // to the top. However, if the fling only flings past the top by a tiny amount, 574 // it will look nicer to snap than to fling. 575 final float predictedScrollPastTop = getTransparentViewHeight() - flingDelta; 576 if (predictedScrollPastTop < -mSnapToTopSlopHeight) { 577 return false; 578 } 579 580 if (getTransparentViewHeight() <= mTransparentStartHeight) { 581 // We are above the starting scroll position so snap to the top. 582 mScroller.forceFinished(true); 583 smoothScrollBy(getTransparentViewHeight()); 584 return true; 585 } 586 return false; 587 } 588 if (getTransparentViewHeight() < mDismissDistanceOnRelease) { 589 mScroller.forceFinished(true); 590 smoothScrollBy(getTransparentViewHeight()); 591 return true; 592 } 593 return false; 594 } 595 596 /** 597 * If needed, scroll all the subviews off the bottom of the Window. 598 */ 599 private void snapToBottomOnDragFinished() { 600 if (mHasEverTouchedTheTop) { 601 if (getTransparentViewHeight() > mDismissDistanceOnRelease) { 602 scrollOffBottom(); 603 } 604 return; 605 } 606 if (getTransparentViewHeight() > mTransparentStartHeight) { 607 scrollOffBottom(); 608 } 609 } 610 611 /** 612 * Returns TRUE if we have scrolled far QuickContacts far enough that we should dismiss it 613 * without waiting for the user to finish their drag. 614 */ 615 private boolean shouldDismissOnScroll() { 616 return mHasEverTouchedTheTop && getTransparentViewHeight() > mDismissDistanceOnScroll; 617 } 618 619 /** 620 * Return ratio of non-transparent:viewgroup-height for this viewgroup at the starting position. 621 */ 622 public float getStartingTransparentHeightRatio() { 623 return getTransparentHeightRatio(mTransparentStartHeight); 624 } 625 626 private float getTransparentHeightRatio(int transparentHeight) { 627 final float heightRatio = (float) transparentHeight / getHeight(); 628 // Clamp between [0, 1] in case this is called before height is initialized. 629 return 1.0f - Math.max(Math.min(1.0f, heightRatio), 0f); 630 } 631 632 public void scrollOffBottom() { 633 mIsTouchDisabledForDismissAnimation = true; 634 final Interpolator interpolator = new AcceleratingFlingInterpolator( 635 EXIT_FLING_ANIMATION_DURATION_MS, getCurrentVelocity(), 636 getScrollUntilOffBottom()); 637 mScroller.forceFinished(true); 638 ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll", 639 getScroll() - getScrollUntilOffBottom()); 640 translateAnimation.setRepeatCount(0); 641 translateAnimation.setInterpolator(interpolator); 642 translateAnimation.setDuration(EXIT_FLING_ANIMATION_DURATION_MS); 643 translateAnimation.addListener(mSnapToBottomListener); 644 translateAnimation.start(); 645 if (mListener != null) { 646 mListener.onStartScrollOffBottom(); 647 } 648 } 649 650 /** 651 * @param scrollToCurrentPosition if true, will scroll from the bottom of the screen to the 652 * current position. Otherwise, will scroll from the bottom of the screen to the top of the 653 * screen. 654 */ 655 public void scrollUpForEntranceAnimation(boolean scrollToCurrentPosition) { 656 final int currentPosition = getScroll(); 657 final int bottomScrollPosition = currentPosition 658 - (getHeight() - getTransparentViewHeight()) + 1; 659 final Interpolator interpolator = AnimationUtils.loadInterpolator(getContext(), 660 android.R.interpolator.linear_out_slow_in); 661 final int desiredValue = currentPosition + (scrollToCurrentPosition ? currentPosition 662 : getTransparentViewHeight()); 663 final ObjectAnimator animator = ObjectAnimator.ofInt(this, "scroll", bottomScrollPosition, 664 desiredValue); 665 animator.setInterpolator(interpolator); 666 animator.addUpdateListener(new AnimatorUpdateListener() { 667 @Override 668 public void onAnimationUpdate(ValueAnimator animation) { 669 if (animation.getAnimatedValue().equals(desiredValue) && mListener != null) { 670 mListener.onEntranceAnimationDone(); 671 } 672 } 673 }); 674 animator.start(); 675 } 676 677 @Override 678 public void scrollTo(int x, int y) { 679 final int delta = y - getScroll(); 680 boolean wasFullscreen = getScrollNeededToBeFullScreen() <= 0; 681 if (delta > 0) { 682 scrollUp(delta); 683 } else { 684 scrollDown(delta); 685 } 686 updatePhotoTintAndDropShadow(); 687 updateHeaderTextSizeAndMargin(); 688 final boolean isFullscreen = getScrollNeededToBeFullScreen() <= 0; 689 mHasEverTouchedTheTop |= isFullscreen; 690 if (mListener != null) { 691 if (wasFullscreen && !isFullscreen) { 692 mListener.onExitFullscreen(); 693 } else if (!wasFullscreen && isFullscreen) { 694 mListener.onEnterFullscreen(); 695 } 696 if (!isFullscreen || !wasFullscreen) { 697 mListener.onTransparentViewHeightChange( 698 getTransparentHeightRatio(getTransparentViewHeight())); 699 } 700 } 701 } 702 703 /** 704 * Change the height of the header/toolbar. Do *not* use this outside animations. This was 705 * designed for use by {@link #prepareForShrinkingScrollChild}. 706 */ 707 @NeededForReflection 708 public void setToolbarHeight(int delta) { 709 final ViewGroup.LayoutParams toolbarLayoutParams 710 = mToolbar.getLayoutParams(); 711 toolbarLayoutParams.height = delta; 712 mToolbar.setLayoutParams(toolbarLayoutParams); 713 714 updatePhotoTintAndDropShadow(); 715 updateHeaderTextSizeAndMargin(); 716 } 717 718 @NeededForReflection 719 public int getToolbarHeight() { 720 return mToolbar.getLayoutParams().height; 721 } 722 723 /** 724 * Set the height of the toolbar and update its tint accordingly. 725 */ 726 @NeededForReflection 727 public void setHeaderHeight(int height) { 728 final ViewGroup.LayoutParams toolbarLayoutParams 729 = mToolbar.getLayoutParams(); 730 toolbarLayoutParams.height = height; 731 mToolbar.setLayoutParams(toolbarLayoutParams); 732 updatePhotoTintAndDropShadow(); 733 updateHeaderTextSizeAndMargin(); 734 } 735 736 @NeededForReflection 737 public int getHeaderHeight() { 738 return mToolbar.getLayoutParams().height; 739 } 740 741 @NeededForReflection 742 public void setScroll(int scroll) { 743 scrollTo(0, scroll); 744 } 745 746 /** 747 * Returns the total amount scrolled inside the nested ScrollView + the amount of shrinking 748 * performed on the ToolBar. This is the value inspected by animators. 749 */ 750 @NeededForReflection 751 public int getScroll() { 752 return mTransparentStartHeight - getTransparentViewHeight() 753 + getMaximumScrollableHeaderHeight() - getToolbarHeight() 754 + mScrollView.getScrollY(); 755 } 756 757 private int getMaximumScrollableHeaderHeight() { 758 return mIsOpenContactSquare ? mMaximumHeaderHeight : mIntermediateHeaderHeight; 759 } 760 761 /** 762 * A variant of {@link #getScroll} that pretends the header is never larger than 763 * than mIntermediateHeaderHeight. This function is sometimes needed when making scrolling 764 * decisions that will not change the header size (ie, snapping to the bottom or top). 765 * 766 * When mIsOpenContactSquare is true, this function considers mIntermediateHeaderHeight == 767 * mMaximumHeaderHeight, since snapping decisions will be made relative the full header 768 * size when mIsOpenContactSquare = true. 769 * 770 * This value should never be used in conjunction with {@link #getScroll} values. 771 */ 772 private int getScroll_ignoreOversizedHeaderForSnapping() { 773 return mTransparentStartHeight - getTransparentViewHeight() 774 + Math.max(getMaximumScrollableHeaderHeight() - getToolbarHeight(), 0) 775 + mScrollView.getScrollY(); 776 } 777 778 /** 779 * Amount of transparent space above the header/toolbar. 780 */ 781 public int getScrollNeededToBeFullScreen() { 782 return getTransparentViewHeight(); 783 } 784 785 /** 786 * Return amount of scrolling needed in order for all the visible subviews to scroll off the 787 * bottom. 788 */ 789 private int getScrollUntilOffBottom() { 790 return getHeight() + getScroll_ignoreOversizedHeaderForSnapping() 791 - mTransparentStartHeight; 792 } 793 794 @Override 795 public void computeScroll() { 796 if (mScroller.computeScrollOffset()) { 797 // Examine the fling results in order to activate EdgeEffect and halt flings. 798 final int oldScroll = getScroll(); 799 scrollTo(0, mScroller.getCurrY()); 800 final int delta = mScroller.getCurrY() - oldScroll; 801 final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll(); 802 if (delta > distanceFromMaxScrolling && distanceFromMaxScrolling > 0) { 803 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 804 } 805 if (mIsFullscreenDownwardsFling && getTransparentViewHeight() > 0) { 806 // Halt the fling once QuickContact's top is on screen. 807 scrollTo(0, getScroll() + getTransparentViewHeight()); 808 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 809 mScroller.abortAnimation(); 810 mIsFullscreenDownwardsFling = false; 811 } 812 if (!awakenScrollBars()) { 813 // Keep on drawing until the animation has finished. 814 postInvalidateOnAnimation(); 815 } 816 if (mScroller.getCurrY() >= getMaximumScrollUpwards()) { 817 // Halt the fling once QuickContact's bottom is on screen. 818 mScroller.abortAnimation(); 819 mIsFullscreenDownwardsFling = false; 820 } 821 } 822 } 823 824 @Override 825 public void draw(Canvas canvas) { 826 super.draw(canvas); 827 828 final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 829 final int height = getHeight(); 830 831 if (!mEdgeGlowBottom.isFinished()) { 832 final int restoreCount = canvas.save(); 833 834 // Draw the EdgeEffect on the bottom of the Window (Or a little bit below the bottom 835 // of the Window if we start to scroll upwards while EdgeEffect is visible). This 836 // does not need to consider the case where this MultiShrinkScroller doesn't fill 837 // the Window, since the nested ScrollView should be set to fillViewport. 838 canvas.translate(-width + getPaddingLeft(), 839 height + getMaximumScrollUpwards() - getScroll()); 840 841 canvas.rotate(180, width, 0); 842 if (mIsTwoPanel) { 843 // Only show the EdgeEffect on the bottom of the ScrollView. 844 mEdgeGlowBottom.setSize(mScrollView.getWidth(), height); 845 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 846 canvas.translate(mPhotoViewContainer.getWidth(), 0); 847 } 848 } else { 849 mEdgeGlowBottom.setSize(width, height); 850 } 851 if (mEdgeGlowBottom.draw(canvas)) { 852 postInvalidateOnAnimation(); 853 } 854 canvas.restoreToCount(restoreCount); 855 } 856 857 if (!mEdgeGlowTop.isFinished()) { 858 final int restoreCount = canvas.save(); 859 if (mIsTwoPanel) { 860 mEdgeGlowTop.setSize(mScrollView.getWidth(), height); 861 if (getLayoutDirection() != View.LAYOUT_DIRECTION_RTL) { 862 canvas.translate(mPhotoViewContainer.getWidth(), 0); 863 } 864 } else { 865 mEdgeGlowTop.setSize(width, height); 866 } 867 if (mEdgeGlowTop.draw(canvas)) { 868 postInvalidateOnAnimation(); 869 } 870 canvas.restoreToCount(restoreCount); 871 } 872 } 873 874 private float getCurrentVelocity() { 875 if (mVelocityTracker == null) { 876 return 0; 877 } 878 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumVelocity); 879 return mVelocityTracker.getYVelocity(); 880 } 881 882 private void fling(float velocity) { 883 // For reasons I do not understand, scrolling is less janky when maxY=Integer.MAX_VALUE 884 // then when maxY is set to an actual value. 885 mScroller.fling(0, getScroll(), 0, (int) velocity, 0, 0, -Integer.MAX_VALUE, 886 Integer.MAX_VALUE); 887 if (velocity < 0 && mTransparentView.getHeight() <= 0) { 888 mIsFullscreenDownwardsFling = true; 889 } 890 invalidate(); 891 } 892 893 private int getMaximumScrollUpwards() { 894 if (!mIsTwoPanel) { 895 return mTransparentStartHeight 896 // How much the Header view can compress 897 + getMaximumScrollableHeaderHeight() - getFullyCompressedHeaderHeight() 898 // How much the ScrollView can scroll. 0, if child is smaller than ScrollView. 899 + Math.max(0, mScrollViewChild.getHeight() - getHeight() 900 + getFullyCompressedHeaderHeight()); 901 } else { 902 return mTransparentStartHeight 903 // How much the ScrollView can scroll. 0, if child is smaller than ScrollView. 904 + Math.max(0, mScrollViewChild.getHeight() - getHeight()); 905 } 906 } 907 908 private int getTransparentViewHeight() { 909 return mTransparentView.getLayoutParams().height; 910 } 911 912 private void setTransparentViewHeight(int height) { 913 mTransparentView.getLayoutParams().height = height; 914 mTransparentView.setLayoutParams(mTransparentView.getLayoutParams()); 915 } 916 917 private void scrollUp(int delta) { 918 if (getTransparentViewHeight() != 0) { 919 final int originalValue = getTransparentViewHeight(); 920 setTransparentViewHeight(getTransparentViewHeight() - delta); 921 setTransparentViewHeight(Math.max(0, getTransparentViewHeight())); 922 delta -= originalValue - getTransparentViewHeight(); 923 } 924 final ViewGroup.LayoutParams toolbarLayoutParams 925 = mToolbar.getLayoutParams(); 926 if (toolbarLayoutParams.height > getFullyCompressedHeaderHeight()) { 927 final int originalValue = toolbarLayoutParams.height; 928 toolbarLayoutParams.height -= delta; 929 toolbarLayoutParams.height = Math.max(toolbarLayoutParams.height, 930 getFullyCompressedHeaderHeight()); 931 mToolbar.setLayoutParams(toolbarLayoutParams); 932 delta -= originalValue - toolbarLayoutParams.height; 933 } 934 mScrollView.scrollBy(0, delta); 935 } 936 937 /** 938 * Returns the minimum size that we want to compress the header to, given that we don't want to 939 * allow the the ScrollView to scroll unless there is new content off of the edge of ScrollView. 940 */ 941 private int getFullyCompressedHeaderHeight() { 942 return Math.min(Math.max(mToolbar.getLayoutParams().height - getOverflowingChildViewSize(), 943 mMinimumHeaderHeight), getMaximumScrollableHeaderHeight()); 944 } 945 946 /** 947 * Returns the amount of mScrollViewChild that doesn't fit inside its parent. 948 */ 949 private int getOverflowingChildViewSize() { 950 final int usedScrollViewSpace = mScrollViewChild.getHeight(); 951 return -getHeight() + usedScrollViewSpace + mToolbar.getLayoutParams().height; 952 } 953 954 private void scrollDown(int delta) { 955 if (mScrollView.getScrollY() > 0) { 956 final int originalValue = mScrollView.getScrollY(); 957 mScrollView.scrollBy(0, delta); 958 delta -= mScrollView.getScrollY() - originalValue; 959 } 960 final ViewGroup.LayoutParams toolbarLayoutParams = mToolbar.getLayoutParams(); 961 if (toolbarLayoutParams.height < getMaximumScrollableHeaderHeight()) { 962 final int originalValue = toolbarLayoutParams.height; 963 toolbarLayoutParams.height -= delta; 964 toolbarLayoutParams.height = Math.min(toolbarLayoutParams.height, 965 getMaximumScrollableHeaderHeight()); 966 mToolbar.setLayoutParams(toolbarLayoutParams); 967 delta -= originalValue - toolbarLayoutParams.height; 968 } 969 setTransparentViewHeight(getTransparentViewHeight() - delta); 970 971 if (getScrollUntilOffBottom() <= 0) { 972 post(new Runnable() { 973 @Override 974 public void run() { 975 if (mListener != null) { 976 mListener.onScrolledOffBottom(); 977 // No other messages need to be sent to the listener. 978 mListener = null; 979 } 980 } 981 }); 982 } 983 } 984 985 /** 986 * Set the header size and padding, based on the current scroll position. 987 */ 988 private void updateHeaderTextSizeAndMargin() { 989 if (mIsTwoPanel) { 990 // The text size stays at a constant size & location in two panel layouts. 991 return; 992 } 993 994 // The pivot point for scaling should be middle of the starting side. 995 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 996 mLargeTextView.setPivotX(mLargeTextView.getWidth()); 997 } else { 998 mLargeTextView.setPivotX(0); 999 } 1000 mLargeTextView.setPivotY(mLargeTextView.getHeight() / 2); 1001 1002 final int toolbarHeight = mToolbar.getLayoutParams().height; 1003 mPhotoTouchInterceptOverlay.setClickable(toolbarHeight != mMaximumHeaderHeight); 1004 1005 if (toolbarHeight >= mMaximumHeaderHeight) { 1006 // Everything is full size when the header is fully expanded. 1007 mLargeTextView.setScaleX(1); 1008 mLargeTextView.setScaleY(1); 1009 setInterpolatedTitleMargins(1); 1010 return; 1011 } 1012 1013 final float ratio = (toolbarHeight - mMinimumHeaderHeight) 1014 / (float)(mMaximumHeaderHeight - mMinimumHeaderHeight); 1015 final float minimumSize = mInvisiblePlaceholderTextView.getHeight(); 1016 float bezierOutput = mTextSizePathInterpolator.getInterpolation(ratio); 1017 float scale = (minimumSize + (mMaximumHeaderTextSize - minimumSize) * bezierOutput) 1018 / mMaximumHeaderTextSize; 1019 1020 // Clamp to reasonable/finite values before passing into framework. The values 1021 // can be wacky before the first pre-render. 1022 bezierOutput = (float) Math.min(bezierOutput, 1.0f); 1023 scale = (float) Math.min(scale, 1.0f); 1024 1025 mLargeTextView.setScaleX(scale); 1026 mLargeTextView.setScaleY(scale); 1027 setInterpolatedTitleMargins(bezierOutput); 1028 } 1029 1030 /** 1031 * Calculate the padding around mLargeTextView so that it will look appropriate once it 1032 * finishes moving into its target location/size. 1033 */ 1034 private void calculateCollapsedLargeTitlePadding() { 1035 int invisiblePlaceHolderLocation[] = new int[2]; 1036 int largeTextViewRectLocation[] = new int[2]; 1037 mInvisiblePlaceholderTextView.getLocationOnScreen(invisiblePlaceHolderLocation); 1038 mToolbar.getLocationOnScreen(largeTextViewRectLocation); 1039 // Distance between top of toolbar to the center of the target rectangle. 1040 final int desiredTopToCenter = invisiblePlaceHolderLocation[1] 1041 + mInvisiblePlaceholderTextView.getHeight() / 2 1042 - largeTextViewRectLocation[1]; 1043 // Padding needed on the mLargeTextView so that it has the same amount of 1044 // padding as the target rectangle. 1045 mCollapsedTitleBottomMargin = desiredTopToCenter - mLargeTextView.getHeight() / 2; 1046 } 1047 1048 /** 1049 * Interpolate the title's margin size. When {@param x}=1, use the maximum title margins. 1050 * When {@param x}=0, use the margin values taken from {@link #mInvisiblePlaceholderTextView}. 1051 */ 1052 private void setInterpolatedTitleMargins(float x) { 1053 final FrameLayout.LayoutParams titleLayoutParams 1054 = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams(); 1055 final LinearLayout.LayoutParams toolbarLayoutParams 1056 = (LinearLayout.LayoutParams) mToolbar.getLayoutParams(); 1057 1058 // Need to add more to margin start if there is a start column 1059 int startColumnWidth = mStartColumn == null ? 0 : mStartColumn.getWidth(); 1060 1061 titleLayoutParams.setMarginStart((int) (mCollapsedTitleStartMargin * (1 - x) 1062 + mMaximumTitleMargin * x) + startColumnWidth); 1063 // How offset the title should be from the bottom of the toolbar 1064 final int pretendBottomMargin = (int) (mCollapsedTitleBottomMargin * (1 - x) 1065 + mMaximumTitleMargin * x) ; 1066 // Calculate how offset the title should be from the top of the screen. Instead of 1067 // calling mLargeTextView.getHeight() use the mMaximumHeaderTextSize for this calculation. 1068 // The getHeight() value acts unexpectedly when mLargeTextView is partially clipped by 1069 // its parent. 1070 titleLayoutParams.topMargin = getTransparentViewHeight() 1071 + toolbarLayoutParams.height - pretendBottomMargin 1072 - mMaximumHeaderTextSize; 1073 titleLayoutParams.bottomMargin = 0; 1074 mLargeTextView.setLayoutParams(titleLayoutParams); 1075 } 1076 1077 private void updatePhotoTintAndDropShadow() { 1078 // Let's keep an eye on how long this method takes to complete. 1079 Trace.beginSection("updatePhotoTintAndDropShadow"); 1080 1081 if (mIsTwoPanel && !mPhotoView.isBasedOffLetterTile()) { 1082 // When in two panel mode, UX considers photo tinting unnecessary for non letter 1083 // tile photos. 1084 mTitleGradientDrawable.setAlpha(0xFF); 1085 mActionBarGradientDrawable.setAlpha(0xFF); 1086 return; 1087 } 1088 1089 // We need to use toolbarLayoutParams to determine the height, since the layout 1090 // params can be updated before the height change is reflected inside the View#getHeight(). 1091 final int toolbarHeight = getToolbarHeight(); 1092 1093 if (toolbarHeight <= mMinimumHeaderHeight && !mIsTwoPanel) { 1094 mPhotoViewContainer.setElevation(mToolbarElevation); 1095 } else { 1096 mPhotoViewContainer.setElevation(0); 1097 } 1098 1099 // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint. 1100 mPhotoView.clearColorFilter(); 1101 mColorMatrix.reset(); 1102 1103 final int gradientAlpha; 1104 if (!mPhotoView.isBasedOffLetterTile()) { 1105 // Constants and equations were arbitrarily picked to choose values for saturation, 1106 // whiteness, tint and gradient alpha. There were four main objectives: 1107 // 1) The transition period between the unmodified image and fully colored image should 1108 // be very short. 1109 // 2) The tinting should be fully applied even before the background image is fully 1110 // faded out and desaturated. Why? A half tinted photo looks bad and results in 1111 // unappealing colors. 1112 // 3) The function should have a derivative of 0 at ratio = 1 to avoid discontinuities. 1113 // 4) The entire process should look awesome. 1114 final float ratio = calculateHeightRatioToBlendingStartHeight(toolbarHeight); 1115 final float alpha = 1.0f - (float) Math.min(Math.pow(ratio, 1.5f) * 2f, 1f); 1116 final float tint = (float) Math.min(Math.pow(ratio, 1.5f) * 3f, 1f); 1117 mColorMatrix.setSaturation(alpha); 1118 mColorMatrix.postConcat(alphaMatrix(alpha, Color.WHITE)); 1119 mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, tint)); 1120 gradientAlpha = (int) (255 * alpha); 1121 } else if (mIsTwoPanel) { 1122 mColorMatrix.reset(); 1123 mColorMatrix.postConcat(alphaMatrix(DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA, 1124 mHeaderTintColor)); 1125 gradientAlpha = 0; 1126 } else { 1127 // We want a function that has DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA value 1128 // at the intermediate position and uses TILE_EXPONENT. Finding an equation 1129 // that satisfies this condition requires the following arithmetic. 1130 final float ratio = calculateHeightRatioToFullyOpen(toolbarHeight); 1131 final float intermediateRatio = calculateHeightRatioToFullyOpen((int) 1132 (mMaximumPortraitHeaderHeight * INTERMEDIATE_HEADER_HEIGHT_RATIO)); 1133 final float TILE_EXPONENT = 3f; 1134 final float slowingFactor = (float) ((1 - intermediateRatio) / intermediateRatio 1135 / (1 - Math.pow(1 - DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA, 1/TILE_EXPONENT))); 1136 float linearBeforeIntermediate = Math.max(1 - (1 - ratio) / intermediateRatio 1137 / slowingFactor, 0); 1138 float colorAlpha = 1 - (float) Math.pow(linearBeforeIntermediate, TILE_EXPONENT); 1139 mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor)); 1140 gradientAlpha = 0; 1141 } 1142 1143 // TODO: remove re-allocation of ColorMatrixColorFilter objects (b/17627000) 1144 mPhotoView.setColorFilter(new ColorMatrixColorFilter(mColorMatrix)); 1145 1146 // Tell the photo view what tint we are trying to achieve. Depending on the type of 1147 // drawable used, the photo view may or may not use this tint. 1148 mPhotoView.setTint(mHeaderTintColor); 1149 mTitleGradientDrawable.setAlpha(gradientAlpha); 1150 mActionBarGradientDrawable.setAlpha(gradientAlpha); 1151 1152 Trace.endSection(); 1153 } 1154 1155 private float calculateHeightRatioToFullyOpen(int height) { 1156 return (height - mMinimumPortraitHeaderHeight) 1157 / (float) (mMaximumPortraitHeaderHeight - mMinimumPortraitHeaderHeight); 1158 } 1159 1160 private float calculateHeightRatioToBlendingStartHeight(int height) { 1161 final float intermediateHeight = mMaximumPortraitHeaderHeight 1162 * COLOR_BLENDING_START_RATIO; 1163 final float interpolatingHeightRange = intermediateHeight - mMinimumPortraitHeaderHeight; 1164 if (height > intermediateHeight) { 1165 return 0; 1166 } 1167 return (intermediateHeight - height) / interpolatingHeightRange; 1168 } 1169 1170 /** 1171 * Simulates alpha blending an image with {@param color}. 1172 */ 1173 private ColorMatrix alphaMatrix(float alpha, int color) { 1174 mAlphaMatrixValues[0] = Color.red(color) * alpha / 255; 1175 mAlphaMatrixValues[6] = Color.green(color) * alpha / 255; 1176 mAlphaMatrixValues[12] = Color.blue(color) * alpha / 255; 1177 mAlphaMatrixValues[4] = 255 * (1 - alpha); 1178 mAlphaMatrixValues[9] = 255 * (1 - alpha); 1179 mAlphaMatrixValues[14] = 255 * (1 - alpha); 1180 mWhitenessColorMatrix.set(mAlphaMatrixValues); 1181 return mWhitenessColorMatrix; 1182 } 1183 1184 /** 1185 * Simulates multiply blending an image with a single {@param color}. 1186 * 1187 * Multiply blending is [Sa * Da, Sc * Dc]. See {@link android.graphics.PorterDuff}. 1188 */ 1189 private ColorMatrix multiplyBlendMatrix(int color, float alpha) { 1190 mMultiplyBlendMatrixValues[0] = multiplyBlend(Color.red(color), alpha); 1191 mMultiplyBlendMatrixValues[6] = multiplyBlend(Color.green(color), alpha); 1192 mMultiplyBlendMatrixValues[12] = multiplyBlend(Color.blue(color), alpha); 1193 mMultiplyBlendMatrix.set(mMultiplyBlendMatrixValues); 1194 return mMultiplyBlendMatrix; 1195 } 1196 1197 private float multiplyBlend(int color, float alpha) { 1198 return color * alpha / 255.0f + (1 - alpha); 1199 } 1200 1201 private void updateLastEventPosition(MotionEvent event) { 1202 mLastEventPosition[0] = event.getX(); 1203 mLastEventPosition[1] = event.getY(); 1204 } 1205 1206 private boolean motionShouldStartDrag(MotionEvent event) { 1207 final float deltaY = event.getY() - mLastEventPosition[1]; 1208 return deltaY > mTouchSlop || deltaY < -mTouchSlop; 1209 } 1210 1211 private float updatePositionAndComputeDelta(MotionEvent event) { 1212 final int VERTICAL = 1; 1213 final float position = mLastEventPosition[VERTICAL]; 1214 updateLastEventPosition(event); 1215 float elasticityFactor = 1; 1216 if (position < mLastEventPosition[VERTICAL] && mHasEverTouchedTheTop) { 1217 // As QuickContacts is dragged from the top of the window, its rate of movement will 1218 // slow down in proportion to its distance from the top. This will feel springy. 1219 elasticityFactor += mTransparentView.getHeight() * SPRING_DAMPENING_FACTOR; 1220 } 1221 return (position - mLastEventPosition[VERTICAL]) / elasticityFactor; 1222 } 1223 1224 private void smoothScrollBy(int delta) { 1225 if (delta == 0) { 1226 // Delta=0 implies the code calling smoothScrollBy is sloppy. We should avoid doing 1227 // this, since it prevents Views from being able to register any clicks for 250ms. 1228 throw new IllegalArgumentException("Smooth scrolling by delta=0 is " 1229 + "pointless and harmful"); 1230 } 1231 mScroller.startScroll(0, getScroll(), 0, delta); 1232 invalidate(); 1233 } 1234 1235 /** 1236 * Interpolator that enforces a specific starting velocity. This is useful to avoid a 1237 * discontinuity between dragging speed and flinging speed. 1238 * 1239 * Similar to a {@link android.view.animation.AccelerateInterpolator} in the sense that 1240 * getInterpolation() is a quadratic function. 1241 */ 1242 private class AcceleratingFlingInterpolator implements Interpolator { 1243 1244 private final float mStartingSpeedPixelsPerFrame; 1245 private final float mDurationMs; 1246 private final int mPixelsDelta; 1247 private final float mNumberFrames; 1248 1249 public AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond, 1250 int pixelsDelta) { 1251 mStartingSpeedPixelsPerFrame = startingSpeedPixelsPerSecond / getRefreshRate(); 1252 mDurationMs = durationMs; 1253 mPixelsDelta = pixelsDelta; 1254 mNumberFrames = mDurationMs / getFrameIntervalMs(); 1255 } 1256 1257 @Override 1258 public float getInterpolation(float input) { 1259 final float animationIntervalNumber = mNumberFrames * input; 1260 final float linearDelta = (animationIntervalNumber * mStartingSpeedPixelsPerFrame) 1261 / mPixelsDelta; 1262 // Add the results of a linear interpolator (with the initial speed) with the 1263 // results of a AccelerateInterpolator. 1264 if (mStartingSpeedPixelsPerFrame > 0) { 1265 return Math.min(input * input + linearDelta, 1); 1266 } else { 1267 // Initial fling was in the wrong direction, make sure that the quadratic component 1268 // grows faster in order to make up for this. 1269 return Math.min(input * (input - linearDelta) + linearDelta, 1); 1270 } 1271 } 1272 1273 private float getRefreshRate() { 1274 final DisplayManager displayManager = (DisplayManager) MultiShrinkScroller 1275 .this.getContext().getSystemService(Context.DISPLAY_SERVICE); 1276 return displayManager.getDisplay(Display.DEFAULT_DISPLAY).getRefreshRate(); 1277 } 1278 1279 public long getFrameIntervalMs() { 1280 return (long)(1000 / getRefreshRate()); 1281 } 1282 } 1283 1284 /** 1285 * Expand the header if the mScrollViewChild is about to shrink by enough to create new empty 1286 * space at the bottom of this ViewGroup. 1287 */ 1288 public void prepareForShrinkingScrollChild(int heightDelta) { 1289 final int newEmptyScrollViewSpace = -getOverflowingChildViewSize() + heightDelta; 1290 if (newEmptyScrollViewSpace > 0 && !mIsTwoPanel) { 1291 final int newDesiredToolbarHeight = Math.min(getToolbarHeight() 1292 + newEmptyScrollViewSpace, getMaximumScrollableHeaderHeight()); 1293 ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).setDuration( 1294 ExpandingEntryCardView.DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS).start(); 1295 } 1296 } 1297 1298 /** 1299 * If {@param areTouchesDisabled} is TRUE, ignore all of the user's touches. 1300 */ 1301 public void setDisableTouchesForSuppressLayout(boolean areTouchesDisabled) { 1302 // The card expansion animation uses the Transition framework's ChangeBounds API. This 1303 // invokes suppressLayout(true) on the MultiShrinkScroller. As a result, we need to avoid 1304 // all layout changes during expansion in order to avoid weird layout artifacts. 1305 mIsTouchDisabledForSuppressLayout = areTouchesDisabled; 1306 } 1307 } 1308