blob: e7ca823fea2308bc4c8e04d9fc5a0cc8ee71372b [file] [log] [blame]
Brian Attwellb7e43642014-06-02 14:33:04 -07001package com.android.contacts.widget;
2
3import com.android.contacts.R;
Wenyi Wang4c3d3e22015-12-17 14:30:02 -08004import com.android.contacts.common.compat.CompatUtils;
Paul Soulos0cda9ae2014-07-23 11:27:28 -07005import com.android.contacts.quickcontact.ExpandingEntryCardView;
Brian Attwellb7e43642014-06-02 14:33:04 -07006import com.android.contacts.test.NeededForReflection;
Brian Attwell64d3abc2014-06-12 17:08:12 -07007import com.android.contacts.util.SchedulingUtils;
Brian Attwellb7e43642014-06-02 14:33:04 -07008
Brian Attwell64d3abc2014-06-12 17:08:12 -07009import android.animation.Animator;
10import android.animation.Animator.AnimatorListener;
Brian Attwellb442dc72014-06-18 11:58:23 -070011import android.animation.AnimatorListenerAdapter;
Brian Attwellb7e43642014-06-02 14:33:04 -070012import android.animation.ObjectAnimator;
Brian Attwellf1076dc2014-08-12 21:28:21 -070013import android.animation.ValueAnimator;
14import android.animation.ValueAnimator.AnimatorUpdateListener;
Brian Attwellb7e43642014-06-02 14:33:04 -070015import android.content.Context;
Brian Attwell64d3abc2014-06-12 17:08:12 -070016import android.content.res.TypedArray;
Brian Attwellb7e43642014-06-02 14:33:04 -070017import android.graphics.Canvas;
Brian Attwell9b889e62014-06-23 18:25:32 -070018import android.graphics.Color;
19import android.graphics.ColorMatrix;
20import android.graphics.ColorMatrixColorFilter;
Brian Attwell4936a922014-07-02 16:16:59 -070021import android.graphics.drawable.GradientDrawable;
Brian Attwell2a3bf462014-12-16 17:24:15 -080022import android.hardware.display.DisplayManager;
Brian Attwelld7980782014-06-24 18:52:26 -070023import android.os.Trace;
Wenyi Wang4c3d3e22015-12-17 14:30:02 -080024import android.support.v4.view.ViewCompat;
25import android.support.v4.view.animation.PathInterpolatorCompat;
Brian Attwellb7e43642014-06-02 14:33:04 -070026import android.util.AttributeSet;
Brian Attwell9b889e62014-06-23 18:25:32 -070027import android.util.TypedValue;
Brian Attwellb442dc72014-06-18 11:58:23 -070028import android.view.Display;
Brian Attwell0d3fd2e2014-07-30 22:23:34 -070029import android.view.Gravity;
Brian Attwellb7e43642014-06-02 14:33:04 -070030import android.view.MotionEvent;
31import android.view.VelocityTracker;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.ViewConfiguration;
Brian Attwellbc3f2952014-07-07 16:05:50 -070035import android.view.animation.AnimationUtils;
Brian Attwellb7e43642014-06-02 14:33:04 -070036import android.view.animation.Interpolator;
37import android.widget.EdgeEffect;
Brian Attwell9b889e62014-06-23 18:25:32 -070038import android.widget.FrameLayout;
Brian Attwellb7e43642014-06-02 14:33:04 -070039import android.widget.LinearLayout;
40import android.widget.Scroller;
41import android.widget.ScrollView;
Brian Attwell9b889e62014-06-23 18:25:32 -070042import android.widget.TextView;
Brian Attwell1c806542014-10-22 14:02:42 -070043import android.widget.Toolbar;
Brian Attwellb7e43642014-06-02 14:33:04 -070044
45/**
46 * A custom {@link ViewGroup} that operates similarly to a {@link ScrollView}, except with multiple
47 * subviews. These subviews are scrolled or shrinked one at a time, until each reaches their
48 * minimum or maximum value.
49 *
50 * MultiShrinkScroller is designed for a specific problem. As such, this class is designed to be
51 * used with a specific layout file: quickcontact_activity.xml. MultiShrinkScroller expects subviews
52 * with specific ID values.
53 *
54 * MultiShrinkScroller's code is heavily influenced by ScrollView. Nonetheless, several ScrollView
55 * features are missing. For example: handling of KEYCODES, OverScroll bounce and saving
56 * scroll state in savedInstanceState bundles.
Brian Attwell7267ee22014-08-20 14:20:20 -070057 *
58 * Before copying this approach to nested scrolling, consider whether something simpler & less
59 * customized will work for you. For example, see the re-usable StickyHeaderListView used by
60 * WifiSetupActivity (very nice). Alternatively, check out Google+'s cover photo scrolling or
61 * Android L's built in nested scrolling support. I thought I needed a more custom ViewGroup in
Brian Attwell5fd86982014-12-04 16:25:33 -080062 * order to track velocity, modify EdgeEffect color & perform the originally specified animations.
63 * As a result this ViewGroup has non-standard talkback and keyboard support.
Brian Attwellb7e43642014-06-02 14:33:04 -070064 */
Brian Attwell0d3fd2e2014-07-30 22:23:34 -070065public class MultiShrinkScroller extends FrameLayout {
Brian Attwellb7e43642014-06-02 14:33:04 -070066
67 /**
68 * 1000 pixels per millisecond. Ie, 1 pixel per second.
69 */
70 private static final int PIXELS_PER_SECOND = 1000;
71
Brian Attwellb442dc72014-06-18 11:58:23 -070072 /**
73 * Length of the acceleration animations. This value was taken from ValueAnimator.java.
74 */
Brian Attwell5fd86982014-12-04 16:25:33 -080075 private static final int EXIT_FLING_ANIMATION_DURATION_MS = 250;
Brian Attwellb442dc72014-06-18 11:58:23 -070076
Brian Attwell9b889e62014-06-23 18:25:32 -070077 /**
78 * In portrait mode, the height:width ratio of the photo's starting height.
79 */
Brian Attwell7609b7d2014-11-19 18:01:29 -080080 private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.6f;
81
82 /**
83 * Color blending will only be performed on the contact photo once the toolbar is compressed
84 * to this ratio of its full height.
85 */
86 private static final float COLOR_BLENDING_START_RATIO = 0.5f;
87
Brian Attwell5fd86982014-12-04 16:25:33 -080088 private static final float SPRING_DAMPENING_FACTOR = 0.01f;
89
Brian Attwell7609b7d2014-11-19 18:01:29 -080090 /**
91 * When displaying a letter tile drawable, this alpha value should be used at the intermediate
92 * toolbar height.
93 */
94 private static final float DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA = 0.8f;
Brian Attwell9b889e62014-06-23 18:25:32 -070095
Brian Attwellb7e43642014-06-02 14:33:04 -070096 private float[] mLastEventPosition = { 0, 0 };
97 private VelocityTracker mVelocityTracker;
98 private boolean mIsBeingDragged = false;
99 private boolean mReceivedDown = false;
Brian Attwell5fd86982014-12-04 16:25:33 -0800100 /**
101 * Did the current downwards fling/scroll-animation start while we were fullscreen?
102 */
103 private boolean mIsFullscreenDownwardsFling = false;
Brian Attwellb7e43642014-06-02 14:33:04 -0700104
105 private ScrollView mScrollView;
106 private View mScrollViewChild;
107 private View mToolbar;
Brian Attwell9b889e62014-06-23 18:25:32 -0700108 private QuickContactImageView mPhotoView;
Brian Attwell019c71f2014-06-10 19:29:03 -0700109 private View mPhotoViewContainer;
Brian Attwell0d90afe2014-06-18 16:45:41 -0700110 private View mTransparentView;
Brian Attwellb7e43642014-06-02 14:33:04 -0700111 private MultiShrinkScrollerListener mListener;
Brian Attwell9b889e62014-06-23 18:25:32 -0700112 private TextView mLargeTextView;
Tingting Wangdf65d162015-07-24 17:04:35 -0700113 private TextView mPhoneticNameView;
114 private View mTitleAndPhoneticNameView;
Brian Attwelled6cda62014-06-26 17:13:14 -0700115 private View mPhotoTouchInterceptOverlay;
Brian Attwell1c806542014-10-22 14:02:42 -0700116 /** Contains desired size & vertical offset of the title, once the header is fully compressed */
Brian Attwell9b889e62014-06-23 18:25:32 -0700117 private TextView mInvisiblePlaceholderTextView;
Brian Attwell4936a922014-07-02 16:16:59 -0700118 private View mTitleGradientView;
119 private View mActionBarGradientView;
Paul Soulos1deb5322014-08-28 18:47:42 -0700120 private View mStartColumn;
Brian Attwell31b2d422014-06-05 00:14:58 -0700121 private int mHeaderTintColor;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700122 private int mMaximumHeaderHeight;
Brian Attwell9b889e62014-06-23 18:25:32 -0700123 private int mMinimumHeaderHeight;
Brian Attwellc33ae172014-07-01 14:21:37 -0700124 /**
125 * When the contact photo is tapped, it is resized to max size or this size. This value also
126 * sometimes represents the maximum achievable header size achieved by scrolling. To enforce
127 * this maximum in scrolling logic, always access this value via
128 * {@link #getMaximumScrollableHeaderHeight}.
129 */
Brian Attwell9b889e62014-06-23 18:25:32 -0700130 private int mIntermediateHeaderHeight;
Brian Attwellc33ae172014-07-01 14:21:37 -0700131 /**
132 * If true, regular scrolling can expand the header beyond mIntermediateHeaderHeight. The
133 * header, that contains the contact photo, can expand to a height equal its width.
134 */
135 private boolean mIsOpenContactSquare;
Brian Attwell9b889e62014-06-23 18:25:32 -0700136 private int mMaximumHeaderTextSize;
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700137 private int mCollapsedTitleBottomMargin;
138 private int mCollapsedTitleStartMargin;
Brian Attwelld7980782014-06-24 18:52:26 -0700139 private int mMinimumPortraitHeaderHeight;
140 private int mMaximumPortraitHeaderHeight;
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700141 /**
142 * True once the header has touched the top of the screen at least once.
143 */
144 private boolean mHasEverTouchedTheTop;
Brian Attwell5fd86982014-12-04 16:25:33 -0800145 private boolean mIsTouchDisabledForDismissAnimation;
Brian Attwell245d3d22015-01-21 09:50:08 -0800146 private boolean mIsTouchDisabledForSuppressLayout;
Brian Attwellb7e43642014-06-02 14:33:04 -0700147
148 private final Scroller mScroller;
149 private final EdgeEffect mEdgeGlowBottom;
Brian Attwell5fd86982014-12-04 16:25:33 -0800150 private final EdgeEffect mEdgeGlowTop;
Brian Attwellb7e43642014-06-02 14:33:04 -0700151 private final int mTouchSlop;
152 private final int mMaximumVelocity;
153 private final int mMinimumVelocity;
Brian Attwell5fd86982014-12-04 16:25:33 -0800154 private final int mDismissDistanceOnScroll;
155 private final int mDismissDistanceOnRelease;
156 private final int mSnapToTopSlopHeight;
Brian Attwellb7e43642014-06-02 14:33:04 -0700157 private final int mTransparentStartHeight;
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700158 private final int mMaximumTitleMargin;
Brian Attwell019c71f2014-06-10 19:29:03 -0700159 private final float mToolbarElevation;
Brian Attwell9b889e62014-06-23 18:25:32 -0700160 private final boolean mIsTwoPanel;
Brian Attwell841c3332014-10-27 15:05:09 -0700161 private final float mLandscapePhotoRatio;
Brian Attwelle48a3b52014-07-22 17:11:34 -0700162 private final int mActionBarSize;
Brian Attwell9b889e62014-06-23 18:25:32 -0700163
164 // Objects used to perform color filtering on the header. These are stored as fields for
165 // the sole purpose of avoiding "new" operations inside animation loops.
166 private final ColorMatrix mWhitenessColorMatrix = new ColorMatrix();
Brian Attwell9b889e62014-06-23 18:25:32 -0700167 private final ColorMatrix mColorMatrix = new ColorMatrix();
168 private final float[] mAlphaMatrixValues = {
169 0, 0, 0, 0, 0,
170 0, 0, 0, 0, 0,
171 0, 0, 0, 0, 0,
172 0, 0, 0, 1, 0
173 };
174 private final ColorMatrix mMultiplyBlendMatrix = new ColorMatrix();
175 private final float[] mMultiplyBlendMatrixValues = {
176 0, 0, 0, 0, 0,
177 0, 0, 0, 0, 0,
178 0, 0, 0, 0, 0,
179 0, 0, 0, 1, 0
180 };
Brian Attwellb7e43642014-06-02 14:33:04 -0700181
Wenyi Wang4c3d3e22015-12-17 14:30:02 -0800182 private final Interpolator mTextSizePathInterpolator =
183 PathInterpolatorCompat.create(0.16f, 0.4f, 0.2f, 1);
Brian Attwell7a47bb52014-07-31 11:19:46 -0700184
Brian Attwell7609b7d2014-11-19 18:01:29 -0800185 private final int[] mGradientColors = new int[] {0,0x88000000};
Brian Attwell4936a922014-07-02 16:16:59 -0700186 private GradientDrawable mTitleGradientDrawable = new GradientDrawable(
187 GradientDrawable.Orientation.TOP_BOTTOM, mGradientColors);
188 private GradientDrawable mActionBarGradientDrawable = new GradientDrawable(
189 GradientDrawable.Orientation.BOTTOM_TOP, mGradientColors);
190
Brian Attwellb7e43642014-06-02 14:33:04 -0700191 public interface MultiShrinkScrollerListener {
192 void onScrolledOffBottom();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700193
Brian Attwell8477eaf2014-06-18 15:39:50 -0700194 void onStartScrollOffBottom();
195
Brian Attwellf1076dc2014-08-12 21:28:21 -0700196 void onTransparentViewHeightChange(float ratio);
197
198 void onEntranceAnimationDone();
199
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700200 void onEnterFullscreen();
201
202 void onExitFullscreen();
Brian Attwellb7e43642014-06-02 14:33:04 -0700203 }
204
Brian Attwellb442dc72014-06-18 11:58:23 -0700205 private final AnimatorListener mSnapToBottomListener = new AnimatorListenerAdapter() {
Brian Attwell64d3abc2014-06-12 17:08:12 -0700206 @Override
Brian Attwellb442dc72014-06-18 11:58:23 -0700207 public void onAnimationEnd(Animator animation) {
208 if (getScrollUntilOffBottom() > 0 && mListener != null) {
209 // Due to a rounding error, after the animation finished we haven't fully scrolled
210 // off the screen. Lie to the listener: tell it that we did scroll off the screen.
211 mListener.onScrolledOffBottom();
Brian Attwell8477eaf2014-06-18 15:39:50 -0700212 // No other messages need to be sent to the listener.
213 mListener = null;
Brian Attwellb442dc72014-06-18 11:58:23 -0700214 }
215 }
Brian Attwell64d3abc2014-06-12 17:08:12 -0700216 };
217
218 /**
219 * Interpolator from android.support.v4.view.ViewPager. Snappier and more elastic feeling
220 * than the default interpolator.
221 */
Brian Attwellb7e43642014-06-02 14:33:04 -0700222 private static final Interpolator sInterpolator = new Interpolator() {
223
224 /**
225 * {@inheritDoc}
226 */
227 @Override
228 public float getInterpolation(float t) {
229 t -= 1.0f;
230 return t * t * t * t * t + 1.0f;
231 }
232 };
233
234 public MultiShrinkScroller(Context context) {
235 this(context, null);
236 }
237
238 public MultiShrinkScroller(Context context, AttributeSet attrs) {
239 this(context, attrs, 0);
240 }
241
242 public MultiShrinkScroller(Context context, AttributeSet attrs, int defStyleAttr) {
243 super(context, attrs, defStyleAttr);
244
245 final ViewConfiguration configuration = ViewConfiguration.get(context);
246 setFocusable(false);
247 // Drawing must be enabled in order to support EdgeEffect
248 setWillNotDraw(/* willNotDraw = */ false);
249
250 mEdgeGlowBottom = new EdgeEffect(context);
Brian Attwell5fd86982014-12-04 16:25:33 -0800251 mEdgeGlowTop = new EdgeEffect(context);
Brian Attwellb7e43642014-06-02 14:33:04 -0700252 mScroller = new Scroller(context, sInterpolator);
253 mTouchSlop = configuration.getScaledTouchSlop();
254 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
Brian Attwell5fd86982014-12-04 16:25:33 -0800255 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Brian Attwellb7e43642014-06-02 14:33:04 -0700256 mTransparentStartHeight = (int) getResources().getDimension(
257 R.dimen.quickcontact_starting_empty_height);
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700258 mToolbarElevation = getResources().getDimension(
Brian Attwell019c71f2014-06-10 19:29:03 -0700259 R.dimen.quick_contact_toolbar_elevation);
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700260 mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel);
261 mMaximumTitleMargin = (int) getResources().getDimension(
262 R.dimen.quickcontact_title_initial_margin);
Brian Attwell64d3abc2014-06-12 17:08:12 -0700263
Brian Attwell5fd86982014-12-04 16:25:33 -0800264 mDismissDistanceOnScroll = (int) getResources().getDimension(
265 R.dimen.quickcontact_dismiss_distance_on_scroll);
266 mDismissDistanceOnRelease = (int) getResources().getDimension(
267 R.dimen.quickcontact_dismiss_distance_on_release);
268 mSnapToTopSlopHeight = (int) getResources().getDimension(
269 R.dimen.quickcontact_snap_to_top_slop_height);
270
Brian Attwell841c3332014-10-27 15:05:09 -0700271 final TypedValue photoRatio = new TypedValue();
272 getResources().getValue(R.dimen.quickcontact_landscape_photo_ratio, photoRatio,
273 /* resolveRefs = */ true);
274 mLandscapePhotoRatio = photoRatio.getFloat();
275
Brian Attwell64d3abc2014-06-12 17:08:12 -0700276 final TypedArray attributeArray = context.obtainStyledAttributes(
277 new int[]{android.R.attr.actionBarSize});
Brian Attwelle48a3b52014-07-22 17:11:34 -0700278 mActionBarSize = attributeArray.getDimensionPixelSize(0, 0);
279 mMinimumHeaderHeight = mActionBarSize;
Brian Attwelld7980782014-06-24 18:52:26 -0700280 // This value is approximately equal to the portrait ActionBar size. It isn't exactly the
281 // same, since the landscape and portrait ActionBar sizes can be different.
282 mMinimumPortraitHeaderHeight = mMinimumHeaderHeight;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700283 attributeArray.recycle();
Brian Attwellb7e43642014-06-02 14:33:04 -0700284 }
285
286 /**
287 * This method must be called inside the Activity's OnCreate.
288 */
Brian Attwellc33ae172014-07-01 14:21:37 -0700289 public void initialize(MultiShrinkScrollerListener listener, boolean isOpenContactSquare) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700290 mScrollView = (ScrollView) findViewById(R.id.content_scroller);
291 mScrollViewChild = findViewById(R.id.card_container);
292 mToolbar = findViewById(R.id.toolbar_parent);
Brian Attwell019c71f2014-06-10 19:29:03 -0700293 mPhotoViewContainer = findViewById(R.id.toolbar_parent);
Brian Attwell0d90afe2014-06-18 16:45:41 -0700294 mTransparentView = findViewById(R.id.transparent_view);
Brian Attwell9b889e62014-06-23 18:25:32 -0700295 mLargeTextView = (TextView) findViewById(R.id.large_title);
Tingting Wangdf65d162015-07-24 17:04:35 -0700296 mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
297 mTitleAndPhoneticNameView = findViewById(R.id.title_and_phonetic_name);
Brian Attwell9b889e62014-06-23 18:25:32 -0700298 mInvisiblePlaceholderTextView = (TextView) findViewById(R.id.placeholder_textview);
Paul Soulos1deb5322014-08-28 18:47:42 -0700299 mStartColumn = findViewById(R.id.empty_start_column);
Paul Soulosfffcf2a2014-09-11 18:15:52 +0000300 // Touching the empty space should close the card
301 if (mStartColumn != null) {
302 mStartColumn.setOnClickListener(new OnClickListener() {
303 @Override
304 public void onClick(View v) {
305 scrollOffBottom();
306 }
307 });
308 findViewById(R.id.empty_end_column).setOnClickListener(new OnClickListener() {
309 @Override
310 public void onClick(View v) {
311 scrollOffBottom();
312 }
313 });
314 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700315 mListener = listener;
Brian Attwellc33ae172014-07-01 14:21:37 -0700316 mIsOpenContactSquare = isOpenContactSquare;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700317
Brian Attwell9b889e62014-06-23 18:25:32 -0700318 mPhotoView = (QuickContactImageView) findViewById(R.id.photo);
Brian Attwelled6cda62014-06-26 17:13:14 -0700319
Brian Attwell4936a922014-07-02 16:16:59 -0700320 mTitleGradientView = findViewById(R.id.title_gradient);
321 mTitleGradientView.setBackground(mTitleGradientDrawable);
322 mActionBarGradientView = findViewById(R.id.action_bar_gradient);
323 mActionBarGradientView.setBackground(mActionBarGradientDrawable);
Brian Attwell1c806542014-10-22 14:02:42 -0700324 mCollapsedTitleStartMargin = ((Toolbar) findViewById(R.id.toolbar)).getContentInsetStart();
Brian Attwell4936a922014-07-02 16:16:59 -0700325
Brian Attwell3d291452014-08-26 17:59:33 -0700326 mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay);
Brian Attwelled6cda62014-06-26 17:13:14 -0700327 if (!mIsTwoPanel) {
Brian Attwelled6cda62014-06-26 17:13:14 -0700328 mPhotoTouchInterceptOverlay.setOnClickListener(new OnClickListener() {
329 @Override
330 public void onClick(View v) {
Brian Attwellf37ba162014-09-23 14:44:17 -0700331 expandHeader();
Brian Attwelled6cda62014-06-26 17:13:14 -0700332 }
333 });
334 }
Brian Attwell64d3abc2014-06-12 17:08:12 -0700335
Brian Attwell9b889e62014-06-23 18:25:32 -0700336 SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ false, new Runnable() {
Brian Attwell64d3abc2014-06-12 17:08:12 -0700337 @Override
338 public void run() {
Brian Attwellac3ed8e2014-06-27 17:24:42 -0700339 if (!mIsTwoPanel) {
340 // We never want the height of the photo view to exceed its width.
Brian Attwell1e3d3a92014-08-29 11:55:07 -0700341 mMaximumHeaderHeight = mPhotoViewContainer.getWidth();
Brian Attwellac3ed8e2014-06-27 17:24:42 -0700342 mIntermediateHeaderHeight = (int) (mMaximumHeaderHeight
343 * INTERMEDIATE_HEADER_HEIGHT_RATIO);
344 }
Brian Attwell841c3332014-10-27 15:05:09 -0700345 mMaximumPortraitHeaderHeight = mIsTwoPanel ? getHeight()
Brian Attwell1e3d3a92014-08-29 11:55:07 -0700346 : mPhotoViewContainer.getWidth();
Brian Attwellc33ae172014-07-01 14:21:37 -0700347 setHeaderHeight(getMaximumScrollableHeaderHeight());
Tingting Wangdf65d162015-07-24 17:04:35 -0700348 mMaximumHeaderTextSize = mTitleAndPhoneticNameView.getHeight();
Brian Attwell9b889e62014-06-23 18:25:32 -0700349 if (mIsTwoPanel) {
350 mMaximumHeaderHeight = getHeight();
351 mMinimumHeaderHeight = mMaximumHeaderHeight;
352 mIntermediateHeaderHeight = mMaximumHeaderHeight;
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700353
354 // Permanently set photo width and height.
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700355 final ViewGroup.LayoutParams photoLayoutParams
356 = mPhotoViewContainer.getLayoutParams();
357 photoLayoutParams.height = mMaximumHeaderHeight;
Brian Attwell841c3332014-10-27 15:05:09 -0700358 photoLayoutParams.width = (int) (mMaximumHeaderHeight * mLandscapePhotoRatio);
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700359 mPhotoViewContainer.setLayoutParams(photoLayoutParams);
360
361 // Permanently set title width and margin.
362 final FrameLayout.LayoutParams largeTextLayoutParams
Tingting Wangdf65d162015-07-24 17:04:35 -0700363 = (FrameLayout.LayoutParams) mTitleAndPhoneticNameView
364 .getLayoutParams();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700365 largeTextLayoutParams.width = photoLayoutParams.width -
366 largeTextLayoutParams.leftMargin - largeTextLayoutParams.rightMargin;
367 largeTextLayoutParams.gravity = Gravity.BOTTOM | Gravity.START;
Tingting Wangdf65d162015-07-24 17:04:35 -0700368 mTitleAndPhoneticNameView.setLayoutParams(largeTextLayoutParams);
Brian Attwellcf930c72014-09-05 15:22:23 -0700369 } else {
370 // Set the width of mLargeTextView as if it was nested inside
371 // mPhotoViewContainer.
372 mLargeTextView.setWidth(mPhotoViewContainer.getWidth()
373 - 2 * mMaximumTitleMargin);
Tingting Wangdf65d162015-07-24 17:04:35 -0700374 mPhoneticNameView.setWidth(mPhotoViewContainer.getWidth()
375 - 2 * mMaximumTitleMargin);
Brian Attwell9b889e62014-06-23 18:25:32 -0700376 }
377
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700378 calculateCollapsedLargeTitlePadding();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700379 updateHeaderTextSizeAndMargin();
Brian Attwell4936a922014-07-02 16:16:59 -0700380 configureGradientViewHeights();
Brian Attwell64d3abc2014-06-12 17:08:12 -0700381 }
382 });
Brian Attwellb7e43642014-06-02 14:33:04 -0700383 }
384
Brian Attwell4936a922014-07-02 16:16:59 -0700385 private void configureGradientViewHeights() {
Brian Attwell4936a922014-07-02 16:16:59 -0700386 final FrameLayout.LayoutParams actionBarGradientLayoutParams
387 = (FrameLayout.LayoutParams) mActionBarGradientView.getLayoutParams();
Brian Attwell7609b7d2014-11-19 18:01:29 -0800388 actionBarGradientLayoutParams.height = mActionBarSize;
Brian Attwell4936a922014-07-02 16:16:59 -0700389 mActionBarGradientView.setLayoutParams(actionBarGradientLayoutParams);
390 final FrameLayout.LayoutParams titleGradientLayoutParams
391 = (FrameLayout.LayoutParams) mTitleGradientView.getLayoutParams();
Brian Attwell7609b7d2014-11-19 18:01:29 -0800392 final float TITLE_GRADIENT_SIZE_COEFFICIENT = 1.25f;
Brian Attwell4936a922014-07-02 16:16:59 -0700393 final FrameLayout.LayoutParams largeTextLayoutParms
Tingting Wangdf65d162015-07-24 17:04:35 -0700394 = (FrameLayout.LayoutParams) mTitleAndPhoneticNameView.getLayoutParams();
395 titleGradientLayoutParams.height = (int) ((mTitleAndPhoneticNameView.getHeight()
Brian Attwell7609b7d2014-11-19 18:01:29 -0800396 + largeTextLayoutParms.bottomMargin) * TITLE_GRADIENT_SIZE_COEFFICIENT);
Brian Attwell4936a922014-07-02 16:16:59 -0700397 mTitleGradientView.setLayoutParams(titleGradientLayoutParams);
398 }
399
Tingting Wang9f153a02015-11-05 18:11:04 -0800400 public void setTitle(String title, boolean isPhoneNumber) {
Brian Attwell9b889e62014-06-23 18:25:32 -0700401 mLargeTextView.setText(title);
Tingting Wang9f153a02015-11-05 18:11:04 -0800402 // We have a phone number as "mLargeTextView" so make it always LTR.
403 if (isPhoneNumber) {
404 mLargeTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
405 }
Brian Attwell91bd3002014-08-25 15:24:22 -0700406 mPhotoTouchInterceptOverlay.setContentDescription(title);
Brian Attwell9b889e62014-06-23 18:25:32 -0700407 }
408
Tingting Wangdf65d162015-07-24 17:04:35 -0700409 public void setPhoneticName(String phoneticName) {
410 mPhoneticNameView.setText(phoneticName);
411 // Every time the phonetic name is changed, set mPhoneticNameView as visible,
412 // in case it just changed from Visibility=GONE.
413 mPhoneticNameView.setVisibility(View.VISIBLE);
414 initialize(mListener, mIsOpenContactSquare);
415 }
416
417 public void setPhoneticNameGone() {
418 mPhoneticNameView.setVisibility(View.GONE);
419 // Initialize to make Visibility work.
420 initialize(mListener, mIsOpenContactSquare);
421 }
422
Brian Attwellb7e43642014-06-02 14:33:04 -0700423 @Override
424 public boolean onInterceptTouchEvent(MotionEvent event) {
Brian Attwellbf2854d2014-10-08 16:12:11 -0700425 if (mVelocityTracker == null) {
426 mVelocityTracker = VelocityTracker.obtain();
427 }
428 mVelocityTracker.addMovement(event);
429
Brian Attwellb7e43642014-06-02 14:33:04 -0700430 // The only time we want to intercept touch events is when we are being dragged.
431 return shouldStartDrag(event);
432 }
433
434 private boolean shouldStartDrag(MotionEvent event) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800435 if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return false;
436
Brian Attwell5fd86982014-12-04 16:25:33 -0800437
Brian Attwellb7e43642014-06-02 14:33:04 -0700438 if (mIsBeingDragged) {
439 mIsBeingDragged = false;
440 return false;
441 }
442
443 switch (event.getAction()) {
444 // If we are in the middle of a fling and there is a down event, we'll steal it and
445 // start a drag.
446 case MotionEvent.ACTION_DOWN:
447 updateLastEventPosition(event);
448 if (!mScroller.isFinished()) {
449 startDrag();
450 return true;
451 } else {
452 mReceivedDown = true;
453 }
454 break;
455
456 // Otherwise, we will start a drag if there is enough motion in the direction we are
457 // capable of scrolling.
458 case MotionEvent.ACTION_MOVE:
459 if (motionShouldStartDrag(event)) {
460 updateLastEventPosition(event);
461 startDrag();
462 return true;
463 }
464 break;
465 }
466
467 return false;
468 }
469
470 @Override
471 public boolean onTouchEvent(MotionEvent event) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800472 if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return true;
Brian Attwell5fd86982014-12-04 16:25:33 -0800473
Brian Attwellb7e43642014-06-02 14:33:04 -0700474 final int action = event.getAction();
475
476 if (mVelocityTracker == null) {
477 mVelocityTracker = VelocityTracker.obtain();
478 }
479 mVelocityTracker.addMovement(event);
480
481 if (!mIsBeingDragged) {
482 if (shouldStartDrag(event)) {
483 return true;
484 }
485
486 if (action == MotionEvent.ACTION_UP && mReceivedDown) {
487 mReceivedDown = false;
488 return performClick();
489 }
490 return true;
491 }
492
493 switch (action) {
494 case MotionEvent.ACTION_MOVE:
495 final float delta = updatePositionAndComputeDelta(event);
496 scrollTo(0, getScroll() + (int) delta);
497 mReceivedDown = false;
498
499 if (mIsBeingDragged) {
Brian Attwell9b889e62014-06-23 18:25:32 -0700500 final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
501 if (delta > distanceFromMaxScrolling) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700502 // The ScrollView is being pulled upwards while there is no more
503 // content offscreen, and the view port is already fully expanded.
504 mEdgeGlowBottom.onPull(delta / getHeight(), 1 - event.getX() / getWidth());
505 }
Brian Attwell9b889e62014-06-23 18:25:32 -0700506
Brian Attwellb7e43642014-06-02 14:33:04 -0700507 if (!mEdgeGlowBottom.isFinished()) {
508 postInvalidateOnAnimation();
509 }
510
Brian Attwell5fd86982014-12-04 16:25:33 -0800511 if (shouldDismissOnScroll()) {
512 scrollOffBottom();
513 }
514
Brian Attwellb7e43642014-06-02 14:33:04 -0700515 }
516 break;
517
518 case MotionEvent.ACTION_UP:
519 case MotionEvent.ACTION_CANCEL:
520 stopDrag(action == MotionEvent.ACTION_CANCEL);
521 mReceivedDown = false;
522 break;
523 }
524
525 return true;
526 }
527
Brian Attwell31b2d422014-06-05 00:14:58 -0700528 public void setHeaderTintColor(int color) {
529 mHeaderTintColor = color;
Brian Attwell019c71f2014-06-10 19:29:03 -0700530 updatePhotoTintAndDropShadow();
Wenyi Wang4c3d3e22015-12-17 14:30:02 -0800531 if (CompatUtils.isLollipopCompatible()) {
532 // Use the same amount of alpha on the new tint color as the previous tint color.
533 final int edgeEffectAlpha = Color.alpha(mEdgeGlowBottom.getColor());
534 mEdgeGlowBottom.setColor((color & 0xffffff) | Color.argb(edgeEffectAlpha, 0, 0, 0));
535 mEdgeGlowTop.setColor(mEdgeGlowBottom.getColor());
536 }
Brian Attwell31b2d422014-06-05 00:14:58 -0700537 }
538
Brian Attwell64d3abc2014-06-12 17:08:12 -0700539 /**
Brian Attwellf37ba162014-09-23 14:44:17 -0700540 * Expand to maximum size.
Brian Attwell64d3abc2014-06-12 17:08:12 -0700541 */
Brian Attwellf37ba162014-09-23 14:44:17 -0700542 private void expandHeader() {
Brian Attwell64d3abc2014-06-12 17:08:12 -0700543 if (getHeaderHeight() != mMaximumHeaderHeight) {
544 final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
545 mMaximumHeaderHeight);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700546 animator.setDuration(ExpandingEntryCardView.DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
Brian Attwell64d3abc2014-06-12 17:08:12 -0700547 animator.start();
Brian Attwell7fa11d12014-06-12 18:04:16 -0700548 // Scroll nested scroll view to its top
549 if (mScrollView.getScrollY() != 0) {
550 ObjectAnimator.ofInt(mScrollView, "scrollY", -mScrollView.getScrollY()).start();
551 }
Brian Attwell64d3abc2014-06-12 17:08:12 -0700552 }
553 }
554
Brian Attwellb7e43642014-06-02 14:33:04 -0700555 private void startDrag() {
556 mIsBeingDragged = true;
557 mScroller.abortAnimation();
558 }
559
560 private void stopDrag(boolean cancelled) {
561 mIsBeingDragged = false;
562 if (!cancelled && getChildCount() > 0) {
563 final float velocity = getCurrentVelocity();
564 if (velocity > mMinimumVelocity || velocity < -mMinimumVelocity) {
565 fling(-velocity);
566 onDragFinished(mScroller.getFinalY() - mScroller.getStartY());
567 } else {
568 onDragFinished(/* flingDelta = */ 0);
569 }
570 } else {
571 onDragFinished(/* flingDelta = */ 0);
572 }
573
574 if (mVelocityTracker != null) {
575 mVelocityTracker.recycle();
576 mVelocityTracker = null;
577 }
578
579 mEdgeGlowBottom.onRelease();
580 }
581
582 private void onDragFinished(int flingDelta) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800583 if (getTransparentViewHeight() <= 0) {
584 // Don't perform any snapping if quick contacts is full screen.
585 return;
586 }
587 if (!snapToTopOnDragFinished(flingDelta)) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700588 // The drag/fling won't result in the content at the top of the Window. Consider
589 // snapping the content to the bottom of the window.
Brian Attwell5fd86982014-12-04 16:25:33 -0800590 snapToBottomOnDragFinished();
Brian Attwellb7e43642014-06-02 14:33:04 -0700591 }
592 }
593
594 /**
595 * If needed, snap the subviews to the top of the Window.
Brian Attwell5fd86982014-12-04 16:25:33 -0800596 *
597 * @return TRUE if QuickContacts will snap/fling to to top after this method call.
Brian Attwellb7e43642014-06-02 14:33:04 -0700598 */
Brian Attwell5fd86982014-12-04 16:25:33 -0800599 private boolean snapToTopOnDragFinished(int flingDelta) {
600 if (!mHasEverTouchedTheTop) {
601 // If the current fling is predicted to scroll past the top, then we don't need to snap
602 // to the top. However, if the fling only flings past the top by a tiny amount,
603 // it will look nicer to snap than to fling.
604 final float predictedScrollPastTop = getTransparentViewHeight() - flingDelta;
605 if (predictedScrollPastTop < -mSnapToTopSlopHeight) {
606 return false;
607 }
608
609 if (getTransparentViewHeight() <= mTransparentStartHeight) {
610 // We are above the starting scroll position so snap to the top.
611 mScroller.forceFinished(true);
612 smoothScrollBy(getTransparentViewHeight());
613 return true;
614 }
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700615 return false;
616 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800617 if (getTransparentViewHeight() < mDismissDistanceOnRelease) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700618 mScroller.forceFinished(true);
Brian Attwell5fd86982014-12-04 16:25:33 -0800619 smoothScrollBy(getTransparentViewHeight());
Brian Attwellb7e43642014-06-02 14:33:04 -0700620 return true;
621 }
622 return false;
623 }
624
625 /**
626 * If needed, scroll all the subviews off the bottom of the Window.
627 */
Brian Attwell5fd86982014-12-04 16:25:33 -0800628 private void snapToBottomOnDragFinished() {
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700629 if (mHasEverTouchedTheTop) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800630 if (getTransparentViewHeight() > mDismissDistanceOnRelease) {
631 scrollOffBottom();
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700632 }
633 return;
634 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800635 if (getTransparentViewHeight() > mTransparentStartHeight) {
Brian Attwell8477eaf2014-06-18 15:39:50 -0700636 scrollOffBottom();
637 }
638 }
639
Brian Attwellf1076dc2014-08-12 21:28:21 -0700640 /**
Brian Attwell5fd86982014-12-04 16:25:33 -0800641 * Returns TRUE if we have scrolled far QuickContacts far enough that we should dismiss it
642 * without waiting for the user to finish their drag.
643 */
644 private boolean shouldDismissOnScroll() {
645 return mHasEverTouchedTheTop && getTransparentViewHeight() > mDismissDistanceOnScroll;
646 }
647
648 /**
Brian Attwellf1076dc2014-08-12 21:28:21 -0700649 * Return ratio of non-transparent:viewgroup-height for this viewgroup at the starting position.
650 */
651 public float getStartingTransparentHeightRatio() {
652 return getTransparentHeightRatio(mTransparentStartHeight);
653 }
654
655 private float getTransparentHeightRatio(int transparentHeight) {
656 final float heightRatio = (float) transparentHeight / getHeight();
657 // Clamp between [0, 1] in case this is called before height is initialized.
658 return 1.0f - Math.max(Math.min(1.0f, heightRatio), 0f);
659 }
660
Brian Attwell8477eaf2014-06-18 15:39:50 -0700661 public void scrollOffBottom() {
Brian Attwell5fd86982014-12-04 16:25:33 -0800662 mIsTouchDisabledForDismissAnimation = true;
Brian Attwell8477eaf2014-06-18 15:39:50 -0700663 final Interpolator interpolator = new AcceleratingFlingInterpolator(
664 EXIT_FLING_ANIMATION_DURATION_MS, getCurrentVelocity(),
665 getScrollUntilOffBottom());
666 mScroller.forceFinished(true);
667 ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll",
668 getScroll() - getScrollUntilOffBottom());
669 translateAnimation.setRepeatCount(0);
670 translateAnimation.setInterpolator(interpolator);
671 translateAnimation.setDuration(EXIT_FLING_ANIMATION_DURATION_MS);
672 translateAnimation.addListener(mSnapToBottomListener);
673 translateAnimation.start();
674 if (mListener != null) {
675 mListener.onStartScrollOffBottom();
Brian Attwellb7e43642014-06-02 14:33:04 -0700676 }
677 }
678
Brian Attwellc33ae172014-07-01 14:21:37 -0700679 /**
680 * @param scrollToCurrentPosition if true, will scroll from the bottom of the screen to the
681 * current position. Otherwise, will scroll from the bottom of the screen to the top of the
682 * screen.
683 */
684 public void scrollUpForEntranceAnimation(boolean scrollToCurrentPosition) {
685 final int currentPosition = getScroll();
686 final int bottomScrollPosition = currentPosition
687 - (getHeight() - getTransparentViewHeight()) + 1;
Brian Attwellbc3f2952014-07-07 16:05:50 -0700688 final Interpolator interpolator = AnimationUtils.loadInterpolator(getContext(),
689 android.R.interpolator.linear_out_slow_in);
Brian Attwellf1076dc2014-08-12 21:28:21 -0700690 final int desiredValue = currentPosition + (scrollToCurrentPosition ? currentPosition
691 : getTransparentViewHeight());
Brian Attwellbc3f2952014-07-07 16:05:50 -0700692 final ObjectAnimator animator = ObjectAnimator.ofInt(this, "scroll", bottomScrollPosition,
Brian Attwellf1076dc2014-08-12 21:28:21 -0700693 desiredValue);
Brian Attwellbc3f2952014-07-07 16:05:50 -0700694 animator.setInterpolator(interpolator);
Brian Attwellf1076dc2014-08-12 21:28:21 -0700695 animator.addUpdateListener(new AnimatorUpdateListener() {
696 @Override
697 public void onAnimationUpdate(ValueAnimator animation) {
698 if (animation.getAnimatedValue().equals(desiredValue) && mListener != null) {
699 mListener.onEntranceAnimationDone();
700 }
701 }
702 });
Brian Attwellbc3f2952014-07-07 16:05:50 -0700703 animator.start();
Brian Attwellc33ae172014-07-01 14:21:37 -0700704 }
705
Brian Attwellb7e43642014-06-02 14:33:04 -0700706 @Override
707 public void scrollTo(int x, int y) {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700708 final int delta = y - getScroll();
709 boolean wasFullscreen = getScrollNeededToBeFullScreen() <= 0;
Brian Attwellb7e43642014-06-02 14:33:04 -0700710 if (delta > 0) {
711 scrollUp(delta);
712 } else {
713 scrollDown(delta);
714 }
Brian Attwell019c71f2014-06-10 19:29:03 -0700715 updatePhotoTintAndDropShadow();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700716 updateHeaderTextSizeAndMargin();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700717 final boolean isFullscreen = getScrollNeededToBeFullScreen() <= 0;
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700718 mHasEverTouchedTheTop |= isFullscreen;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700719 if (mListener != null) {
720 if (wasFullscreen && !isFullscreen) {
721 mListener.onExitFullscreen();
722 } else if (!wasFullscreen && isFullscreen) {
723 mListener.onEnterFullscreen();
724 }
Brian Attwellf1076dc2014-08-12 21:28:21 -0700725 if (!isFullscreen || !wasFullscreen) {
726 mListener.onTransparentViewHeightChange(
727 getTransparentHeightRatio(getTransparentViewHeight()));
728 }
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700729 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700730 }
731
Brian Attwell64d3abc2014-06-12 17:08:12 -0700732 /**
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700733 * Change the height of the header/toolbar. Do *not* use this outside animations. This was
734 * designed for use by {@link #prepareForShrinkingScrollChild}.
735 */
736 @NeededForReflection
737 public void setToolbarHeight(int delta) {
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700738 final ViewGroup.LayoutParams toolbarLayoutParams
739 = mToolbar.getLayoutParams();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700740 toolbarLayoutParams.height = delta;
741 mToolbar.setLayoutParams(toolbarLayoutParams);
742
743 updatePhotoTintAndDropShadow();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700744 updateHeaderTextSizeAndMargin();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700745 }
746
747 @NeededForReflection
748 public int getToolbarHeight() {
749 return mToolbar.getLayoutParams().height;
750 }
751
752 /**
Brian Attwell64d3abc2014-06-12 17:08:12 -0700753 * Set the height of the toolbar and update its tint accordingly.
754 */
755 @NeededForReflection
756 public void setHeaderHeight(int height) {
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700757 final ViewGroup.LayoutParams toolbarLayoutParams
758 = mToolbar.getLayoutParams();
Brian Attwell64d3abc2014-06-12 17:08:12 -0700759 toolbarLayoutParams.height = height;
760 mToolbar.setLayoutParams(toolbarLayoutParams);
761 updatePhotoTintAndDropShadow();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700762 updateHeaderTextSizeAndMargin();
Brian Attwell64d3abc2014-06-12 17:08:12 -0700763 }
764
765 @NeededForReflection
766 public int getHeaderHeight() {
767 return mToolbar.getLayoutParams().height;
768 }
769
Brian Attwellb7e43642014-06-02 14:33:04 -0700770 @NeededForReflection
771 public void setScroll(int scroll) {
772 scrollTo(0, scroll);
773 }
774
775 /**
776 * Returns the total amount scrolled inside the nested ScrollView + the amount of shrinking
Brian Attwell64d3abc2014-06-12 17:08:12 -0700777 * performed on the ToolBar. This is the value inspected by animators.
Brian Attwellb7e43642014-06-02 14:33:04 -0700778 */
Brian Attwell64d3abc2014-06-12 17:08:12 -0700779 @NeededForReflection
Brian Attwellb7e43642014-06-02 14:33:04 -0700780 public int getScroll() {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700781 return mTransparentStartHeight - getTransparentViewHeight()
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700782 + getMaximumScrollableHeaderHeight() - getToolbarHeight()
Brian Attwellc33ae172014-07-01 14:21:37 -0700783 + mScrollView.getScrollY();
784 }
785
786 private int getMaximumScrollableHeaderHeight() {
787 return mIsOpenContactSquare ? mMaximumHeaderHeight : mIntermediateHeaderHeight;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700788 }
789
790 /**
791 * A variant of {@link #getScroll} that pretends the header is never larger than
792 * than mIntermediateHeaderHeight. This function is sometimes needed when making scrolling
793 * decisions that will not change the header size (ie, snapping to the bottom or top).
Brian Attwellc33ae172014-07-01 14:21:37 -0700794 *
795 * When mIsOpenContactSquare is true, this function considers mIntermediateHeaderHeight ==
796 * mMaximumHeaderHeight, since snapping decisions will be made relative the full header
797 * size when mIsOpenContactSquare = true.
798 *
799 * This value should never be used in conjunction with {@link #getScroll} values.
Brian Attwell64d3abc2014-06-12 17:08:12 -0700800 */
Brian Attwellc33ae172014-07-01 14:21:37 -0700801 private int getScroll_ignoreOversizedHeaderForSnapping() {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700802 return mTransparentStartHeight - getTransparentViewHeight()
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700803 + Math.max(getMaximumScrollableHeaderHeight() - getToolbarHeight(), 0)
Brian Attwell64d3abc2014-06-12 17:08:12 -0700804 + mScrollView.getScrollY();
Brian Attwellb7e43642014-06-02 14:33:04 -0700805 }
806
807 /**
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700808 * Amount of transparent space above the header/toolbar.
809 */
810 public int getScrollNeededToBeFullScreen() {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700811 return getTransparentViewHeight();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700812 }
813
814 /**
Brian Attwellb7e43642014-06-02 14:33:04 -0700815 * Return amount of scrolling needed in order for all the visible subviews to scroll off the
816 * bottom.
817 */
Brian Attwellc33ae172014-07-01 14:21:37 -0700818 private int getScrollUntilOffBottom() {
819 return getHeight() + getScroll_ignoreOversizedHeaderForSnapping()
820 - mTransparentStartHeight;
Brian Attwellb7e43642014-06-02 14:33:04 -0700821 }
822
823 @Override
824 public void computeScroll() {
825 if (mScroller.computeScrollOffset()) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800826 // Examine the fling results in order to activate EdgeEffect and halt flings.
Brian Attwellb7e43642014-06-02 14:33:04 -0700827 final int oldScroll = getScroll();
828 scrollTo(0, mScroller.getCurrY());
829 final int delta = mScroller.getCurrY() - oldScroll;
830 final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
831 if (delta > distanceFromMaxScrolling && distanceFromMaxScrolling > 0) {
832 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
833 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800834 if (mIsFullscreenDownwardsFling && getTransparentViewHeight() > 0) {
835 // Halt the fling once QuickContact's top is on screen.
836 scrollTo(0, getScroll() + getTransparentViewHeight());
837 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
838 mScroller.abortAnimation();
839 mIsFullscreenDownwardsFling = false;
840 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700841 if (!awakenScrollBars()) {
842 // Keep on drawing until the animation has finished.
843 postInvalidateOnAnimation();
844 }
845 if (mScroller.getCurrY() >= getMaximumScrollUpwards()) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800846 // Halt the fling once QuickContact's bottom is on screen.
Brian Attwellb7e43642014-06-02 14:33:04 -0700847 mScroller.abortAnimation();
Brian Attwell5fd86982014-12-04 16:25:33 -0800848 mIsFullscreenDownwardsFling = false;
Brian Attwellb7e43642014-06-02 14:33:04 -0700849 }
850 }
851 }
852
853 @Override
854 public void draw(Canvas canvas) {
855 super.draw(canvas);
856
Brian Attwell5fd86982014-12-04 16:25:33 -0800857 final int width = getWidth() - getPaddingLeft() - getPaddingRight();
858 final int height = getHeight();
859
Brian Attwellb7e43642014-06-02 14:33:04 -0700860 if (!mEdgeGlowBottom.isFinished()) {
861 final int restoreCount = canvas.save();
Brian Attwellb7e43642014-06-02 14:33:04 -0700862
863 // Draw the EdgeEffect on the bottom of the Window (Or a little bit below the bottom
864 // of the Window if we start to scroll upwards while EdgeEffect is visible). This
865 // does not need to consider the case where this MultiShrinkScroller doesn't fill
866 // the Window, since the nested ScrollView should be set to fillViewport.
Brian Attwell90689f22014-07-24 14:31:26 -0700867 canvas.translate(-width + getPaddingLeft(),
Brian Attwellb7e43642014-06-02 14:33:04 -0700868 height + getMaximumScrollUpwards() - getScroll());
869
870 canvas.rotate(180, width, 0);
Brian Attwell9b889e62014-06-23 18:25:32 -0700871 if (mIsTwoPanel) {
872 // Only show the EdgeEffect on the bottom of the ScrollView.
873 mEdgeGlowBottom.setSize(mScrollView.getWidth(), height);
Brian Attwellf1402272014-12-16 16:00:08 -0800874 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Brian Attwell90689f22014-07-24 14:31:26 -0700875 canvas.translate(mPhotoViewContainer.getWidth(), 0);
876 }
Brian Attwell9b889e62014-06-23 18:25:32 -0700877 } else {
878 mEdgeGlowBottom.setSize(width, height);
879 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700880 if (mEdgeGlowBottom.draw(canvas)) {
881 postInvalidateOnAnimation();
882 }
883 canvas.restoreToCount(restoreCount);
884 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800885
886 if (!mEdgeGlowTop.isFinished()) {
887 final int restoreCount = canvas.save();
888 if (mIsTwoPanel) {
889 mEdgeGlowTop.setSize(mScrollView.getWidth(), height);
Brian Attwellf1402272014-12-16 16:00:08 -0800890 if (getLayoutDirection() != View.LAYOUT_DIRECTION_RTL) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800891 canvas.translate(mPhotoViewContainer.getWidth(), 0);
892 }
893 } else {
894 mEdgeGlowTop.setSize(width, height);
895 }
896 if (mEdgeGlowTop.draw(canvas)) {
897 postInvalidateOnAnimation();
898 }
899 canvas.restoreToCount(restoreCount);
900 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700901 }
902
903 private float getCurrentVelocity() {
Brian Attwell8477eaf2014-06-18 15:39:50 -0700904 if (mVelocityTracker == null) {
905 return 0;
906 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700907 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumVelocity);
908 return mVelocityTracker.getYVelocity();
909 }
910
911 private void fling(float velocity) {
912 // For reasons I do not understand, scrolling is less janky when maxY=Integer.MAX_VALUE
913 // then when maxY is set to an actual value.
914 mScroller.fling(0, getScroll(), 0, (int) velocity, 0, 0, -Integer.MAX_VALUE,
915 Integer.MAX_VALUE);
Brian Attwell5fd86982014-12-04 16:25:33 -0800916 if (velocity < 0 && mTransparentView.getHeight() <= 0) {
917 mIsFullscreenDownwardsFling = true;
918 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700919 invalidate();
920 }
921
922 private int getMaximumScrollUpwards() {
Brian Attwell9b889e62014-06-23 18:25:32 -0700923 if (!mIsTwoPanel) {
924 return mTransparentStartHeight
925 // How much the Header view can compress
Brian Attwellc33ae172014-07-01 14:21:37 -0700926 + getMaximumScrollableHeaderHeight() - getFullyCompressedHeaderHeight()
Brian Attwell9b889e62014-06-23 18:25:32 -0700927 // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
928 + Math.max(0, mScrollViewChild.getHeight() - getHeight()
Brian Attwelle838a442014-06-27 15:53:13 -0700929 + getFullyCompressedHeaderHeight());
Brian Attwell9b889e62014-06-23 18:25:32 -0700930 } else {
931 return mTransparentStartHeight
932 // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
933 + Math.max(0, mScrollViewChild.getHeight() - getHeight());
934 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700935 }
936
Brian Attwell0d90afe2014-06-18 16:45:41 -0700937 private int getTransparentViewHeight() {
938 return mTransparentView.getLayoutParams().height;
939 }
940
941 private void setTransparentViewHeight(int height) {
942 mTransparentView.getLayoutParams().height = height;
943 mTransparentView.setLayoutParams(mTransparentView.getLayoutParams());
944 }
945
Brian Attwellb7e43642014-06-02 14:33:04 -0700946 private void scrollUp(int delta) {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700947 if (getTransparentViewHeight() != 0) {
948 final int originalValue = getTransparentViewHeight();
949 setTransparentViewHeight(getTransparentViewHeight() - delta);
950 setTransparentViewHeight(Math.max(0, getTransparentViewHeight()));
951 delta -= originalValue - getTransparentViewHeight();
Brian Attwellb7e43642014-06-02 14:33:04 -0700952 }
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700953 final ViewGroup.LayoutParams toolbarLayoutParams
954 = mToolbar.getLayoutParams();
Brian Attwelle838a442014-06-27 15:53:13 -0700955 if (toolbarLayoutParams.height > getFullyCompressedHeaderHeight()) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700956 final int originalValue = toolbarLayoutParams.height;
957 toolbarLayoutParams.height -= delta;
Brian Attwelle838a442014-06-27 15:53:13 -0700958 toolbarLayoutParams.height = Math.max(toolbarLayoutParams.height,
959 getFullyCompressedHeaderHeight());
Brian Attwellb7e43642014-06-02 14:33:04 -0700960 mToolbar.setLayoutParams(toolbarLayoutParams);
961 delta -= originalValue - toolbarLayoutParams.height;
962 }
963 mScrollView.scrollBy(0, delta);
964 }
965
Brian Attwelle838a442014-06-27 15:53:13 -0700966 /**
967 * Returns the minimum size that we want to compress the header to, given that we don't want to
968 * allow the the ScrollView to scroll unless there is new content off of the edge of ScrollView.
969 */
970 private int getFullyCompressedHeaderHeight() {
Brian Attwellf37ba162014-09-23 14:44:17 -0700971 return Math.min(Math.max(mToolbar.getLayoutParams().height - getOverflowingChildViewSize(),
Brian Attwell7d13d9c2014-09-08 18:25:58 -0700972 mMinimumHeaderHeight), getMaximumScrollableHeaderHeight());
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700973 }
974
975 /**
976 * Returns the amount of mScrollViewChild that doesn't fit inside its parent.
977 */
978 private int getOverflowingChildViewSize() {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700979 final int usedScrollViewSpace = mScrollViewChild.getHeight();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700980 return -getHeight() + usedScrollViewSpace + mToolbar.getLayoutParams().height;
Brian Attwelle838a442014-06-27 15:53:13 -0700981 }
982
Brian Attwellb7e43642014-06-02 14:33:04 -0700983 private void scrollDown(int delta) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700984 if (mScrollView.getScrollY() > 0) {
985 final int originalValue = mScrollView.getScrollY();
986 mScrollView.scrollBy(0, delta);
987 delta -= mScrollView.getScrollY() - originalValue;
988 }
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700989 final ViewGroup.LayoutParams toolbarLayoutParams = mToolbar.getLayoutParams();
Brian Attwellc33ae172014-07-01 14:21:37 -0700990 if (toolbarLayoutParams.height < getMaximumScrollableHeaderHeight()) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700991 final int originalValue = toolbarLayoutParams.height;
992 toolbarLayoutParams.height -= delta;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700993 toolbarLayoutParams.height = Math.min(toolbarLayoutParams.height,
Brian Attwellc33ae172014-07-01 14:21:37 -0700994 getMaximumScrollableHeaderHeight());
Brian Attwellb7e43642014-06-02 14:33:04 -0700995 mToolbar.setLayoutParams(toolbarLayoutParams);
996 delta -= originalValue - toolbarLayoutParams.height;
997 }
Brian Attwell0d90afe2014-06-18 16:45:41 -0700998 setTransparentViewHeight(getTransparentViewHeight() - delta);
Brian Attwellb7e43642014-06-02 14:33:04 -0700999
Brian Attwell8477eaf2014-06-18 15:39:50 -07001000 if (getScrollUntilOffBottom() <= 0) {
Brian Attwellb7e43642014-06-02 14:33:04 -07001001 post(new Runnable() {
1002 @Override
1003 public void run() {
Brian Attwell8477eaf2014-06-18 15:39:50 -07001004 if (mListener != null) {
1005 mListener.onScrolledOffBottom();
1006 // No other messages need to be sent to the listener.
1007 mListener = null;
1008 }
Brian Attwellb7e43642014-06-02 14:33:04 -07001009 }
1010 });
1011 }
1012 }
1013
Brian Attwell9b889e62014-06-23 18:25:32 -07001014 /**
1015 * Set the header size and padding, based on the current scroll position.
1016 */
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001017 private void updateHeaderTextSizeAndMargin() {
Brian Attwell9b889e62014-06-23 18:25:32 -07001018 if (mIsTwoPanel) {
Brian Attwell2c47b342014-11-13 00:34:32 +00001019 // The text size stays at a constant size & location in two panel layouts.
Brian Attwell9b889e62014-06-23 18:25:32 -07001020 return;
1021 }
1022
1023 // The pivot point for scaling should be middle of the starting side.
Brian Attwellf1402272014-12-16 16:00:08 -08001024 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Tingting Wangdf65d162015-07-24 17:04:35 -07001025 mTitleAndPhoneticNameView.setPivotX(mTitleAndPhoneticNameView.getWidth());
Brian Attwell9b889e62014-06-23 18:25:32 -07001026 } else {
Tingting Wangdf65d162015-07-24 17:04:35 -07001027 mTitleAndPhoneticNameView.setPivotX(0);
Brian Attwell9b889e62014-06-23 18:25:32 -07001028 }
Tingting Wangdf65d162015-07-24 17:04:35 -07001029 mTitleAndPhoneticNameView.setPivotY(mTitleAndPhoneticNameView.getHeight() / 2);
Brian Attwell9b889e62014-06-23 18:25:32 -07001030
Brian Attwell7a47bb52014-07-31 11:19:46 -07001031 final int toolbarHeight = mToolbar.getLayoutParams().height;
Brian Attwellf37ba162014-09-23 14:44:17 -07001032 mPhotoTouchInterceptOverlay.setClickable(toolbarHeight != mMaximumHeaderHeight);
1033
Brian Attwell7a47bb52014-07-31 11:19:46 -07001034 if (toolbarHeight >= mMaximumHeaderHeight) {
1035 // Everything is full size when the header is fully expanded.
Tingting Wangdf65d162015-07-24 17:04:35 -07001036 mTitleAndPhoneticNameView.setScaleX(1);
1037 mTitleAndPhoneticNameView.setScaleY(1);
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001038 setInterpolatedTitleMargins(1);
Brian Attwell9b889e62014-06-23 18:25:32 -07001039 return;
1040 }
Brian Attwell7a47bb52014-07-31 11:19:46 -07001041
Brian Attwell15195ee2014-08-06 16:51:33 -07001042 final float ratio = (toolbarHeight - mMinimumHeaderHeight)
Brian Attwell7a47bb52014-07-31 11:19:46 -07001043 / (float)(mMaximumHeaderHeight - mMinimumHeaderHeight);
Brian Attwell9b889e62014-06-23 18:25:32 -07001044 final float minimumSize = mInvisiblePlaceholderTextView.getHeight();
Brian Attwell15195ee2014-08-06 16:51:33 -07001045 float bezierOutput = mTextSizePathInterpolator.getInterpolation(ratio);
Brian Attwell7a47bb52014-07-31 11:19:46 -07001046 float scale = (minimumSize + (mMaximumHeaderTextSize - minimumSize) * bezierOutput)
Brian Attwell9b889e62014-06-23 18:25:32 -07001047 / mMaximumHeaderTextSize;
1048
Brian Attwell15195ee2014-08-06 16:51:33 -07001049 // Clamp to reasonable/finite values before passing into framework. The values
1050 // can be wacky before the first pre-render.
1051 bezierOutput = (float) Math.min(bezierOutput, 1.0f);
1052 scale = (float) Math.min(scale, 1.0f);
1053
Tingting Wangdf65d162015-07-24 17:04:35 -07001054 mTitleAndPhoneticNameView.setScaleX(scale);
1055 mTitleAndPhoneticNameView.setScaleY(scale);
Brian Attwell7a47bb52014-07-31 11:19:46 -07001056 setInterpolatedTitleMargins(bezierOutput);
Brian Attwell9b889e62014-06-23 18:25:32 -07001057 }
1058
1059 /**
Tingting Wangdf65d162015-07-24 17:04:35 -07001060 * Calculate the padding around mTitleAndPhoneticNameView so that it will look appropriate once it
Brian Attwell9b889e62014-06-23 18:25:32 -07001061 * finishes moving into its target location/size.
1062 */
Brian Attwellbe48e0c2014-06-25 10:31:59 -07001063 private void calculateCollapsedLargeTitlePadding() {
Brian Attwell2a3bf462014-12-16 17:24:15 -08001064 int invisiblePlaceHolderLocation[] = new int[2];
1065 int largeTextViewRectLocation[] = new int[2];
1066 mInvisiblePlaceholderTextView.getLocationOnScreen(invisiblePlaceHolderLocation);
1067 mToolbar.getLocationOnScreen(largeTextViewRectLocation);
Brian Attwell9b889e62014-06-23 18:25:32 -07001068 // Distance between top of toolbar to the center of the target rectangle.
Brian Attwell2a3bf462014-12-16 17:24:15 -08001069 final int desiredTopToCenter = invisiblePlaceHolderLocation[1]
1070 + mInvisiblePlaceholderTextView.getHeight() / 2
1071 - largeTextViewRectLocation[1];
Tingting Wangdf65d162015-07-24 17:04:35 -07001072 // Padding needed on the mTitleAndPhoneticNameView so that it has the same amount of
Brian Attwell9b889e62014-06-23 18:25:32 -07001073 // padding as the target rectangle.
Tingting Wangdf65d162015-07-24 17:04:35 -07001074 mCollapsedTitleBottomMargin =
1075 desiredTopToCenter - mTitleAndPhoneticNameView.getHeight() / 2;
Brian Attwellbe48e0c2014-06-25 10:31:59 -07001076 }
Brian Attwell9b889e62014-06-23 18:25:32 -07001077
Brian Attwellbe48e0c2014-06-25 10:31:59 -07001078 /**
1079 * Interpolate the title's margin size. When {@param x}=1, use the maximum title margins.
1080 * When {@param x}=0, use the margin values taken from {@link #mInvisiblePlaceholderTextView}.
1081 */
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001082 private void setInterpolatedTitleMargins(float x) {
1083 final FrameLayout.LayoutParams titleLayoutParams
Tingting Wangdf65d162015-07-24 17:04:35 -07001084 = (FrameLayout.LayoutParams) mTitleAndPhoneticNameView.getLayoutParams();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001085 final LinearLayout.LayoutParams toolbarLayoutParams
1086 = (LinearLayout.LayoutParams) mToolbar.getLayoutParams();
Paul Soulos1deb5322014-08-28 18:47:42 -07001087
1088 // Need to add more to margin start if there is a start column
1089 int startColumnWidth = mStartColumn == null ? 0 : mStartColumn.getWidth();
1090
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001091 titleLayoutParams.setMarginStart((int) (mCollapsedTitleStartMargin * (1 - x)
Paul Soulos1deb5322014-08-28 18:47:42 -07001092 + mMaximumTitleMargin * x) + startColumnWidth);
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001093 // How offset the title should be from the bottom of the toolbar
1094 final int pretendBottomMargin = (int) (mCollapsedTitleBottomMargin * (1 - x)
1095 + mMaximumTitleMargin * x) ;
Brian Attwell77b4a592014-08-13 12:22:36 -07001096 // Calculate how offset the title should be from the top of the screen. Instead of
Tingting Wangdf65d162015-07-24 17:04:35 -07001097 // calling mTitleAndPhoneticNameView.getHeight() use the mMaximumHeaderTextSize for this
1098 // calculation. The getHeight() value acts unexpectedly when mTitleAndPhoneticNameView is
1099 // partially clipped by its parent.
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001100 titleLayoutParams.topMargin = getTransparentViewHeight()
1101 + toolbarLayoutParams.height - pretendBottomMargin
Brian Attwell77b4a592014-08-13 12:22:36 -07001102 - mMaximumHeaderTextSize;
1103 titleLayoutParams.bottomMargin = 0;
Tingting Wangdf65d162015-07-24 17:04:35 -07001104 mTitleAndPhoneticNameView.setLayoutParams(titleLayoutParams);
Brian Attwell9b889e62014-06-23 18:25:32 -07001105 }
1106
Brian Attwell019c71f2014-06-10 19:29:03 -07001107 private void updatePhotoTintAndDropShadow() {
Brian Attwell7609b7d2014-11-19 18:01:29 -08001108 // Let's keep an eye on how long this method takes to complete.
Brian Attwelld7980782014-06-24 18:52:26 -07001109 Trace.beginSection("updatePhotoTintAndDropShadow");
1110
Brian Attwelle48a3b52014-07-22 17:11:34 -07001111 if (mIsTwoPanel && !mPhotoView.isBasedOffLetterTile()) {
1112 // When in two panel mode, UX considers photo tinting unnecessary for non letter
1113 // tile photos.
1114 mTitleGradientDrawable.setAlpha(0xFF);
1115 mActionBarGradientDrawable.setAlpha(0xFF);
1116 return;
1117 }
1118
Brian Attwelldcb938f2014-06-03 23:05:59 -07001119 // We need to use toolbarLayoutParams to determine the height, since the layout
1120 // params can be updated before the height change is reflected inside the View#getHeight().
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001121 final int toolbarHeight = getToolbarHeight();
Brian Attwell9b889e62014-06-23 18:25:32 -07001122
1123 if (toolbarHeight <= mMinimumHeaderHeight && !mIsTwoPanel) {
Wenyi Wang4c3d3e22015-12-17 14:30:02 -08001124 ViewCompat.setElevation(mPhotoViewContainer, mToolbarElevation);
Brian Attwell9b889e62014-06-23 18:25:32 -07001125 } else {
Wenyi Wang4c3d3e22015-12-17 14:30:02 -08001126 ViewCompat.setElevation(mPhotoViewContainer, 0);
Brian Attwell9b889e62014-06-23 18:25:32 -07001127 }
1128
Brian Attwelldcb938f2014-06-03 23:05:59 -07001129 // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
1130 mPhotoView.clearColorFilter();
Brian Attwell9b889e62014-06-23 18:25:32 -07001131 mColorMatrix.reset();
Brian Attwell9b889e62014-06-23 18:25:32 -07001132
Brian Attwell7609b7d2014-11-19 18:01:29 -08001133 final int gradientAlpha;
1134 if (!mPhotoView.isBasedOffLetterTile()) {
1135 // Constants and equations were arbitrarily picked to choose values for saturation,
1136 // whiteness, tint and gradient alpha. There were four main objectives:
1137 // 1) The transition period between the unmodified image and fully colored image should
1138 // be very short.
1139 // 2) The tinting should be fully applied even before the background image is fully
1140 // faded out and desaturated. Why? A half tinted photo looks bad and results in
1141 // unappealing colors.
1142 // 3) The function should have a derivative of 0 at ratio = 1 to avoid discontinuities.
1143 // 4) The entire process should look awesome.
1144 final float ratio = calculateHeightRatioToBlendingStartHeight(toolbarHeight);
1145 final float alpha = 1.0f - (float) Math.min(Math.pow(ratio, 1.5f) * 2f, 1f);
1146 final float tint = (float) Math.min(Math.pow(ratio, 1.5f) * 3f, 1f);
1147 mColorMatrix.setSaturation(alpha);
1148 mColorMatrix.postConcat(alphaMatrix(alpha, Color.WHITE));
1149 mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, tint));
1150 gradientAlpha = (int) (255 * alpha);
1151 } else if (mIsTwoPanel) {
1152 mColorMatrix.reset();
1153 mColorMatrix.postConcat(alphaMatrix(DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA,
1154 mHeaderTintColor));
1155 gradientAlpha = 0;
Brian Attwell9b889e62014-06-23 18:25:32 -07001156 } else {
Brian Attwell7609b7d2014-11-19 18:01:29 -08001157 // We want a function that has DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA value
1158 // at the intermediate position and uses TILE_EXPONENT. Finding an equation
1159 // that satisfies this condition requires the following arithmetic.
1160 final float ratio = calculateHeightRatioToFullyOpen(toolbarHeight);
1161 final float intermediateRatio = calculateHeightRatioToFullyOpen((int)
1162 (mMaximumPortraitHeaderHeight * INTERMEDIATE_HEADER_HEIGHT_RATIO));
1163 final float TILE_EXPONENT = 3f;
1164 final float slowingFactor = (float) ((1 - intermediateRatio) / intermediateRatio
1165 / (1 - Math.pow(1 - DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA, 1/TILE_EXPONENT)));
1166 float linearBeforeIntermediate = Math.max(1 - (1 - ratio) / intermediateRatio
1167 / slowingFactor, 0);
1168 float colorAlpha = 1 - (float) Math.pow(linearBeforeIntermediate, TILE_EXPONENT);
1169 mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor));
1170 gradientAlpha = 0;
Brian Attwelldcb938f2014-06-03 23:05:59 -07001171 }
Brian Attwell9b889e62014-06-23 18:25:32 -07001172
Brian Attwell7609b7d2014-11-19 18:01:29 -08001173 // TODO: remove re-allocation of ColorMatrixColorFilter objects (b/17627000)
Chris Craikc95fbc92014-09-19 09:59:10 -07001174 mPhotoView.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
Brian Attwell7609b7d2014-11-19 18:01:29 -08001175
Brian Attwell9b889e62014-06-23 18:25:32 -07001176 // Tell the photo view what tint we are trying to achieve. Depending on the type of
1177 // drawable used, the photo view may or may not use this tint.
Brian Attwelld7980782014-06-24 18:52:26 -07001178 mPhotoView.setTint(mHeaderTintColor);
Brian Attwell4936a922014-07-02 16:16:59 -07001179 mTitleGradientDrawable.setAlpha(gradientAlpha);
1180 mActionBarGradientDrawable.setAlpha(gradientAlpha);
1181
Brian Attwelld7980782014-06-24 18:52:26 -07001182 Trace.endSection();
1183 }
1184
Brian Attwell7609b7d2014-11-19 18:01:29 -08001185 private float calculateHeightRatioToFullyOpen(int height) {
Brian Attwelld7980782014-06-24 18:52:26 -07001186 return (height - mMinimumPortraitHeaderHeight)
1187 / (float) (mMaximumPortraitHeaderHeight - mMinimumPortraitHeaderHeight);
Brian Attwell9b889e62014-06-23 18:25:32 -07001188 }
1189
Brian Attwell7609b7d2014-11-19 18:01:29 -08001190 private float calculateHeightRatioToBlendingStartHeight(int height) {
1191 final float intermediateHeight = mMaximumPortraitHeaderHeight
1192 * COLOR_BLENDING_START_RATIO;
1193 final float interpolatingHeightRange = intermediateHeight - mMinimumPortraitHeaderHeight;
1194 if (height > intermediateHeight) {
1195 return 0;
1196 }
1197 return (intermediateHeight - height) / interpolatingHeightRange;
1198 }
1199
Brian Attwell9b889e62014-06-23 18:25:32 -07001200 /**
1201 * Simulates alpha blending an image with {@param color}.
1202 */
1203 private ColorMatrix alphaMatrix(float alpha, int color) {
1204 mAlphaMatrixValues[0] = Color.red(color) * alpha / 255;
1205 mAlphaMatrixValues[6] = Color.green(color) * alpha / 255;
1206 mAlphaMatrixValues[12] = Color.blue(color) * alpha / 255;
1207 mAlphaMatrixValues[4] = 255 * (1 - alpha);
1208 mAlphaMatrixValues[9] = 255 * (1 - alpha);
1209 mAlphaMatrixValues[14] = 255 * (1 - alpha);
1210 mWhitenessColorMatrix.set(mAlphaMatrixValues);
1211 return mWhitenessColorMatrix;
1212 }
1213
1214 /**
1215 * Simulates multiply blending an image with a single {@param color}.
1216 *
1217 * Multiply blending is [Sa * Da, Sc * Dc]. See {@link android.graphics.PorterDuff}.
1218 */
1219 private ColorMatrix multiplyBlendMatrix(int color, float alpha) {
1220 mMultiplyBlendMatrixValues[0] = multiplyBlend(Color.red(color), alpha);
1221 mMultiplyBlendMatrixValues[6] = multiplyBlend(Color.green(color), alpha);
1222 mMultiplyBlendMatrixValues[12] = multiplyBlend(Color.blue(color), alpha);
1223 mMultiplyBlendMatrix.set(mMultiplyBlendMatrixValues);
1224 return mMultiplyBlendMatrix;
1225 }
1226
1227 private float multiplyBlend(int color, float alpha) {
1228 return color * alpha / 255.0f + (1 - alpha);
Brian Attwelldcb938f2014-06-03 23:05:59 -07001229 }
1230
Brian Attwellb7e43642014-06-02 14:33:04 -07001231 private void updateLastEventPosition(MotionEvent event) {
1232 mLastEventPosition[0] = event.getX();
1233 mLastEventPosition[1] = event.getY();
1234 }
1235
1236 private boolean motionShouldStartDrag(MotionEvent event) {
Brian Attwellb7e43642014-06-02 14:33:04 -07001237 final float deltaY = event.getY() - mLastEventPosition[1];
Brian Attwellbf2854d2014-10-08 16:12:11 -07001238 return deltaY > mTouchSlop || deltaY < -mTouchSlop;
Brian Attwellb7e43642014-06-02 14:33:04 -07001239 }
1240
1241 private float updatePositionAndComputeDelta(MotionEvent event) {
1242 final int VERTICAL = 1;
1243 final float position = mLastEventPosition[VERTICAL];
1244 updateLastEventPosition(event);
Brian Attwell5fd86982014-12-04 16:25:33 -08001245 float elasticityFactor = 1;
1246 if (position < mLastEventPosition[VERTICAL] && mHasEverTouchedTheTop) {
1247 // As QuickContacts is dragged from the top of the window, its rate of movement will
1248 // slow down in proportion to its distance from the top. This will feel springy.
1249 elasticityFactor += mTransparentView.getHeight() * SPRING_DAMPENING_FACTOR;
1250 }
1251 return (position - mLastEventPosition[VERTICAL]) / elasticityFactor;
Brian Attwellb7e43642014-06-02 14:33:04 -07001252 }
1253
1254 private void smoothScrollBy(int delta) {
Brian Attwell64d3abc2014-06-12 17:08:12 -07001255 if (delta == 0) {
1256 // Delta=0 implies the code calling smoothScrollBy is sloppy. We should avoid doing
1257 // this, since it prevents Views from being able to register any clicks for 250ms.
1258 throw new IllegalArgumentException("Smooth scrolling by delta=0 is "
1259 + "pointless and harmful");
1260 }
Brian Attwellb7e43642014-06-02 14:33:04 -07001261 mScroller.startScroll(0, getScroll(), 0, delta);
1262 invalidate();
1263 }
Brian Attwellb442dc72014-06-18 11:58:23 -07001264
1265 /**
1266 * Interpolator that enforces a specific starting velocity. This is useful to avoid a
1267 * discontinuity between dragging speed and flinging speed.
1268 *
1269 * Similar to a {@link android.view.animation.AccelerateInterpolator} in the sense that
1270 * getInterpolation() is a quadratic function.
1271 */
Brian Attwell2a3bf462014-12-16 17:24:15 -08001272 private class AcceleratingFlingInterpolator implements Interpolator {
Brian Attwellb442dc72014-06-18 11:58:23 -07001273
1274 private final float mStartingSpeedPixelsPerFrame;
1275 private final float mDurationMs;
1276 private final int mPixelsDelta;
1277 private final float mNumberFrames;
1278
1279 public AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond,
1280 int pixelsDelta) {
1281 mStartingSpeedPixelsPerFrame = startingSpeedPixelsPerSecond / getRefreshRate();
1282 mDurationMs = durationMs;
1283 mPixelsDelta = pixelsDelta;
1284 mNumberFrames = mDurationMs / getFrameIntervalMs();
1285 }
1286
1287 @Override
1288 public float getInterpolation(float input) {
1289 final float animationIntervalNumber = mNumberFrames * input;
1290 final float linearDelta = (animationIntervalNumber * mStartingSpeedPixelsPerFrame)
1291 / mPixelsDelta;
1292 // Add the results of a linear interpolator (with the initial speed) with the
1293 // results of a AccelerateInterpolator.
1294 if (mStartingSpeedPixelsPerFrame > 0) {
1295 return Math.min(input * input + linearDelta, 1);
1296 } else {
1297 // Initial fling was in the wrong direction, make sure that the quadratic component
1298 // grows faster in order to make up for this.
1299 return Math.min(input * (input - linearDelta) + linearDelta, 1);
1300 }
1301 }
1302
1303 private float getRefreshRate() {
Brian Attwell2a3bf462014-12-16 17:24:15 -08001304 final DisplayManager displayManager = (DisplayManager) MultiShrinkScroller
1305 .this.getContext().getSystemService(Context.DISPLAY_SERVICE);
1306 return displayManager.getDisplay(Display.DEFAULT_DISPLAY).getRefreshRate();
Brian Attwellb442dc72014-06-18 11:58:23 -07001307 }
1308
1309 public long getFrameIntervalMs() {
1310 return (long)(1000 / getRefreshRate());
1311 }
1312 }
Brian Attwelle8ce6ee2014-06-27 18:26:32 -07001313
1314 /**
1315 * Expand the header if the mScrollViewChild is about to shrink by enough to create new empty
1316 * space at the bottom of this ViewGroup.
1317 */
1318 public void prepareForShrinkingScrollChild(int heightDelta) {
1319 final int newEmptyScrollViewSpace = -getOverflowingChildViewSize() + heightDelta;
1320 if (newEmptyScrollViewSpace > 0 && !mIsTwoPanel) {
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001321 final int newDesiredToolbarHeight = Math.min(getToolbarHeight()
Brian Attwellc33ae172014-07-01 14:21:37 -07001322 + newEmptyScrollViewSpace, getMaximumScrollableHeaderHeight());
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001323 ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).setDuration(
1324 ExpandingEntryCardView.DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS).start();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -07001325 }
1326 }
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001327
Brian Attwell245d3d22015-01-21 09:50:08 -08001328 /**
1329 * If {@param areTouchesDisabled} is TRUE, ignore all of the user's touches.
1330 */
1331 public void setDisableTouchesForSuppressLayout(boolean areTouchesDisabled) {
1332 // The card expansion animation uses the Transition framework's ChangeBounds API. This
1333 // invokes suppressLayout(true) on the MultiShrinkScroller. As a result, we need to avoid
1334 // all layout changes during expansion in order to avoid weird layout artifacts.
1335 mIsTouchDisabledForSuppressLayout = areTouchesDisabled;
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001336 }
Brian Attwellb7e43642014-06-02 14:33:04 -07001337}