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