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