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