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