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