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