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