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