Home | History | Annotate | Download | only in widget
      1 package com.android.contacts.widget;
      2 
      3 import android.animation.Animator;
      4 import android.animation.Animator.AnimatorListener;
      5 import android.animation.AnimatorListenerAdapter;
      6 import android.animation.ObjectAnimator;
      7 import android.animation.ValueAnimator;
      8 import android.animation.ValueAnimator.AnimatorUpdateListener;
      9 import android.content.Context;
     10 import android.content.res.TypedArray;
     11 import android.graphics.Canvas;
     12 import android.graphics.Color;
     13 import android.graphics.ColorMatrix;
     14 import android.graphics.ColorMatrixColorFilter;
     15 import android.graphics.drawable.GradientDrawable;
     16 import android.hardware.display.DisplayManager;
     17 import android.os.Trace;
     18 import android.support.v4.view.ViewCompat;
     19 import android.support.v4.view.animation.PathInterpolatorCompat;
     20 import android.util.AttributeSet;
     21 import android.util.TypedValue;
     22 import android.view.Display;
     23 import android.view.Gravity;
     24 import android.view.MotionEvent;
     25 import android.view.VelocityTracker;
     26 import android.view.View;
     27 import android.view.ViewConfiguration;
     28 import android.view.ViewGroup;
     29 import android.view.animation.AnimationUtils;
     30 import android.view.animation.Interpolator;
     31 import android.widget.EdgeEffect;
     32 import android.widget.FrameLayout;
     33 import android.widget.LinearLayout;
     34 import android.widget.ScrollView;
     35 import android.widget.Scroller;
     36 import android.widget.TextView;
     37 import android.widget.Toolbar;
     38 
     39 import com.android.contacts.R;
     40 import com.android.contacts.compat.CompatUtils;
     41 import com.android.contacts.compat.EdgeEffectCompat;
     42 import com.android.contacts.quickcontact.ExpandingEntryCardView;
     43 import com.android.contacts.test.NeededForReflection;
     44 import com.android.contacts.util.SchedulingUtils;
     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 mFullNameView;
    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         mFullNameView = (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 = mFullNameView.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 mFullNameView as if it was nested inside
    388                     // mPhotoViewContainer.
    389                     mFullNameView.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         mFullNameView.setText(title);
    419         // We have a phone number as "mFullNameView" so make it always LTR.
    420         if (isPhoneNumber) {
    421             mFullNameView.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         final int maximumHeaderTextSize =
    437                 mMaximumFullNameViewHeight * mFullNameView.getLineCount()
    438                 + mMaximumPhoneticNameViewHeight * mPhoneticNameView.getLineCount();
    439         // TODO try not using initialize() to refresh phonetic name view: b/27410518
    440         initialize(mListener, mIsOpenContactSquare, maximumHeaderTextSize,
    441                 /* shouldUpdateNameViewHeight */ false);
    442     }
    443 
    444     public void setPhoneticNameGone() {
    445         // Remove phonetic name only when it was visible before.
    446         if (mPhoneticNameView.getVisibility() == View.GONE) {
    447             return;
    448         }
    449         mPhoneticNameView.setVisibility(View.GONE);
    450         final int maximumHeaderTextSize =
    451                 mMaximumFullNameViewHeight * mFullNameView.getLineCount();
    452         // TODO try not using initialize() to refresh phonetic name view: b/27410518
    453         initialize(mListener, mIsOpenContactSquare, maximumHeaderTextSize,
    454                 /* shouldUpdateNameViewHeight */ false);
    455     }
    456 
    457     @Override
    458     public boolean onInterceptTouchEvent(MotionEvent event) {
    459         if (mVelocityTracker == null) {
    460             mVelocityTracker = VelocityTracker.obtain();
    461         }
    462         mVelocityTracker.addMovement(event);
    463 
    464         // The only time we want to intercept touch events is when we are being dragged.
    465         return shouldStartDrag(event);
    466     }
    467 
    468     private boolean shouldStartDrag(MotionEvent event) {
    469         if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return false;
    470 
    471 
    472         if (mIsBeingDragged) {
    473             mIsBeingDragged = false;
    474             return false;
    475         }
    476 
    477         switch (event.getAction()) {
    478             // If we are in the middle of a fling and there is a down event, we'll steal it and
    479             // start a drag.
    480             case MotionEvent.ACTION_DOWN:
    481                 updateLastEventPosition(event);
    482                 if (!mScroller.isFinished()) {
    483                     startDrag();
    484                     return true;
    485                 } else {
    486                     mReceivedDown = true;
    487                 }
    488                 break;
    489 
    490             // Otherwise, we will start a drag if there is enough motion in the direction we are
    491             // capable of scrolling.
    492             case MotionEvent.ACTION_MOVE:
    493                 if (motionShouldStartDrag(event)) {
    494                     updateLastEventPosition(event);
    495                     startDrag();
    496                     return true;
    497                 }
    498                 break;
    499         }
    500 
    501         return false;
    502     }
    503 
    504     @Override
    505     public boolean onTouchEvent(MotionEvent event) {
    506         if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return true;
    507 
    508         final int action = event.getAction();
    509 
    510         if (mVelocityTracker == null) {
    511             mVelocityTracker = VelocityTracker.obtain();
    512         }
    513         mVelocityTracker.addMovement(event);
    514 
    515         if (!mIsBeingDragged) {
    516             if (shouldStartDrag(event)) {
    517                 return true;
    518             }
    519 
    520             if (action == MotionEvent.ACTION_UP && mReceivedDown) {
    521                 mReceivedDown = false;
    522                 return performClick();
    523             }
    524             return true;
    525         }
    526 
    527         switch (action) {
    528             case MotionEvent.ACTION_MOVE:
    529                 final float delta = updatePositionAndComputeDelta(event);
    530                 scrollTo(0, getScroll() + (int) delta);
    531                 mReceivedDown = false;
    532 
    533                 if (mIsBeingDragged) {
    534                     final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
    535                     if (delta > distanceFromMaxScrolling) {
    536                         // The ScrollView is being pulled upwards while there is no more
    537                         // content offscreen, and the view port is already fully expanded.
    538                         EdgeEffectCompat.onPull(mEdgeGlowBottom, delta / getHeight(),
    539                                 1 - event.getX() / getWidth());
    540                     }
    541 
    542                     if (!mEdgeGlowBottom.isFinished()) {
    543                         postInvalidateOnAnimation();
    544                     }
    545 
    546                     if (shouldDismissOnScroll()) {
    547                         scrollOffBottom();
    548                     }
    549 
    550                 }
    551                 break;
    552 
    553             case MotionEvent.ACTION_UP:
    554             case MotionEvent.ACTION_CANCEL:
    555                 stopDrag(action == MotionEvent.ACTION_CANCEL);
    556                 mReceivedDown = false;
    557                 break;
    558         }
    559 
    560         return true;
    561     }
    562 
    563     public void setHeaderTintColor(int color) {
    564         mHeaderTintColor = color;
    565         updatePhotoTintAndDropShadow();
    566         if (CompatUtils.isLollipopCompatible()) {
    567             // Use the same amount of alpha on the new tint color as the previous tint color.
    568             final int edgeEffectAlpha = Color.alpha(mEdgeGlowBottom.getColor());
    569             mEdgeGlowBottom.setColor((color & 0xffffff) | Color.argb(edgeEffectAlpha, 0, 0, 0));
    570             mEdgeGlowTop.setColor(mEdgeGlowBottom.getColor());
    571         }
    572     }
    573 
    574     /**
    575      * Expand to maximum size.
    576      */
    577     private void expandHeader() {
    578         if (getHeaderHeight() != mMaximumHeaderHeight) {
    579             final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
    580                     mMaximumHeaderHeight);
    581             animator.setDuration(ExpandingEntryCardView.DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
    582             animator.start();
    583             // Scroll nested scroll view to its top
    584             if (mScrollView.getScrollY() != 0) {
    585                 ObjectAnimator.ofInt(mScrollView, "scrollY", -mScrollView.getScrollY()).start();
    586             }
    587         }
    588     }
    589 
    590     private void startDrag() {
    591         mIsBeingDragged = true;
    592         mScroller.abortAnimation();
    593     }
    594 
    595     private void stopDrag(boolean cancelled) {
    596         mIsBeingDragged = false;
    597         if (!cancelled && getChildCount() > 0) {
    598             final float velocity = getCurrentVelocity();
    599             if (velocity > mMinimumVelocity || velocity < -mMinimumVelocity) {
    600                 fling(-velocity);
    601                 onDragFinished(mScroller.getFinalY() - mScroller.getStartY());
    602             } else {
    603                 onDragFinished(/* flingDelta = */ 0);
    604             }
    605         } else {
    606             onDragFinished(/* flingDelta = */ 0);
    607         }
    608 
    609         if (mVelocityTracker != null) {
    610             mVelocityTracker.recycle();
    611             mVelocityTracker = null;
    612         }
    613 
    614         mEdgeGlowBottom.onRelease();
    615     }
    616 
    617     private void onDragFinished(int flingDelta) {
    618         if (getTransparentViewHeight() <= 0) {
    619             // Don't perform any snapping if quick contacts is full screen.
    620             return;
    621         }
    622         if (!snapToTopOnDragFinished(flingDelta)) {
    623             // The drag/fling won't result in the content at the top of the Window. Consider
    624             // snapping the content to the bottom of the window.
    625             snapToBottomOnDragFinished();
    626         }
    627     }
    628 
    629     /**
    630      * If needed, snap the subviews to the top of the Window.
    631      *
    632      * @return TRUE if QuickContacts will snap/fling to to top after this method call.
    633      */
    634     private boolean snapToTopOnDragFinished(int flingDelta) {
    635         if (!mHasEverTouchedTheTop) {
    636             // If the current fling is predicted to scroll past the top, then we don't need to snap
    637             // to the top. However, if the fling only flings past the top by a tiny amount,
    638             // it will look nicer to snap than to fling.
    639             final float predictedScrollPastTop = getTransparentViewHeight() - flingDelta;
    640             if (predictedScrollPastTop < -mSnapToTopSlopHeight) {
    641                 return false;
    642             }
    643 
    644             if (getTransparentViewHeight() <= mTransparentStartHeight) {
    645                 // We are above the starting scroll position so snap to the top.
    646                 mScroller.forceFinished(true);
    647                 smoothScrollBy(getTransparentViewHeight());
    648                 return true;
    649             }
    650             return false;
    651         }
    652         if (getTransparentViewHeight() < mDismissDistanceOnRelease) {
    653             mScroller.forceFinished(true);
    654             smoothScrollBy(getTransparentViewHeight());
    655             return true;
    656         }
    657         return false;
    658     }
    659 
    660     /**
    661      * If needed, scroll all the subviews off the bottom of the Window.
    662      */
    663     private void snapToBottomOnDragFinished() {
    664         if (mHasEverTouchedTheTop) {
    665             if (getTransparentViewHeight() > mDismissDistanceOnRelease) {
    666                 scrollOffBottom();
    667             }
    668             return;
    669         }
    670         if (getTransparentViewHeight() > mTransparentStartHeight) {
    671             scrollOffBottom();
    672         }
    673     }
    674 
    675     /**
    676      * Returns TRUE if we have scrolled far QuickContacts far enough that we should dismiss it
    677      * without waiting for the user to finish their drag.
    678      */
    679     private boolean shouldDismissOnScroll() {
    680         return mHasEverTouchedTheTop && getTransparentViewHeight() > mDismissDistanceOnScroll;
    681     }
    682 
    683     /**
    684      * Return ratio of non-transparent:viewgroup-height for this viewgroup at the starting position.
    685      */
    686     public float getStartingTransparentHeightRatio() {
    687         return getTransparentHeightRatio(mTransparentStartHeight);
    688     }
    689 
    690     private float getTransparentHeightRatio(int transparentHeight) {
    691         final float heightRatio = (float) transparentHeight / getHeight();
    692         // Clamp between [0, 1] in case this is called before height is initialized.
    693         return 1.0f - Math.max(Math.min(1.0f, heightRatio), 0f);
    694     }
    695 
    696     public void scrollOffBottom() {
    697         mIsTouchDisabledForDismissAnimation = true;
    698         final Interpolator interpolator = new AcceleratingFlingInterpolator(
    699                 EXIT_FLING_ANIMATION_DURATION_MS, getCurrentVelocity(),
    700                 getScrollUntilOffBottom());
    701         mScroller.forceFinished(true);
    702         ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll",
    703                 getScroll() - getScrollUntilOffBottom());
    704         translateAnimation.setRepeatCount(0);
    705         translateAnimation.setInterpolator(interpolator);
    706         translateAnimation.setDuration(EXIT_FLING_ANIMATION_DURATION_MS);
    707         translateAnimation.addListener(mSnapToBottomListener);
    708         translateAnimation.start();
    709         if (mListener != null) {
    710             mListener.onStartScrollOffBottom();
    711         }
    712     }
    713 
    714     /**
    715      * @param scrollToCurrentPosition if true, will scroll from the bottom of the screen to the
    716      * current position. Otherwise, will scroll from the bottom of the screen to the top of the
    717      * screen.
    718      */
    719     public void scrollUpForEntranceAnimation(boolean scrollToCurrentPosition) {
    720         final int currentPosition = getScroll();
    721         final int bottomScrollPosition = currentPosition
    722                 - (getHeight() - getTransparentViewHeight()) + 1;
    723         final Interpolator interpolator = AnimationUtils.loadInterpolator(getContext(),
    724                 android.R.interpolator.linear_out_slow_in);
    725         final int desiredValue = currentPosition + (scrollToCurrentPosition ? currentPosition
    726                 : getTransparentViewHeight());
    727         final ObjectAnimator animator = ObjectAnimator.ofInt(this, "scroll", bottomScrollPosition,
    728                 desiredValue);
    729         animator.setInterpolator(interpolator);
    730         animator.addUpdateListener(new AnimatorUpdateListener() {
    731             @Override
    732             public void onAnimationUpdate(ValueAnimator animation) {
    733                 if (animation.getAnimatedValue().equals(desiredValue) && mListener != null) {
    734                     mListener.onEntranceAnimationDone();
    735                 }
    736             }
    737         });
    738         animator.start();
    739     }
    740 
    741     @Override
    742     public void scrollTo(int x, int y) {
    743         final int delta = y - getScroll();
    744         boolean wasFullscreen = getScrollNeededToBeFullScreen() <= 0;
    745         if (delta > 0) {
    746             scrollUp(delta);
    747         } else {
    748             scrollDown(delta);
    749         }
    750         updatePhotoTintAndDropShadow();
    751         updateHeaderTextSizeAndMargin();
    752         final boolean isFullscreen = getScrollNeededToBeFullScreen() <= 0;
    753         mHasEverTouchedTheTop |= isFullscreen;
    754         if (mListener != null) {
    755             if (wasFullscreen && !isFullscreen) {
    756                  mListener.onExitFullscreen();
    757             } else if (!wasFullscreen && isFullscreen) {
    758                 mListener.onEnterFullscreen();
    759             }
    760             if (!isFullscreen || !wasFullscreen) {
    761                 mListener.onTransparentViewHeightChange(
    762                         getTransparentHeightRatio(getTransparentViewHeight()));
    763             }
    764         }
    765     }
    766 
    767     /**
    768      * Change the height of the header/toolbar. Do *not* use this outside animations. This was
    769      * designed for use by {@link #prepareForShrinkingScrollChild}.
    770      */
    771     @NeededForReflection
    772     public void setToolbarHeight(int delta) {
    773         final ViewGroup.LayoutParams toolbarLayoutParams
    774                 = mToolbar.getLayoutParams();
    775         toolbarLayoutParams.height = delta;
    776         mToolbar.setLayoutParams(toolbarLayoutParams);
    777 
    778         updatePhotoTintAndDropShadow();
    779         updateHeaderTextSizeAndMargin();
    780     }
    781 
    782     @NeededForReflection
    783     public int getToolbarHeight() {
    784         return mToolbar.getLayoutParams().height;
    785     }
    786 
    787     /**
    788      * Set the height of the toolbar and update its tint accordingly.
    789      */
    790     @NeededForReflection
    791     public void setHeaderHeight(int height) {
    792         final ViewGroup.LayoutParams toolbarLayoutParams
    793                 = mToolbar.getLayoutParams();
    794         toolbarLayoutParams.height = height;
    795         mToolbar.setLayoutParams(toolbarLayoutParams);
    796         updatePhotoTintAndDropShadow();
    797         updateHeaderTextSizeAndMargin();
    798     }
    799 
    800     @NeededForReflection
    801     public int getHeaderHeight() {
    802         return mToolbar.getLayoutParams().height;
    803     }
    804 
    805     @NeededForReflection
    806     public void setScroll(int scroll) {
    807         scrollTo(0, scroll);
    808     }
    809 
    810     /**
    811      * Returns the total amount scrolled inside the nested ScrollView + the amount of shrinking
    812      * performed on the ToolBar. This is the value inspected by animators.
    813      */
    814     @NeededForReflection
    815     public int getScroll() {
    816         return mTransparentStartHeight - getTransparentViewHeight()
    817                 + getMaximumScrollableHeaderHeight() - getToolbarHeight()
    818                 + mScrollView.getScrollY();
    819     }
    820 
    821     private int getMaximumScrollableHeaderHeight() {
    822         return mIsOpenContactSquare ? mMaximumHeaderHeight : mIntermediateHeaderHeight;
    823     }
    824 
    825     /**
    826      * A variant of {@link #getScroll} that pretends the header is never larger than
    827      * than mIntermediateHeaderHeight. This function is sometimes needed when making scrolling
    828      * decisions that will not change the header size (ie, snapping to the bottom or top).
    829      *
    830      * When mIsOpenContactSquare is true, this function considers mIntermediateHeaderHeight ==
    831      * mMaximumHeaderHeight, since snapping decisions will be made relative the full header
    832      * size when mIsOpenContactSquare = true.
    833      *
    834      * This value should never be used in conjunction with {@link #getScroll} values.
    835      */
    836     private int getScroll_ignoreOversizedHeaderForSnapping() {
    837         return mTransparentStartHeight - getTransparentViewHeight()
    838                 + Math.max(getMaximumScrollableHeaderHeight() - getToolbarHeight(), 0)
    839                 + mScrollView.getScrollY();
    840     }
    841 
    842     /**
    843      * Amount of transparent space above the header/toolbar.
    844      */
    845     public int getScrollNeededToBeFullScreen() {
    846         return getTransparentViewHeight();
    847     }
    848 
    849     /**
    850      * Return amount of scrolling needed in order for all the visible subviews to scroll off the
    851      * bottom.
    852      */
    853     private int getScrollUntilOffBottom() {
    854         return getHeight() + getScroll_ignoreOversizedHeaderForSnapping()
    855                 - mTransparentStartHeight;
    856     }
    857 
    858     @Override
    859     public void computeScroll() {
    860         if (mScroller.computeScrollOffset()) {
    861             // Examine the fling results in order to activate EdgeEffect and halt flings.
    862             final int oldScroll = getScroll();
    863             scrollTo(0, mScroller.getCurrY());
    864             final int delta = mScroller.getCurrY() - oldScroll;
    865             final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
    866             if (delta > distanceFromMaxScrolling && distanceFromMaxScrolling > 0) {
    867                 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
    868             }
    869             if (mIsFullscreenDownwardsFling && getTransparentViewHeight() > 0) {
    870                 // Halt the fling once QuickContact's top is on screen.
    871                 scrollTo(0, getScroll() + getTransparentViewHeight());
    872                 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
    873                 mScroller.abortAnimation();
    874                 mIsFullscreenDownwardsFling = false;
    875             }
    876             if (!awakenScrollBars()) {
    877                 // Keep on drawing until the animation has finished.
    878                 postInvalidateOnAnimation();
    879             }
    880             if (mScroller.getCurrY() >= getMaximumScrollUpwards()) {
    881                 // Halt the fling once QuickContact's bottom is on screen.
    882                 mScroller.abortAnimation();
    883                 mIsFullscreenDownwardsFling = false;
    884             }
    885         }
    886     }
    887 
    888     @Override
    889     public void draw(Canvas canvas) {
    890         super.draw(canvas);
    891 
    892         final int width = getWidth() - getPaddingLeft() - getPaddingRight();
    893         final int height = getHeight();
    894 
    895         if (!mEdgeGlowBottom.isFinished()) {
    896             final int restoreCount = canvas.save();
    897 
    898             // Draw the EdgeEffect on the bottom of the Window (Or a little bit below the bottom
    899             // of the Window if we start to scroll upwards while EdgeEffect is visible). This
    900             // does not need to consider the case where this MultiShrinkScroller doesn't fill
    901             // the Window, since the nested ScrollView should be set to fillViewport.
    902             canvas.translate(-width + getPaddingLeft(),
    903                     height + getMaximumScrollUpwards() - getScroll());
    904 
    905             canvas.rotate(180, width, 0);
    906             if (mIsTwoPanel) {
    907                 // Only show the EdgeEffect on the bottom of the ScrollView.
    908                 mEdgeGlowBottom.setSize(mScrollView.getWidth(), height);
    909                 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
    910                     canvas.translate(mPhotoViewContainer.getWidth(), 0);
    911                 }
    912             } else {
    913                 mEdgeGlowBottom.setSize(width, height);
    914             }
    915             if (mEdgeGlowBottom.draw(canvas)) {
    916                 postInvalidateOnAnimation();
    917             }
    918             canvas.restoreToCount(restoreCount);
    919         }
    920 
    921         if (!mEdgeGlowTop.isFinished()) {
    922             final int restoreCount = canvas.save();
    923             if (mIsTwoPanel) {
    924                 mEdgeGlowTop.setSize(mScrollView.getWidth(), height);
    925                 if (getLayoutDirection() != View.LAYOUT_DIRECTION_RTL) {
    926                     canvas.translate(mPhotoViewContainer.getWidth(), 0);
    927                 }
    928             } else {
    929                 mEdgeGlowTop.setSize(width, height);
    930             }
    931             if (mEdgeGlowTop.draw(canvas)) {
    932                 postInvalidateOnAnimation();
    933             }
    934             canvas.restoreToCount(restoreCount);
    935         }
    936     }
    937 
    938     private float getCurrentVelocity() {
    939         if (mVelocityTracker == null) {
    940             return 0;
    941         }
    942         mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumVelocity);
    943         return mVelocityTracker.getYVelocity();
    944     }
    945 
    946     private void fling(float velocity) {
    947         // For reasons I do not understand, scrolling is less janky when maxY=Integer.MAX_VALUE
    948         // then when maxY is set to an actual value.
    949         mScroller.fling(0, getScroll(), 0, (int) velocity, 0, 0, -Integer.MAX_VALUE,
    950                 Integer.MAX_VALUE);
    951         if (velocity < 0 && mTransparentView.getHeight() <= 0) {
    952             mIsFullscreenDownwardsFling = true;
    953         }
    954         invalidate();
    955     }
    956 
    957     private int getMaximumScrollUpwards() {
    958         if (!mIsTwoPanel) {
    959             return mTransparentStartHeight
    960                     // How much the Header view can compress
    961                     + getMaximumScrollableHeaderHeight() - getFullyCompressedHeaderHeight()
    962                     // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
    963                     + Math.max(0, mScrollViewChild.getHeight() - getHeight()
    964                     + getFullyCompressedHeaderHeight());
    965         } else {
    966             return mTransparentStartHeight
    967                     // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
    968                     + Math.max(0, mScrollViewChild.getHeight() - getHeight());
    969         }
    970     }
    971 
    972     private int getTransparentViewHeight() {
    973         return mTransparentView.getLayoutParams().height;
    974     }
    975 
    976     private void setTransparentViewHeight(int height) {
    977         mTransparentView.getLayoutParams().height = height;
    978         mTransparentView.setLayoutParams(mTransparentView.getLayoutParams());
    979     }
    980 
    981     private void scrollUp(int delta) {
    982         if (getTransparentViewHeight() != 0) {
    983             final int originalValue = getTransparentViewHeight();
    984             setTransparentViewHeight(getTransparentViewHeight() - delta);
    985             setTransparentViewHeight(Math.max(0, getTransparentViewHeight()));
    986             delta -= originalValue - getTransparentViewHeight();
    987         }
    988         final ViewGroup.LayoutParams toolbarLayoutParams
    989                 = mToolbar.getLayoutParams();
    990         if (toolbarLayoutParams.height > getFullyCompressedHeaderHeight()) {
    991             final int originalValue = toolbarLayoutParams.height;
    992             toolbarLayoutParams.height -= delta;
    993             toolbarLayoutParams.height = Math.max(toolbarLayoutParams.height,
    994                     getFullyCompressedHeaderHeight());
    995             mToolbar.setLayoutParams(toolbarLayoutParams);
    996             delta -= originalValue - toolbarLayoutParams.height;
    997         }
    998         mScrollView.scrollBy(0, delta);
    999     }
   1000 
   1001     /**
   1002      * Returns the minimum size that we want to compress the header to, given that we don't want to
   1003      * allow the the ScrollView to scroll unless there is new content off of the edge of ScrollView.
   1004      */
   1005     private int getFullyCompressedHeaderHeight() {
   1006         return Math.min(Math.max(mToolbar.getLayoutParams().height - getOverflowingChildViewSize(),
   1007                 mMinimumHeaderHeight), getMaximumScrollableHeaderHeight());
   1008     }
   1009 
   1010     /**
   1011      * Returns the amount of mScrollViewChild that doesn't fit inside its parent.
   1012      */
   1013     private int getOverflowingChildViewSize() {
   1014         final int usedScrollViewSpace = mScrollViewChild.getHeight();
   1015         return -getHeight() + usedScrollViewSpace + mToolbar.getLayoutParams().height;
   1016     }
   1017 
   1018     private void scrollDown(int delta) {
   1019         if (mScrollView.getScrollY() > 0) {
   1020             final int originalValue = mScrollView.getScrollY();
   1021             mScrollView.scrollBy(0, delta);
   1022             delta -= mScrollView.getScrollY() - originalValue;
   1023         }
   1024         final ViewGroup.LayoutParams toolbarLayoutParams = mToolbar.getLayoutParams();
   1025         if (toolbarLayoutParams.height < getMaximumScrollableHeaderHeight()) {
   1026             final int originalValue = toolbarLayoutParams.height;
   1027             toolbarLayoutParams.height -= delta;
   1028             toolbarLayoutParams.height = Math.min(toolbarLayoutParams.height,
   1029                     getMaximumScrollableHeaderHeight());
   1030             mToolbar.setLayoutParams(toolbarLayoutParams);
   1031             delta -= originalValue - toolbarLayoutParams.height;
   1032         }
   1033         setTransparentViewHeight(getTransparentViewHeight() - delta);
   1034 
   1035         if (getScrollUntilOffBottom() <= 0) {
   1036             post(new Runnable() {
   1037                 @Override
   1038                 public void run() {
   1039                     if (mListener != null) {
   1040                         mListener.onScrolledOffBottom();
   1041                         // No other messages need to be sent to the listener.
   1042                         mListener = null;
   1043                     }
   1044                 }
   1045             });
   1046         }
   1047     }
   1048 
   1049     /**
   1050      * Set the header size and padding, based on the current scroll position.
   1051      */
   1052     private void updateHeaderTextSizeAndMargin() {
   1053         if (mIsTwoPanel) {
   1054             // The text size stays at a constant size & location in two panel layouts.
   1055             return;
   1056         }
   1057 
   1058         // The pivot point for scaling should be middle of the starting side.
   1059         if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
   1060             mTitleAndPhoneticNameView.setPivotX(mTitleAndPhoneticNameView.getWidth());
   1061         } else {
   1062             mTitleAndPhoneticNameView.setPivotX(0);
   1063         }
   1064         mTitleAndPhoneticNameView.setPivotY(mMaximumHeaderTextSize / 2);
   1065 
   1066         final int toolbarHeight = mToolbar.getLayoutParams().height;
   1067         mPhotoTouchInterceptOverlay.setClickable(toolbarHeight != mMaximumHeaderHeight);
   1068 
   1069         if (toolbarHeight >= mMaximumHeaderHeight) {
   1070             // Everything is full size when the header is fully expanded.
   1071             mTitleAndPhoneticNameView.setScaleX(1);
   1072             mTitleAndPhoneticNameView.setScaleY(1);
   1073             setInterpolatedTitleMargins(1);
   1074             return;
   1075         }
   1076 
   1077         final float ratio = (toolbarHeight  - mMinimumHeaderHeight)
   1078                 / (float)(mMaximumHeaderHeight - mMinimumHeaderHeight);
   1079         final float minimumSize = mInvisiblePlaceholderTextView.getHeight();
   1080         float bezierOutput = mTextSizePathInterpolator.getInterpolation(ratio);
   1081         float scale = (minimumSize + (mMaximumHeaderTextSize - minimumSize) * bezierOutput)
   1082                 / mMaximumHeaderTextSize;
   1083 
   1084         // Clamp to reasonable/finite values before passing into framework. The values
   1085         // can be wacky before the first pre-render.
   1086         bezierOutput = (float) Math.min(bezierOutput, 1.0f);
   1087         scale = (float) Math.min(scale, 1.0f);
   1088 
   1089         mTitleAndPhoneticNameView.setScaleX(scale);
   1090         mTitleAndPhoneticNameView.setScaleY(scale);
   1091         setInterpolatedTitleMargins(bezierOutput);
   1092     }
   1093 
   1094     /**
   1095      * Calculate the padding around mTitleAndPhoneticNameView so that it will look appropriate once it
   1096      * finishes moving into its target location/size.
   1097      */
   1098     private void calculateCollapsedLargeTitlePadding() {
   1099         int invisiblePlaceHolderLocation[] = new int[2];
   1100         int largeTextViewRectLocation[] = new int[2];
   1101         mInvisiblePlaceholderTextView.getLocationOnScreen(invisiblePlaceHolderLocation);
   1102         mToolbar.getLocationOnScreen(largeTextViewRectLocation);
   1103         // Distance between top of toolbar to the center of the target rectangle.
   1104         final int desiredTopToCenter = invisiblePlaceHolderLocation[1]
   1105                 + mInvisiblePlaceholderTextView.getHeight() / 2
   1106                 - largeTextViewRectLocation[1];
   1107         // Padding needed on the mTitleAndPhoneticNameView so that it has the same amount of
   1108         // padding as the target rectangle.
   1109         mCollapsedTitleBottomMargin =
   1110                 desiredTopToCenter - mMaximumHeaderTextSize / 2;
   1111     }
   1112 
   1113     /**
   1114      * Interpolate the title's margin size. When {@param x}=1, use the maximum title margins.
   1115      * When {@param x}=0, use the margin values taken from {@link #mInvisiblePlaceholderTextView}.
   1116      */
   1117     private void setInterpolatedTitleMargins(float x) {
   1118         final FrameLayout.LayoutParams titleLayoutParams
   1119                 = (FrameLayout.LayoutParams) mTitleAndPhoneticNameView.getLayoutParams();
   1120         final LinearLayout.LayoutParams toolbarLayoutParams
   1121                 = (LinearLayout.LayoutParams) mToolbar.getLayoutParams();
   1122 
   1123         // Need to add more to margin start if there is a start column
   1124         int startColumnWidth = mStartColumn == null ? 0 : mStartColumn.getWidth();
   1125 
   1126         titleLayoutParams.setMarginStart((int) (mCollapsedTitleStartMargin * (1 - x)
   1127                 + mMaximumTitleMargin * x) + startColumnWidth);
   1128         // How offset the title should be from the bottom of the toolbar
   1129         final int pretendBottomMargin =  (int) (mCollapsedTitleBottomMargin * (1 - x)
   1130                 + mMaximumTitleMargin * x) ;
   1131         // Calculate how offset the title should be from the top of the screen. Instead of
   1132         // calling mTitleAndPhoneticNameView.getHeight() use the mMaximumHeaderTextSize for this
   1133         // calculation. The getHeight() value acts unexpectedly when mTitleAndPhoneticNameView is
   1134         // partially clipped by its parent.
   1135         titleLayoutParams.topMargin = getTransparentViewHeight()
   1136                 + toolbarLayoutParams.height - pretendBottomMargin
   1137                 - mMaximumHeaderTextSize;
   1138         titleLayoutParams.bottomMargin = 0;
   1139         mTitleAndPhoneticNameView.setLayoutParams(titleLayoutParams);
   1140     }
   1141 
   1142     private void updatePhotoTintAndDropShadow() {
   1143         // Let's keep an eye on how long this method takes to complete.
   1144         Trace.beginSection("updatePhotoTintAndDropShadow");
   1145 
   1146         // Tell the photo view what tint we are trying to achieve. Depending on the type of
   1147         // drawable used, the photo view may or may not use this tint.
   1148         mPhotoView.setTint(mHeaderTintColor);
   1149 
   1150         if (mIsTwoPanel && !mPhotoView.isBasedOffLetterTile()) {
   1151             // When in two panel mode, UX considers photo tinting unnecessary for non letter
   1152             // tile photos.
   1153             mTitleGradientDrawable.setAlpha(0xFF);
   1154             mActionBarGradientDrawable.setAlpha(0xFF);
   1155             return;
   1156         }
   1157 
   1158         // We need to use toolbarLayoutParams to determine the height, since the layout
   1159         // params can be updated before the height change is reflected inside the View#getHeight().
   1160         final int toolbarHeight = getToolbarHeight();
   1161 
   1162         if (toolbarHeight <= mMinimumHeaderHeight && !mIsTwoPanel) {
   1163             ViewCompat.setElevation(mPhotoViewContainer, mToolbarElevation);
   1164         } else {
   1165             ViewCompat.setElevation(mPhotoViewContainer, 0);
   1166         }
   1167 
   1168         // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
   1169         mPhotoView.clearColorFilter();
   1170         mColorMatrix.reset();
   1171 
   1172         final int gradientAlpha;
   1173         if (!mPhotoView.isBasedOffLetterTile()) {
   1174             // Constants and equations were arbitrarily picked to choose values for saturation,
   1175             // whiteness, tint and gradient alpha. There were four main objectives:
   1176             // 1) The transition period between the unmodified image and fully colored image should
   1177             //    be very short.
   1178             // 2) The tinting should be fully applied even before the background image is fully
   1179             //    faded out and desaturated. Why? A half tinted photo looks bad and results in
   1180             //    unappealing colors.
   1181             // 3) The function should have a derivative of 0 at ratio = 1 to avoid discontinuities.
   1182             // 4) The entire process should look awesome.
   1183             final float ratio = calculateHeightRatioToBlendingStartHeight(toolbarHeight);
   1184             final float alpha = 1.0f - (float) Math.min(Math.pow(ratio, 1.5f) * 2f, 1f);
   1185             final float tint = (float) Math.min(Math.pow(ratio, 1.5f) * 3f, 1f);
   1186             mColorMatrix.setSaturation(alpha);
   1187             mColorMatrix.postConcat(alphaMatrix(alpha, Color.WHITE));
   1188             mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, tint));
   1189             gradientAlpha = (int) (255 * alpha);
   1190         } else if (mIsTwoPanel) {
   1191             mColorMatrix.reset();
   1192             mColorMatrix.postConcat(alphaMatrix(DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA,
   1193                     mHeaderTintColor));
   1194             gradientAlpha = 0;
   1195         } else {
   1196             // We want a function that has DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA value
   1197             // at the intermediate position and uses TILE_EXPONENT. Finding an equation
   1198             // that satisfies this condition requires the following arithmetic.
   1199             final float ratio = calculateHeightRatioToFullyOpen(toolbarHeight);
   1200             final float intermediateRatio = calculateHeightRatioToFullyOpen((int)
   1201                     (mMaximumPortraitHeaderHeight * INTERMEDIATE_HEADER_HEIGHT_RATIO));
   1202             final float TILE_EXPONENT = 3f;
   1203             final float slowingFactor = (float) ((1 - intermediateRatio) / intermediateRatio
   1204                     / (1 - Math.pow(1 - DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA, 1/TILE_EXPONENT)));
   1205             float linearBeforeIntermediate = Math.max(1 - (1 - ratio) / intermediateRatio
   1206                     / slowingFactor, 0);
   1207             float colorAlpha = 1 - (float) Math.pow(linearBeforeIntermediate, TILE_EXPONENT);
   1208             mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor));
   1209             gradientAlpha = 0;
   1210         }
   1211 
   1212         // TODO: remove re-allocation of ColorMatrixColorFilter objects (b/17627000)
   1213         mPhotoView.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
   1214 
   1215         mTitleGradientDrawable.setAlpha(gradientAlpha);
   1216         mActionBarGradientDrawable.setAlpha(gradientAlpha);
   1217 
   1218         Trace.endSection();
   1219     }
   1220 
   1221     private float calculateHeightRatioToFullyOpen(int height) {
   1222         return (height - mMinimumPortraitHeaderHeight)
   1223                 / (float) (mMaximumPortraitHeaderHeight - mMinimumPortraitHeaderHeight);
   1224     }
   1225 
   1226     private float calculateHeightRatioToBlendingStartHeight(int height) {
   1227         final float intermediateHeight = mMaximumPortraitHeaderHeight
   1228                 * COLOR_BLENDING_START_RATIO;
   1229         final float interpolatingHeightRange = intermediateHeight - mMinimumPortraitHeaderHeight;
   1230         if (height > intermediateHeight) {
   1231             return 0;
   1232         }
   1233         return (intermediateHeight - height) / interpolatingHeightRange;
   1234     }
   1235 
   1236     /**
   1237      * Simulates alpha blending an image with {@param color}.
   1238      */
   1239     private ColorMatrix alphaMatrix(float alpha, int color) {
   1240         mAlphaMatrixValues[0] = Color.red(color) * alpha / 255;
   1241         mAlphaMatrixValues[6] = Color.green(color) * alpha / 255;
   1242         mAlphaMatrixValues[12] = Color.blue(color) * alpha / 255;
   1243         mAlphaMatrixValues[4] = 255 * (1 - alpha);
   1244         mAlphaMatrixValues[9] = 255 * (1 - alpha);
   1245         mAlphaMatrixValues[14] = 255 * (1 - alpha);
   1246         mWhitenessColorMatrix.set(mAlphaMatrixValues);
   1247         return mWhitenessColorMatrix;
   1248     }
   1249 
   1250     /**
   1251      * Simulates multiply blending an image with a single {@param color}.
   1252      *
   1253      * Multiply blending is [Sa * Da, Sc * Dc]. See {@link android.graphics.PorterDuff}.
   1254      */
   1255     private ColorMatrix multiplyBlendMatrix(int color, float alpha) {
   1256         mMultiplyBlendMatrixValues[0] = multiplyBlend(Color.red(color), alpha);
   1257         mMultiplyBlendMatrixValues[6] = multiplyBlend(Color.green(color), alpha);
   1258         mMultiplyBlendMatrixValues[12] = multiplyBlend(Color.blue(color), alpha);
   1259         mMultiplyBlendMatrix.set(mMultiplyBlendMatrixValues);
   1260         return mMultiplyBlendMatrix;
   1261     }
   1262 
   1263     private float multiplyBlend(int color, float alpha) {
   1264         return color * alpha / 255.0f + (1 - alpha);
   1265     }
   1266 
   1267     private void updateLastEventPosition(MotionEvent event) {
   1268         mLastEventPosition[0] = event.getX();
   1269         mLastEventPosition[1] = event.getY();
   1270     }
   1271 
   1272     private boolean motionShouldStartDrag(MotionEvent event) {
   1273         final float deltaY = event.getY() - mLastEventPosition[1];
   1274         return deltaY > mTouchSlop || deltaY < -mTouchSlop;
   1275     }
   1276 
   1277     private float updatePositionAndComputeDelta(MotionEvent event) {
   1278         final int VERTICAL = 1;
   1279         final float position = mLastEventPosition[VERTICAL];
   1280         updateLastEventPosition(event);
   1281         float elasticityFactor = 1;
   1282         if (position < mLastEventPosition[VERTICAL] && mHasEverTouchedTheTop) {
   1283             // As QuickContacts is dragged from the top of the window, its rate of movement will
   1284             // slow down in proportion to its distance from the top. This will feel springy.
   1285             elasticityFactor += mTransparentView.getHeight() * SPRING_DAMPENING_FACTOR;
   1286         }
   1287         return (position - mLastEventPosition[VERTICAL]) / elasticityFactor;
   1288     }
   1289 
   1290     private void smoothScrollBy(int delta) {
   1291         if (delta == 0) {
   1292             // Delta=0 implies the code calling smoothScrollBy is sloppy. We should avoid doing
   1293             // this, since it prevents Views from being able to register any clicks for 250ms.
   1294             throw new IllegalArgumentException("Smooth scrolling by delta=0 is "
   1295                     + "pointless and harmful");
   1296         }
   1297         mScroller.startScroll(0, getScroll(), 0, delta);
   1298         invalidate();
   1299     }
   1300 
   1301     /**
   1302      * Interpolator that enforces a specific starting velocity. This is useful to avoid a
   1303      * discontinuity between dragging speed and flinging speed.
   1304      *
   1305      * Similar to a {@link android.view.animation.AccelerateInterpolator} in the sense that
   1306      * getInterpolation() is a quadratic function.
   1307      */
   1308     private class AcceleratingFlingInterpolator implements Interpolator {
   1309 
   1310         private final float mStartingSpeedPixelsPerFrame;
   1311         private final float mDurationMs;
   1312         private final int mPixelsDelta;
   1313         private final float mNumberFrames;
   1314 
   1315         public AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond,
   1316                 int pixelsDelta) {
   1317             mStartingSpeedPixelsPerFrame = startingSpeedPixelsPerSecond / getRefreshRate();
   1318             mDurationMs = durationMs;
   1319             mPixelsDelta = pixelsDelta;
   1320             mNumberFrames = mDurationMs / getFrameIntervalMs();
   1321         }
   1322 
   1323         @Override
   1324         public float getInterpolation(float input) {
   1325             final float animationIntervalNumber = mNumberFrames * input;
   1326             final float linearDelta = (animationIntervalNumber * mStartingSpeedPixelsPerFrame)
   1327                     / mPixelsDelta;
   1328             // Add the results of a linear interpolator (with the initial speed) with the
   1329             // results of a AccelerateInterpolator.
   1330             if (mStartingSpeedPixelsPerFrame > 0) {
   1331                 return Math.min(input * input + linearDelta, 1);
   1332             } else {
   1333                 // Initial fling was in the wrong direction, make sure that the quadratic component
   1334                 // grows faster in order to make up for this.
   1335                 return Math.min(input * (input - linearDelta) + linearDelta, 1);
   1336             }
   1337         }
   1338 
   1339         private float getRefreshRate() {
   1340             final DisplayManager displayManager = (DisplayManager) MultiShrinkScroller
   1341                     .this.getContext().getSystemService(Context.DISPLAY_SERVICE);
   1342             return displayManager.getDisplay(Display.DEFAULT_DISPLAY).getRefreshRate();
   1343         }
   1344 
   1345         public long getFrameIntervalMs() {
   1346             return (long)(1000 / getRefreshRate());
   1347         }
   1348     }
   1349 
   1350     /**
   1351      * Expand the header if the mScrollViewChild is about to shrink by enough to create new empty
   1352      * space at the bottom of this ViewGroup.
   1353      */
   1354     public void prepareForShrinkingScrollChild(int heightDelta) {
   1355         final int newEmptyScrollViewSpace = -getOverflowingChildViewSize() + heightDelta;
   1356         if (newEmptyScrollViewSpace > 0 && !mIsTwoPanel) {
   1357             final int newDesiredToolbarHeight = Math.min(getToolbarHeight()
   1358                     + newEmptyScrollViewSpace, getMaximumScrollableHeaderHeight());
   1359             ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).setDuration(
   1360                     ExpandingEntryCardView.DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS).start();
   1361         }
   1362     }
   1363 
   1364     /**
   1365      * If {@param areTouchesDisabled} is TRUE, ignore all of the user's touches.
   1366      */
   1367     public void setDisableTouchesForSuppressLayout(boolean areTouchesDisabled) {
   1368         // The card expansion animation uses the Transition framework's ChangeBounds API. This
   1369         // invokes suppressLayout(true) on the MultiShrinkScroller. As a result, we need to avoid
   1370         // all layout changes during expansion in order to avoid weird layout artifacts.
   1371         mIsTouchDisabledForSuppressLayout = areTouchesDisabled;
   1372     }
   1373 }
   1374