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