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.Rect; 21 import android.graphics.drawable.GradientDrawable; 22 import android.hardware.display.DisplayManagerGlobal; 23 import android.os.Trace; 24 import android.util.AttributeSet; 25 import android.util.TypedValue; 26 import android.view.Display; 27 import android.view.DisplayInfo; 28 import android.view.Gravity; 29 import android.view.MotionEvent; 30 import android.view.VelocityTracker; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.ViewConfiguration; 34 import android.view.animation.AnimationUtils; 35 import android.view.animation.Interpolator; 36 import android.view.animation.PathInterpolator; 37 import android.widget.EdgeEffect; 38 import android.widget.FrameLayout; 39 import android.widget.LinearLayout; 40 import android.widget.Scroller; 41 import android.widget.ScrollView; 42 import android.widget.TextView; 43 import android.widget.Toolbar; 44 45 /** 46 * A custom {@link ViewGroup} that operates similarly to a {@link ScrollView}, except with multiple 47 * subviews. These subviews are scrolled or shrinked one at a time, until each reaches their 48 * minimum or maximum value. 49 * 50 * MultiShrinkScroller is designed for a specific problem. As such, this class is designed to be 51 * used with a specific layout file: quickcontact_activity.xml. MultiShrinkScroller expects subviews 52 * with specific ID values. 53 * 54 * MultiShrinkScroller's code is heavily influenced by ScrollView. Nonetheless, several ScrollView 55 * features are missing. For example: handling of KEYCODES, OverScroll bounce and saving 56 * scroll state in savedInstanceState bundles. 57 * 58 * Before copying this approach to nested scrolling, consider whether something simpler & less 59 * customized will work for you. For example, see the re-usable StickyHeaderListView used by 60 * WifiSetupActivity (very nice). Alternatively, check out Google+'s cover photo scrolling or 61 * Android L's built in nested scrolling support. I thought I needed a more custom ViewGroup in 62 * order to track velocity, modify EdgeEffect color & perform the originally specified animations. 63 * As a result this ViewGroup has non-standard talkback and keyboard support. 64 */ 65 public class MultiShrinkScroller extends FrameLayout { 66 67 /** 68 * 1000 pixels per millisecond. Ie, 1 pixel per second. 69 */ 70 private static final int PIXELS_PER_SECOND = 1000; 71 72 /** 73 * Length of the acceleration animations. This value was taken from ValueAnimator.java. 74 */ 75 private static final int EXIT_FLING_ANIMATION_DURATION_MS = 250; 76 77 /** 78 * In portrait mode, the height:width ratio of the photo's starting height. 79 */ 80 private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.6f; 81 82 /** 83 * Color blending will only be performed on the contact photo once the toolbar is compressed 84 * to this ratio of its full height. 85 */ 86 private static final float COLOR_BLENDING_START_RATIO = 0.5f; 87 88 private static final float SPRING_DAMPENING_FACTOR = 0.01f; 89 90 /** 91 * When displaying a letter tile drawable, this alpha value should be used at the intermediate 92 * toolbar height. 93 */ 94 private static final float DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA = 0.8f; 95 96 private float[] mLastEventPosition = { 0, 0 }; 97 private VelocityTracker mVelocityTracker; 98 private boolean mIsBeingDragged = false; 99 private boolean mReceivedDown = false; 100 /** 101 * Did the current downwards fling/scroll-animation start while we were fullscreen? 102 */ 103 private boolean mIsFullscreenDownwardsFling = false; 104 105 private ScrollView mScrollView; 106 private View mScrollViewChild; 107 private View mToolbar; 108 private QuickContactImageView mPhotoView; 109 private View mPhotoViewContainer; 110 private View mTransparentView; 111 private MultiShrinkScrollerListener mListener; 112 private TextView mLargeTextView; 113 private View mPhotoTouchInterceptOverlay; 114 /** Contains desired size & vertical offset of the title, once the header is fully compressed */ 115 private TextView mInvisiblePlaceholderTextView; 116 private View mTitleGradientView; 117 private View mActionBarGradientView; 118 private View mStartColumn; 119 private int mHeaderTintColor; 120 private int mMaximumHeaderHeight; 121 private int mMinimumHeaderHeight; 122 /** 123 * When the contact photo is tapped, it is resized to max size or this size. This value also 124 * sometimes represents the maximum achievable header size achieved by scrolling. To enforce 125 * this maximum in scrolling logic, always access this value via 126 * {@link #getMaximumScrollableHeaderHeight}. 127 */ 128 private int mIntermediateHeaderHeight; 129 /** 130 * If true, regular scrolling can expand the header beyond mIntermediateHeaderHeight. The 131 * header, that contains the contact photo, can expand to a height equal its width. 132 */ 133 private boolean mIsOpenContactSquare; 134 private int mMaximumHeaderTextSize; 135 private int mCollapsedTitleBottomMargin; 136 private int mCollapsedTitleStartMargin; 137 private int mMinimumPortraitHeaderHeight; 138 private int mMaximumPortraitHeaderHeight; 139 /** 140 * True once the header has touched the top of the screen at least once. 141 */ 142 private boolean mHasEverTouchedTheTop; 143 private boolean mIsTouchDisabledForDismissAnimation; 144 145 private final Scroller mScroller; 146 private final EdgeEffect mEdgeGlowBottom; 147 private final EdgeEffect mEdgeGlowTop; 148 private final int mTouchSlop; 149 private final int mMaximumVelocity; 150 private final int mMinimumVelocity; 151 private final int mDismissDistanceOnScroll; 152 private final int mDismissDistanceOnRelease; 153 private final int mSnapToTopSlopHeight; 154 private final int mTransparentStartHeight; 155 private final int mMaximumTitleMargin; 156 private final float mToolbarElevation; 157 private final boolean mIsTwoPanel; 158 private final float mLandscapePhotoRatio; 159 private final int mActionBarSize; 160 161 // Objects used to perform color filtering on the header. These are stored as fields for 162 // the sole purpose of avoiding "new" operations inside animation loops. 163 private final ColorMatrix mWhitenessColorMatrix = new ColorMatrix(); 164 private final ColorMatrix mColorMatrix = new ColorMatrix(); 165 private final float[] mAlphaMatrixValues = { 166 0, 0, 0, 0, 0, 167 0, 0, 0, 0, 0, 168 0, 0, 0, 0, 0, 169 0, 0, 0, 1, 0 170 }; 171 private final ColorMatrix mMultiplyBlendMatrix = new ColorMatrix(); 172 private final float[] mMultiplyBlendMatrixValues = { 173 0, 0, 0, 0, 0, 174 0, 0, 0, 0, 0, 175 0, 0, 0, 0, 0, 176 0, 0, 0, 1, 0 177 }; 178 179 private final PathInterpolator mTextSizePathInterpolator 180 = new PathInterpolator(0.16f, 0.4f, 0.2f, 1); 181 182 private final int[] mGradientColors = new int[] {0,0x88000000}; 183 private GradientDrawable mTitleGradientDrawable = new GradientDrawable( 184 GradientDrawable.Orientation.TOP_BOTTOM, mGradientColors); 185 private GradientDrawable mActionBarGradientDrawable = new GradientDrawable( 186 GradientDrawable.Orientation.BOTTOM_TOP, mGradientColors); 187 188 public interface MultiShrinkScrollerListener { onScrolledOffBottom()189 void onScrolledOffBottom(); 190 onStartScrollOffBottom()191 void onStartScrollOffBottom(); 192 onTransparentViewHeightChange(float ratio)193 void onTransparentViewHeightChange(float ratio); 194 onEntranceAnimationDone()195 void onEntranceAnimationDone(); 196 onEnterFullscreen()197 void onEnterFullscreen(); 198 onExitFullscreen()199 void onExitFullscreen(); 200 } 201 202 private final AnimatorListener mSnapToBottomListener = new AnimatorListenerAdapter() { 203 @Override 204 public void onAnimationEnd(Animator animation) { 205 if (getScrollUntilOffBottom() > 0 && mListener != null) { 206 // Due to a rounding error, after the animation finished we haven't fully scrolled 207 // off the screen. Lie to the listener: tell it that we did scroll off the screen. 208 mListener.onScrolledOffBottom(); 209 // No other messages need to be sent to the listener. 210 mListener = null; 211 } 212 } 213 }; 214 215 /** 216 * Interpolator from android.support.v4.view.ViewPager. Snappier and more elastic feeling 217 * than the default interpolator. 218 */ 219 private static final Interpolator sInterpolator = new Interpolator() { 220 221 /** 222 * {@inheritDoc} 223 */ 224 @Override 225 public float getInterpolation(float t) { 226 t -= 1.0f; 227 return t * t * t * t * t + 1.0f; 228 } 229 }; 230 MultiShrinkScroller(Context context)231 public MultiShrinkScroller(Context context) { 232 this(context, null); 233 } 234 MultiShrinkScroller(Context context, AttributeSet attrs)235 public MultiShrinkScroller(Context context, AttributeSet attrs) { 236 this(context, attrs, 0); 237 } 238 MultiShrinkScroller(Context context, AttributeSet attrs, int defStyleAttr)239 public MultiShrinkScroller(Context context, AttributeSet attrs, int defStyleAttr) { 240 super(context, attrs, defStyleAttr); 241 242 final ViewConfiguration configuration = ViewConfiguration.get(context); 243 setFocusable(false); 244 // Drawing must be enabled in order to support EdgeEffect 245 setWillNotDraw(/* willNotDraw = */ false); 246 247 mEdgeGlowBottom = new EdgeEffect(context); 248 mEdgeGlowTop = new EdgeEffect(context); 249 mScroller = new Scroller(context, sInterpolator); 250 mTouchSlop = configuration.getScaledTouchSlop(); 251 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 252 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 253 mTransparentStartHeight = (int) getResources().getDimension( 254 R.dimen.quickcontact_starting_empty_height); 255 mToolbarElevation = getResources().getDimension( 256 R.dimen.quick_contact_toolbar_elevation); 257 mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel); 258 mMaximumTitleMargin = (int) getResources().getDimension( 259 R.dimen.quickcontact_title_initial_margin); 260 261 mDismissDistanceOnScroll = (int) getResources().getDimension( 262 R.dimen.quickcontact_dismiss_distance_on_scroll); 263 mDismissDistanceOnRelease = (int) getResources().getDimension( 264 R.dimen.quickcontact_dismiss_distance_on_release); 265 mSnapToTopSlopHeight = (int) getResources().getDimension( 266 R.dimen.quickcontact_snap_to_top_slop_height); 267 268 final TypedValue photoRatio = new TypedValue(); 269 getResources().getValue(R.dimen.quickcontact_landscape_photo_ratio, photoRatio, 270 /* resolveRefs = */ true); 271 mLandscapePhotoRatio = photoRatio.getFloat(); 272 273 final TypedArray attributeArray = context.obtainStyledAttributes( 274 new int[]{android.R.attr.actionBarSize}); 275 mActionBarSize = attributeArray.getDimensionPixelSize(0, 0); 276 mMinimumHeaderHeight = mActionBarSize; 277 // This value is approximately equal to the portrait ActionBar size. It isn't exactly the 278 // same, since the landscape and portrait ActionBar sizes can be different. 279 mMinimumPortraitHeaderHeight = mMinimumHeaderHeight; 280 attributeArray.recycle(); 281 } 282 283 /** 284 * This method must be called inside the Activity's OnCreate. 285 */ initialize(MultiShrinkScrollerListener listener, boolean isOpenContactSquare)286 public void initialize(MultiShrinkScrollerListener listener, boolean isOpenContactSquare) { 287 mScrollView = (ScrollView) findViewById(R.id.content_scroller); 288 mScrollViewChild = findViewById(R.id.card_container); 289 mToolbar = findViewById(R.id.toolbar_parent); 290 mPhotoViewContainer = findViewById(R.id.toolbar_parent); 291 mTransparentView = findViewById(R.id.transparent_view); 292 mLargeTextView = (TextView) findViewById(R.id.large_title); 293 mInvisiblePlaceholderTextView = (TextView) findViewById(R.id.placeholder_textview); 294 mStartColumn = findViewById(R.id.empty_start_column); 295 // Touching the empty space should close the card 296 if (mStartColumn != null) { 297 mStartColumn.setOnClickListener(new OnClickListener() { 298 @Override 299 public void onClick(View v) { 300 scrollOffBottom(); 301 } 302 }); 303 findViewById(R.id.empty_end_column).setOnClickListener(new OnClickListener() { 304 @Override 305 public void onClick(View v) { 306 scrollOffBottom(); 307 } 308 }); 309 } 310 mListener = listener; 311 mIsOpenContactSquare = isOpenContactSquare; 312 313 mPhotoView = (QuickContactImageView) findViewById(R.id.photo); 314 315 mTitleGradientView = findViewById(R.id.title_gradient); 316 mTitleGradientView.setBackground(mTitleGradientDrawable); 317 mActionBarGradientView = findViewById(R.id.action_bar_gradient); 318 mActionBarGradientView.setBackground(mActionBarGradientDrawable); 319 mCollapsedTitleStartMargin = ((Toolbar) findViewById(R.id.toolbar)).getContentInsetStart(); 320 321 mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay); 322 if (!mIsTwoPanel) { 323 mPhotoTouchInterceptOverlay.setOnClickListener(new OnClickListener() { 324 @Override 325 public void onClick(View v) { 326 expandHeader(); 327 } 328 }); 329 } 330 331 SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ false, new Runnable() { 332 @Override 333 public void run() { 334 if (!mIsTwoPanel) { 335 // We never want the height of the photo view to exceed its width. 336 mMaximumHeaderHeight = mPhotoViewContainer.getWidth(); 337 mIntermediateHeaderHeight = (int) (mMaximumHeaderHeight 338 * INTERMEDIATE_HEADER_HEIGHT_RATIO); 339 } 340 mMaximumPortraitHeaderHeight = mIsTwoPanel ? getHeight() 341 : mPhotoViewContainer.getWidth(); 342 setHeaderHeight(getMaximumScrollableHeaderHeight()); 343 mMaximumHeaderTextSize = mLargeTextView.getHeight(); 344 if (mIsTwoPanel) { 345 mMaximumHeaderHeight = getHeight(); 346 mMinimumHeaderHeight = mMaximumHeaderHeight; 347 mIntermediateHeaderHeight = mMaximumHeaderHeight; 348 349 // Permanently set photo width and height. 350 final ViewGroup.LayoutParams photoLayoutParams 351 = mPhotoViewContainer.getLayoutParams(); 352 photoLayoutParams.height = mMaximumHeaderHeight; 353 photoLayoutParams.width = (int) (mMaximumHeaderHeight * mLandscapePhotoRatio); 354 mPhotoViewContainer.setLayoutParams(photoLayoutParams); 355 356 // Permanently set title width and margin. 357 final FrameLayout.LayoutParams largeTextLayoutParams 358 = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams(); 359 largeTextLayoutParams.width = photoLayoutParams.width - 360 largeTextLayoutParams.leftMargin - largeTextLayoutParams.rightMargin; 361 largeTextLayoutParams.gravity = Gravity.BOTTOM | Gravity.START; 362 mLargeTextView.setLayoutParams(largeTextLayoutParams); 363 } else { 364 // Set the width of mLargeTextView as if it was nested inside 365 // mPhotoViewContainer. 366 mLargeTextView.setWidth(mPhotoViewContainer.getWidth() 367 - 2 * mMaximumTitleMargin); 368 } 369 370 calculateCollapsedLargeTitlePadding(); 371 updateHeaderTextSizeAndMargin(); 372 configureGradientViewHeights(); 373 } 374 }); 375 } 376 configureGradientViewHeights()377 private void configureGradientViewHeights() { 378 final FrameLayout.LayoutParams actionBarGradientLayoutParams 379 = (FrameLayout.LayoutParams) mActionBarGradientView.getLayoutParams(); 380 actionBarGradientLayoutParams.height = mActionBarSize; 381 mActionBarGradientView.setLayoutParams(actionBarGradientLayoutParams); 382 final FrameLayout.LayoutParams titleGradientLayoutParams 383 = (FrameLayout.LayoutParams) mTitleGradientView.getLayoutParams(); 384 final float TITLE_GRADIENT_SIZE_COEFFICIENT = 1.25f; 385 final FrameLayout.LayoutParams largeTextLayoutParms 386 = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams(); 387 titleGradientLayoutParams.height = (int) ((mLargeTextView.getHeight() 388 + largeTextLayoutParms.bottomMargin) * TITLE_GRADIENT_SIZE_COEFFICIENT); 389 mTitleGradientView.setLayoutParams(titleGradientLayoutParams); 390 } 391 setTitle(String title)392 public void setTitle(String title) { 393 mLargeTextView.setText(title); 394 mPhotoTouchInterceptOverlay.setContentDescription(title); 395 } 396 397 @Override onInterceptTouchEvent(MotionEvent event)398 public boolean onInterceptTouchEvent(MotionEvent event) { 399 if (mVelocityTracker == null) { 400 mVelocityTracker = VelocityTracker.obtain(); 401 } 402 mVelocityTracker.addMovement(event); 403 404 // The only time we want to intercept touch events is when we are being dragged. 405 return shouldStartDrag(event); 406 } 407 shouldStartDrag(MotionEvent event)408 private boolean shouldStartDrag(MotionEvent event) { 409 if (mIsTouchDisabledForDismissAnimation) return false; 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 onTouchEvent(MotionEvent event)444 public boolean onTouchEvent(MotionEvent event) { 445 if (mIsTouchDisabledForDismissAnimation) 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 setHeaderTintColor(int color)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 */ expandHeader()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 startDrag()526 private void startDrag() { 527 mIsBeingDragged = true; 528 mScroller.abortAnimation(); 529 } 530 stopDrag(boolean cancelled)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 onDragFinished(int flingDelta)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 */ snapToTopOnDragFinished(int flingDelta)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 */ snapToBottomOnDragFinished()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 */ shouldDismissOnScroll()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 */ getStartingTransparentHeightRatio()622 public float getStartingTransparentHeightRatio() { 623 return getTransparentHeightRatio(mTransparentStartHeight); 624 } 625 getTransparentHeightRatio(int transparentHeight)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 scrollOffBottom()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 */ scrollUpForEntranceAnimation(boolean scrollToCurrentPosition)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 scrollTo(int x, int y)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 setToolbarHeight(int delta)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 getToolbarHeight()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 setHeaderHeight(int height)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 getHeaderHeight()737 public int getHeaderHeight() { 738 return mToolbar.getLayoutParams().height; 739 } 740 741 @NeededForReflection setScroll(int scroll)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 getScroll()751 public int getScroll() { 752 return mTransparentStartHeight - getTransparentViewHeight() 753 + getMaximumScrollableHeaderHeight() - getToolbarHeight() 754 + mScrollView.getScrollY(); 755 } 756 getMaximumScrollableHeaderHeight()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 */ getScroll_ignoreOversizedHeaderForSnapping()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 */ getScrollNeededToBeFullScreen()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 */ getScrollUntilOffBottom()789 private int getScrollUntilOffBottom() { 790 return getHeight() + getScroll_ignoreOversizedHeaderForSnapping() 791 - mTransparentStartHeight; 792 } 793 794 @Override computeScroll()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 draw(Canvas canvas)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 (isLayoutRtl()) { 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 (!isLayoutRtl()) { 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 getCurrentVelocity()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 fling(float velocity)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 getMaximumScrollUpwards()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 getTransparentViewHeight()908 private int getTransparentViewHeight() { 909 return mTransparentView.getLayoutParams().height; 910 } 911 setTransparentViewHeight(int height)912 private void setTransparentViewHeight(int height) { 913 mTransparentView.getLayoutParams().height = height; 914 mTransparentView.setLayoutParams(mTransparentView.getLayoutParams()); 915 } 916 scrollUp(int delta)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 */ getFullyCompressedHeaderHeight()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 */ getOverflowingChildViewSize()949 private int getOverflowingChildViewSize() { 950 final int usedScrollViewSpace = mScrollViewChild.getHeight(); 951 return -getHeight() + usedScrollViewSpace + mToolbar.getLayoutParams().height; 952 } 953 scrollDown(int delta)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 */ updateHeaderTextSizeAndMargin()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 (isLayoutRtl()) { 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 */ calculateCollapsedLargeTitlePadding()1034 private void calculateCollapsedLargeTitlePadding() { 1035 final Rect largeTextViewRect = new Rect(); 1036 mToolbar.getBoundsOnScreen(largeTextViewRect); 1037 final Rect invisiblePlaceholderTextViewRect = new Rect(); 1038 mInvisiblePlaceholderTextView.getBoundsOnScreen(invisiblePlaceholderTextViewRect); 1039 // Distance between top of toolbar to the center of the target rectangle. 1040 final int desiredTopToCenter = ( 1041 invisiblePlaceholderTextViewRect.top + invisiblePlaceholderTextViewRect.bottom) 1042 / 2 - largeTextViewRect.top; 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 */ setInterpolatedTitleMargins(float x)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 updatePhotoTintAndDropShadow()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 calculateHeightRatioToFullyOpen(int height)1155 private float calculateHeightRatioToFullyOpen(int height) { 1156 return (height - mMinimumPortraitHeaderHeight) 1157 / (float) (mMaximumPortraitHeaderHeight - mMinimumPortraitHeaderHeight); 1158 } 1159 calculateHeightRatioToBlendingStartHeight(int height)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 */ alphaMatrix(float alpha, int color)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 */ multiplyBlendMatrix(int color, float alpha)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 multiplyBlend(int color, float alpha)1197 private float multiplyBlend(int color, float alpha) { 1198 return color * alpha / 255.0f + (1 - alpha); 1199 } 1200 updateLastEventPosition(MotionEvent event)1201 private void updateLastEventPosition(MotionEvent event) { 1202 mLastEventPosition[0] = event.getX(); 1203 mLastEventPosition[1] = event.getY(); 1204 } 1205 motionShouldStartDrag(MotionEvent event)1206 private boolean motionShouldStartDrag(MotionEvent event) { 1207 final float deltaY = event.getY() - mLastEventPosition[1]; 1208 return deltaY > mTouchSlop || deltaY < -mTouchSlop; 1209 } 1210 updatePositionAndComputeDelta(MotionEvent event)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 smoothScrollBy(int delta)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 static 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 AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond, int pixelsDelta)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 getInterpolation(float input)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 getRefreshRate()1273 private float getRefreshRate() { 1274 DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo( 1275 Display.DEFAULT_DISPLAY); 1276 return di.refreshRate; 1277 } 1278 getFrameIntervalMs()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 */ prepareForShrinkingScrollChild(int heightDelta)1288 public void prepareForShrinkingScrollChild(int heightDelta) { 1289 // The Transition framework may suppress layout on the scene root and its children. If 1290 // mScrollView has its layout suppressed, user scrolling interactions will not display 1291 // correctly. By turning suppress off for mScrollView, mScrollView properly adjusts its 1292 // graphics as the user scrolls during the transition. 1293 mScrollView.suppressLayout(false); 1294 1295 final int newEmptyScrollViewSpace = -getOverflowingChildViewSize() + heightDelta; 1296 if (newEmptyScrollViewSpace > 0 && !mIsTwoPanel) { 1297 final int newDesiredToolbarHeight = Math.min(getToolbarHeight() 1298 + newEmptyScrollViewSpace, getMaximumScrollableHeaderHeight()); 1299 ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).setDuration( 1300 ExpandingEntryCardView.DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS).start(); 1301 } 1302 } 1303 prepareForExpandingScrollChild()1304 public void prepareForExpandingScrollChild() { 1305 // The Transition framework may suppress layout on the scene root and its children. If 1306 // mScrollView has its layout suppressed, user scrolling interactions will not display 1307 // correctly. By turning suppress off for mScrollView, mScrollView properly adjusts its 1308 // graphics as the user scrolls during the transition. 1309 mScrollView.suppressLayout(false); 1310 } 1311 } 1312