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