Home | History | Annotate | Download | only in widget
      1 package com.android.contacts.widget;
      2 
      3 import com.android.contacts.R;
      4 import com.android.contacts.quickcontact.ExpandingEntryCardView;
      5 import com.android.contacts.test.NeededForReflection;
      6 import com.android.contacts.util.SchedulingUtils;
      7 
      8 import android.animation.Animator;
      9 import android.animation.Animator.AnimatorListener;
     10 import android.animation.AnimatorListenerAdapter;
     11 import android.animation.ObjectAnimator;
     12 import android.animation.ValueAnimator;
     13 import android.animation.ValueAnimator.AnimatorUpdateListener;
     14 import android.content.Context;
     15 import android.content.res.Configuration;
     16 import android.content.res.TypedArray;
     17 import android.graphics.Canvas;
     18 import android.graphics.Color;
     19 import android.graphics.ColorMatrix;
     20 import android.graphics.ColorMatrixColorFilter;
     21 import android.graphics.Rect;
     22 import android.graphics.drawable.GradientDrawable;
     23 import android.hardware.display.DisplayManagerGlobal;
     24 import android.os.Trace;
     25 import android.util.AttributeSet;
     26 import android.util.TypedValue;
     27 import android.view.Display;
     28 import android.view.DisplayInfo;
     29 import android.view.Gravity;
     30 import android.view.MotionEvent;
     31 import android.view.VelocityTracker;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.view.ViewConfiguration;
     35 import android.view.animation.AnimationUtils;
     36 import android.view.animation.Interpolator;
     37 import android.view.animation.PathInterpolator;
     38 import android.widget.EdgeEffect;
     39 import android.widget.FrameLayout;
     40 import android.widget.LinearLayout;
     41 import android.widget.Scroller;
     42 import android.widget.ScrollView;
     43 import android.widget.TextView;
     44 
     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  */
     65 public 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