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.Rect;
21import android.graphics.drawable.GradientDrawable;
22import android.hardware.display.DisplayManagerGlobal;
23import android.os.Trace;
24import android.util.AttributeSet;
25import android.util.TypedValue;
26import android.view.Display;
27import android.view.DisplayInfo;
28import android.view.Gravity;
29import android.view.MotionEvent;
30import android.view.VelocityTracker;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.ViewConfiguration;
34import android.view.animation.AnimationUtils;
35import android.view.animation.Interpolator;
36import android.view.animation.PathInterpolator;
37import android.widget.EdgeEffect;
38import android.widget.FrameLayout;
39import android.widget.LinearLayout;
40import android.widget.Scroller;
41import android.widget.ScrollView;
42import android.widget.TextView;
43import android.widget.Toolbar;
44
45/**
46 * A custom {@link ViewGroup} that operates similarly to a {@link ScrollView}, except with multiple
47 * subviews. These subviews are scrolled or shrinked one at a time, until each reaches their
48 * minimum or maximum value.
49 *
50 * MultiShrinkScroller is designed for a specific problem. As such, this class is designed to be
51 * used with a specific layout file: quickcontact_activity.xml. MultiShrinkScroller expects subviews
52 * with specific ID values.
53 *
54 * MultiShrinkScroller's code is heavily influenced by ScrollView. Nonetheless, several ScrollView
55 * features are missing. For example: handling of KEYCODES, OverScroll bounce and saving
56 * scroll state in savedInstanceState bundles.
57 *
58 * Before copying this approach to nested scrolling, consider whether something simpler & less
59 * customized will work for you. For example, see the re-usable StickyHeaderListView used by
60 * WifiSetupActivity (very nice). Alternatively, check out Google+'s cover photo scrolling or
61 * Android L's built in nested scrolling support. I thought I needed a more custom ViewGroup in
62 * order to track velocity, modify EdgeEffect color & perform the originally specified animations.
63 * As a result this ViewGroup has non-standard talkback and keyboard support.
64 */
65public class MultiShrinkScroller extends FrameLayout {
66
67    /**
68     * 1000 pixels per millisecond. Ie, 1 pixel per second.
69     */
70    private static final int PIXELS_PER_SECOND = 1000;
71
72    /**
73     * Length of the acceleration animations. This value was taken from ValueAnimator.java.
74     */
75    private static final int EXIT_FLING_ANIMATION_DURATION_MS = 250;
76
77    /**
78     * In portrait mode, the height:width ratio of the photo's starting height.
79     */
80    private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.6f;
81
82    /**
83     * Color blending will only be performed on the contact photo once the toolbar is compressed
84     * to this ratio of its full height.
85     */
86    private static final float COLOR_BLENDING_START_RATIO = 0.5f;
87
88    private static final float SPRING_DAMPENING_FACTOR = 0.01f;
89
90    /**
91     * When displaying a letter tile drawable, this alpha value should be used at the intermediate
92     * toolbar height.
93     */
94    private static final float DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA = 0.8f;
95
96    private float[] mLastEventPosition = { 0, 0 };
97    private VelocityTracker mVelocityTracker;
98    private boolean mIsBeingDragged = false;
99    private boolean mReceivedDown = false;
100    /**
101     * Did the current downwards fling/scroll-animation start while we were fullscreen?
102     */
103    private boolean mIsFullscreenDownwardsFling = false;
104
105    private ScrollView mScrollView;
106    private View mScrollViewChild;
107    private View mToolbar;
108    private QuickContactImageView mPhotoView;
109    private View mPhotoViewContainer;
110    private View mTransparentView;
111    private MultiShrinkScrollerListener mListener;
112    private TextView mLargeTextView;
113    private View mPhotoTouchInterceptOverlay;
114    /** Contains desired size & vertical offset of the title, once the header is fully compressed */
115    private TextView mInvisiblePlaceholderTextView;
116    private View mTitleGradientView;
117    private View mActionBarGradientView;
118    private View mStartColumn;
119    private int mHeaderTintColor;
120    private int mMaximumHeaderHeight;
121    private int mMinimumHeaderHeight;
122    /**
123     * When the contact photo is tapped, it is resized to max size or this size. This value also
124     * sometimes represents the maximum achievable header size achieved by scrolling. To enforce
125     * this maximum in scrolling logic, always access this value via
126     * {@link #getMaximumScrollableHeaderHeight}.
127     */
128    private int mIntermediateHeaderHeight;
129    /**
130     * If true, regular scrolling can expand the header beyond mIntermediateHeaderHeight. The
131     * header, that contains the contact photo, can expand to a height equal its width.
132     */
133    private boolean mIsOpenContactSquare;
134    private int mMaximumHeaderTextSize;
135    private int mCollapsedTitleBottomMargin;
136    private int mCollapsedTitleStartMargin;
137    private int mMinimumPortraitHeaderHeight;
138    private int mMaximumPortraitHeaderHeight;
139    /**
140     * True once the header has touched the top of the screen at least once.
141     */
142    private boolean mHasEverTouchedTheTop;
143    private boolean mIsTouchDisabledForDismissAnimation;
144
145    private final Scroller mScroller;
146    private final EdgeEffect mEdgeGlowBottom;
147    private final EdgeEffect mEdgeGlowTop;
148    private final int mTouchSlop;
149    private final int mMaximumVelocity;
150    private final int mMinimumVelocity;
151    private final int mDismissDistanceOnScroll;
152    private final int mDismissDistanceOnRelease;
153    private final int mSnapToTopSlopHeight;
154    private final int mTransparentStartHeight;
155    private final int mMaximumTitleMargin;
156    private final float mToolbarElevation;
157    private final boolean mIsTwoPanel;
158    private final float mLandscapePhotoRatio;
159    private final int mActionBarSize;
160
161    // Objects used to perform color filtering on the header. These are stored as fields for
162    // the sole purpose of avoiding "new" operations inside animation loops.
163    private final ColorMatrix mWhitenessColorMatrix = new ColorMatrix();
164    private final ColorMatrix mColorMatrix = new ColorMatrix();
165    private final float[] mAlphaMatrixValues = {
166            0, 0, 0, 0, 0,
167            0, 0, 0, 0, 0,
168            0, 0, 0, 0, 0,
169            0, 0, 0, 1, 0
170    };
171    private final ColorMatrix mMultiplyBlendMatrix = new ColorMatrix();
172    private final float[] mMultiplyBlendMatrixValues = {
173            0, 0, 0, 0, 0,
174            0, 0, 0, 0, 0,
175            0, 0, 0, 0, 0,
176            0, 0, 0, 1, 0
177    };
178
179    private final PathInterpolator mTextSizePathInterpolator
180            = new PathInterpolator(0.16f, 0.4f, 0.2f, 1);
181
182    private final int[] mGradientColors = new int[] {0,0x88000000};
183    private GradientDrawable mTitleGradientDrawable = new GradientDrawable(
184            GradientDrawable.Orientation.TOP_BOTTOM, mGradientColors);
185    private GradientDrawable mActionBarGradientDrawable = new GradientDrawable(
186            GradientDrawable.Orientation.BOTTOM_TOP, mGradientColors);
187
188    public interface MultiShrinkScrollerListener {
189        void onScrolledOffBottom();
190
191        void onStartScrollOffBottom();
192
193        void onTransparentViewHeightChange(float ratio);
194
195        void onEntranceAnimationDone();
196
197        void onEnterFullscreen();
198
199        void onExitFullscreen();
200    }
201
202    private final AnimatorListener mSnapToBottomListener = new AnimatorListenerAdapter() {
203        @Override
204        public void onAnimationEnd(Animator animation) {
205            if (getScrollUntilOffBottom() > 0 && mListener != null) {
206                // Due to a rounding error, after the animation finished we haven't fully scrolled
207                // off the screen. Lie to the listener: tell it that we did scroll off the screen.
208                mListener.onScrolledOffBottom();
209                // No other messages need to be sent to the listener.
210                mListener = null;
211            }
212        }
213    };
214
215    /**
216     * Interpolator from android.support.v4.view.ViewPager. Snappier and more elastic feeling
217     * than the default interpolator.
218     */
219    private static final Interpolator sInterpolator = new Interpolator() {
220
221        /**
222         * {@inheritDoc}
223         */
224        @Override
225        public float getInterpolation(float t) {
226            t -= 1.0f;
227            return t * t * t * t * t + 1.0f;
228        }
229    };
230
231    public MultiShrinkScroller(Context context) {
232        this(context, null);
233    }
234
235    public MultiShrinkScroller(Context context, AttributeSet attrs) {
236        this(context, attrs, 0);
237    }
238
239    public MultiShrinkScroller(Context context, AttributeSet attrs, int defStyleAttr) {
240        super(context, attrs, defStyleAttr);
241
242        final ViewConfiguration configuration = ViewConfiguration.get(context);
243        setFocusable(false);
244        // Drawing must be enabled in order to support EdgeEffect
245        setWillNotDraw(/* willNotDraw = */ false);
246
247        mEdgeGlowBottom = new EdgeEffect(context);
248        mEdgeGlowTop = new EdgeEffect(context);
249        mScroller = new Scroller(context, sInterpolator);
250        mTouchSlop = configuration.getScaledTouchSlop();
251        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
252        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
253        mTransparentStartHeight = (int) getResources().getDimension(
254                R.dimen.quickcontact_starting_empty_height);
255        mToolbarElevation = getResources().getDimension(
256                R.dimen.quick_contact_toolbar_elevation);
257        mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel);
258        mMaximumTitleMargin = (int) getResources().getDimension(
259                R.dimen.quickcontact_title_initial_margin);
260
261        mDismissDistanceOnScroll = (int) getResources().getDimension(
262                R.dimen.quickcontact_dismiss_distance_on_scroll);
263        mDismissDistanceOnRelease = (int) getResources().getDimension(
264                R.dimen.quickcontact_dismiss_distance_on_release);
265        mSnapToTopSlopHeight = (int) getResources().getDimension(
266                R.dimen.quickcontact_snap_to_top_slop_height);
267
268        final TypedValue photoRatio = new TypedValue();
269        getResources().getValue(R.dimen.quickcontact_landscape_photo_ratio, photoRatio,
270                            /* resolveRefs = */ true);
271        mLandscapePhotoRatio = photoRatio.getFloat();
272
273        final TypedArray attributeArray = context.obtainStyledAttributes(
274                new int[]{android.R.attr.actionBarSize});
275        mActionBarSize = attributeArray.getDimensionPixelSize(0, 0);
276        mMinimumHeaderHeight = mActionBarSize;
277        // This value is approximately equal to the portrait ActionBar size. It isn't exactly the
278        // same, since the landscape and portrait ActionBar sizes can be different.
279        mMinimumPortraitHeaderHeight = mMinimumHeaderHeight;
280        attributeArray.recycle();
281    }
282
283    /**
284     * This method must be called inside the Activity's OnCreate.
285     */
286    public void initialize(MultiShrinkScrollerListener listener, boolean isOpenContactSquare) {
287        mScrollView = (ScrollView) findViewById(R.id.content_scroller);
288        mScrollViewChild = findViewById(R.id.card_container);
289        mToolbar = findViewById(R.id.toolbar_parent);
290        mPhotoViewContainer = findViewById(R.id.toolbar_parent);
291        mTransparentView = findViewById(R.id.transparent_view);
292        mLargeTextView = (TextView) findViewById(R.id.large_title);
293        mInvisiblePlaceholderTextView = (TextView) findViewById(R.id.placeholder_textview);
294        mStartColumn = findViewById(R.id.empty_start_column);
295        // Touching the empty space should close the card
296        if (mStartColumn != null) {
297            mStartColumn.setOnClickListener(new OnClickListener() {
298                @Override
299                public void onClick(View v) {
300                    scrollOffBottom();
301                }
302            });
303            findViewById(R.id.empty_end_column).setOnClickListener(new OnClickListener() {
304                @Override
305                public void onClick(View v) {
306                    scrollOffBottom();
307                }
308            });
309        }
310        mListener = listener;
311        mIsOpenContactSquare = isOpenContactSquare;
312
313        mPhotoView = (QuickContactImageView) findViewById(R.id.photo);
314
315        mTitleGradientView = findViewById(R.id.title_gradient);
316        mTitleGradientView.setBackground(mTitleGradientDrawable);
317        mActionBarGradientView = findViewById(R.id.action_bar_gradient);
318        mActionBarGradientView.setBackground(mActionBarGradientDrawable);
319        mCollapsedTitleStartMargin = ((Toolbar) findViewById(R.id.toolbar)).getContentInsetStart();
320
321        mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay);
322        if (!mIsTwoPanel) {
323            mPhotoTouchInterceptOverlay.setOnClickListener(new OnClickListener() {
324                @Override
325                public void onClick(View v) {
326                    expandHeader();
327                }
328            });
329        }
330
331        SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ false, new Runnable() {
332            @Override
333            public void run() {
334                if (!mIsTwoPanel) {
335                    // We never want the height of the photo view to exceed its width.
336                    mMaximumHeaderHeight = mPhotoViewContainer.getWidth();
337                    mIntermediateHeaderHeight = (int) (mMaximumHeaderHeight
338                            * INTERMEDIATE_HEADER_HEIGHT_RATIO);
339                }
340                mMaximumPortraitHeaderHeight = mIsTwoPanel ? getHeight()
341                        : mPhotoViewContainer.getWidth();
342                setHeaderHeight(getMaximumScrollableHeaderHeight());
343                mMaximumHeaderTextSize = mLargeTextView.getHeight();
344                if (mIsTwoPanel) {
345                    mMaximumHeaderHeight = getHeight();
346                    mMinimumHeaderHeight = mMaximumHeaderHeight;
347                    mIntermediateHeaderHeight = mMaximumHeaderHeight;
348
349                    // Permanently set photo width and height.
350                    final ViewGroup.LayoutParams photoLayoutParams
351                            = mPhotoViewContainer.getLayoutParams();
352                    photoLayoutParams.height = mMaximumHeaderHeight;
353                    photoLayoutParams.width = (int) (mMaximumHeaderHeight * mLandscapePhotoRatio);
354                    mPhotoViewContainer.setLayoutParams(photoLayoutParams);
355
356                    // Permanently set title width and margin.
357                    final FrameLayout.LayoutParams largeTextLayoutParams
358                            = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams();
359                    largeTextLayoutParams.width = photoLayoutParams.width -
360                            largeTextLayoutParams.leftMargin - largeTextLayoutParams.rightMargin;
361                    largeTextLayoutParams.gravity = Gravity.BOTTOM | Gravity.START;
362                    mLargeTextView.setLayoutParams(largeTextLayoutParams);
363                } else {
364                    // Set the width of mLargeTextView as if it was nested inside
365                    // mPhotoViewContainer.
366                    mLargeTextView.setWidth(mPhotoViewContainer.getWidth()
367                            - 2 * mMaximumTitleMargin);
368                }
369
370                calculateCollapsedLargeTitlePadding();
371                updateHeaderTextSizeAndMargin();
372                configureGradientViewHeights();
373            }
374        });
375    }
376
377    private void configureGradientViewHeights() {
378        final FrameLayout.LayoutParams actionBarGradientLayoutParams
379                = (FrameLayout.LayoutParams) mActionBarGradientView.getLayoutParams();
380        actionBarGradientLayoutParams.height = mActionBarSize;
381        mActionBarGradientView.setLayoutParams(actionBarGradientLayoutParams);
382        final FrameLayout.LayoutParams titleGradientLayoutParams
383                = (FrameLayout.LayoutParams) mTitleGradientView.getLayoutParams();
384        final float TITLE_GRADIENT_SIZE_COEFFICIENT = 1.25f;
385        final FrameLayout.LayoutParams largeTextLayoutParms
386                = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams();
387        titleGradientLayoutParams.height = (int) ((mLargeTextView.getHeight()
388                + largeTextLayoutParms.bottomMargin) * TITLE_GRADIENT_SIZE_COEFFICIENT);
389        mTitleGradientView.setLayoutParams(titleGradientLayoutParams);
390    }
391
392    public void setTitle(String title) {
393        mLargeTextView.setText(title);
394        mPhotoTouchInterceptOverlay.setContentDescription(title);
395    }
396
397    @Override
398    public boolean onInterceptTouchEvent(MotionEvent event) {
399        if (mVelocityTracker == null) {
400            mVelocityTracker = VelocityTracker.obtain();
401        }
402        mVelocityTracker.addMovement(event);
403
404        // The only time we want to intercept touch events is when we are being dragged.
405        return shouldStartDrag(event);
406    }
407
408    private boolean shouldStartDrag(MotionEvent event) {
409        if (mIsTouchDisabledForDismissAnimation) return false;
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) 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 (isLayoutRtl()) {
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 (!isLayoutRtl()) {
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 (isLayoutRtl()) {
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        final Rect largeTextViewRect = new Rect();
1036        mToolbar.getBoundsOnScreen(largeTextViewRect);
1037        final Rect invisiblePlaceholderTextViewRect = new Rect();
1038        mInvisiblePlaceholderTextView.getBoundsOnScreen(invisiblePlaceholderTextViewRect);
1039        // Distance between top of toolbar to the center of the target rectangle.
1040        final int desiredTopToCenter = (
1041                invisiblePlaceholderTextViewRect.top + invisiblePlaceholderTextViewRect.bottom)
1042                / 2 - largeTextViewRect.top;
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 static 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            DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
1275                    Display.DEFAULT_DISPLAY);
1276            return di.refreshRate;
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        // The Transition framework may suppress layout on the scene root and its children. If
1290        // mScrollView has its layout suppressed, user scrolling interactions will not display
1291        // correctly. By turning suppress off for mScrollView, mScrollView properly adjusts its
1292        // graphics as the user scrolls during the transition.
1293        mScrollView.suppressLayout(false);
1294
1295        final int newEmptyScrollViewSpace = -getOverflowingChildViewSize() + heightDelta;
1296        if (newEmptyScrollViewSpace > 0 && !mIsTwoPanel) {
1297            final int newDesiredToolbarHeight = Math.min(getToolbarHeight()
1298                    + newEmptyScrollViewSpace, getMaximumScrollableHeaderHeight());
1299            ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).setDuration(
1300                    ExpandingEntryCardView.DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS).start();
1301        }
1302    }
1303
1304    public void prepareForExpandingScrollChild() {
1305        // The Transition framework may suppress layout on the scene root and its children. If
1306        // mScrollView has its layout suppressed, user scrolling interactions will not display
1307        // correctly. By turning suppress off for mScrollView, mScrollView properly adjusts its
1308        // graphics as the user scrolls during the transition.
1309        mScrollView.suppressLayout(false);
1310    }
1311}
1312