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