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