blob: b86bebf9d65900a579939e99bebe04585d6274a6 [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;
Wenyi Wangc85af282015-12-21 15:55:32 -08005import com.android.contacts.compat.EdgeEffectCompat;
Paul Soulos0cda9ae2014-07-23 11:27:28 -07006import com.android.contacts.quickcontact.ExpandingEntryCardView;
Brian Attwellb7e43642014-06-02 14:33:04 -07007import com.android.contacts.test.NeededForReflection;
Brian Attwell64d3abc2014-06-12 17:08:12 -07008import com.android.contacts.util.SchedulingUtils;
Brian Attwellb7e43642014-06-02 14:33:04 -07009
Brian Attwell64d3abc2014-06-12 17:08:12 -070010import android.animation.Animator;
11import android.animation.Animator.AnimatorListener;
Brian Attwellb442dc72014-06-18 11:58:23 -070012import android.animation.AnimatorListenerAdapter;
Brian Attwellb7e43642014-06-02 14:33:04 -070013import android.animation.ObjectAnimator;
Brian Attwellf1076dc2014-08-12 21:28:21 -070014import android.animation.ValueAnimator;
15import android.animation.ValueAnimator.AnimatorUpdateListener;
Brian Attwellb7e43642014-06-02 14:33:04 -070016import android.content.Context;
Brian Attwell64d3abc2014-06-12 17:08:12 -070017import android.content.res.TypedArray;
Brian Attwellb7e43642014-06-02 14:33:04 -070018import android.graphics.Canvas;
Brian Attwell9b889e62014-06-23 18:25:32 -070019import android.graphics.Color;
20import android.graphics.ColorMatrix;
21import android.graphics.ColorMatrixColorFilter;
Brian Attwell4936a922014-07-02 16:16:59 -070022import android.graphics.drawable.GradientDrawable;
Brian Attwell2a3bf462014-12-16 17:24:15 -080023import android.hardware.display.DisplayManager;
Brian Attwelld7980782014-06-24 18:52:26 -070024import android.os.Trace;
Wenyi Wang4c3d3e22015-12-17 14:30:02 -080025import android.support.v4.view.ViewCompat;
26import android.support.v4.view.animation.PathInterpolatorCompat;
Brian Attwellb7e43642014-06-02 14:33:04 -070027import android.util.AttributeSet;
Brian Attwell9b889e62014-06-23 18:25:32 -070028import android.util.TypedValue;
Brian Attwellb442dc72014-06-18 11:58:23 -070029import android.view.Display;
Brian Attwell0d3fd2e2014-07-30 22:23:34 -070030import android.view.Gravity;
Brian Attwellb7e43642014-06-02 14:33:04 -070031import android.view.MotionEvent;
32import android.view.VelocityTracker;
33import android.view.View;
34import android.view.ViewGroup;
35import android.view.ViewConfiguration;
Brian Attwellbc3f2952014-07-07 16:05:50 -070036import android.view.animation.AnimationUtils;
Brian Attwellb7e43642014-06-02 14:33:04 -070037import android.view.animation.Interpolator;
38import android.widget.EdgeEffect;
Brian Attwell9b889e62014-06-23 18:25:32 -070039import android.widget.FrameLayout;
Brian Attwellb7e43642014-06-02 14:33:04 -070040import android.widget.LinearLayout;
41import android.widget.Scroller;
42import android.widget.ScrollView;
Brian Attwell9b889e62014-06-23 18:25:32 -070043import android.widget.TextView;
Brian Attwell1c806542014-10-22 14:02:42 -070044import android.widget.Toolbar;
Brian Attwellb7e43642014-06-02 14:33:04 -070045
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.
Brian Attwell7267ee22014-08-20 14:20:20 -070058 *
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
Brian Attwell5fd86982014-12-04 16:25:33 -080063 * 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.
Brian Attwellb7e43642014-06-02 14:33:04 -070065 */
Brian Attwell0d3fd2e2014-07-30 22:23:34 -070066public class MultiShrinkScroller extends FrameLayout {
Brian Attwellb7e43642014-06-02 14:33:04 -070067
68 /**
Wenyi Wangcaf26192016-05-09 15:00:25 -070069 * 1000 pixels per second. Ie, 1 pixel per millisecond.
Brian Attwellb7e43642014-06-02 14:33:04 -070070 */
71 private static final int PIXELS_PER_SECOND = 1000;
72
Brian Attwellb442dc72014-06-18 11:58:23 -070073 /**
74 * Length of the acceleration animations. This value was taken from ValueAnimator.java.
75 */
Brian Attwell5fd86982014-12-04 16:25:33 -080076 private static final int EXIT_FLING_ANIMATION_DURATION_MS = 250;
Brian Attwellb442dc72014-06-18 11:58:23 -070077
Brian Attwell9b889e62014-06-23 18:25:32 -070078 /**
79 * In portrait mode, the height:width ratio of the photo's starting height.
80 */
Brian Attwell7609b7d2014-11-19 18:01:29 -080081 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
Brian Attwell5fd86982014-12-04 16:25:33 -080089 private static final float SPRING_DAMPENING_FACTOR = 0.01f;
90
Brian Attwell7609b7d2014-11-19 18:01:29 -080091 /**
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;
Brian Attwell9b889e62014-06-23 18:25:32 -070096
Brian Attwellb7e43642014-06-02 14:33:04 -070097 private float[] mLastEventPosition = { 0, 0 };
98 private VelocityTracker mVelocityTracker;
99 private boolean mIsBeingDragged = false;
100 private boolean mReceivedDown = false;
Brian Attwell5fd86982014-12-04 16:25:33 -0800101 /**
102 * Did the current downwards fling/scroll-animation start while we were fullscreen?
103 */
104 private boolean mIsFullscreenDownwardsFling = false;
Brian Attwellb7e43642014-06-02 14:33:04 -0700105
106 private ScrollView mScrollView;
107 private View mScrollViewChild;
108 private View mToolbar;
Brian Attwell9b889e62014-06-23 18:25:32 -0700109 private QuickContactImageView mPhotoView;
Brian Attwell019c71f2014-06-10 19:29:03 -0700110 private View mPhotoViewContainer;
Brian Attwell0d90afe2014-06-18 16:45:41 -0700111 private View mTransparentView;
Brian Attwellb7e43642014-06-02 14:33:04 -0700112 private MultiShrinkScrollerListener mListener;
Brian Attwell9b889e62014-06-23 18:25:32 -0700113 private TextView mLargeTextView;
Tingting Wangdf65d162015-07-24 17:04:35 -0700114 private TextView mPhoneticNameView;
115 private View mTitleAndPhoneticNameView;
Brian Attwelled6cda62014-06-26 17:13:14 -0700116 private View mPhotoTouchInterceptOverlay;
Brian Attwell1c806542014-10-22 14:02:42 -0700117 /** Contains desired size & vertical offset of the title, once the header is fully compressed */
Brian Attwell9b889e62014-06-23 18:25:32 -0700118 private TextView mInvisiblePlaceholderTextView;
Brian Attwell4936a922014-07-02 16:16:59 -0700119 private View mTitleGradientView;
120 private View mActionBarGradientView;
Paul Soulos1deb5322014-08-28 18:47:42 -0700121 private View mStartColumn;
Brian Attwell31b2d422014-06-05 00:14:58 -0700122 private int mHeaderTintColor;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700123 private int mMaximumHeaderHeight;
Brian Attwell9b889e62014-06-23 18:25:32 -0700124 private int mMinimumHeaderHeight;
Brian Attwellc33ae172014-07-01 14:21:37 -0700125 /**
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 */
Brian Attwell9b889e62014-06-23 18:25:32 -0700131 private int mIntermediateHeaderHeight;
Brian Attwellc33ae172014-07-01 14:21:37 -0700132 /**
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;
Brian Attwell9b889e62014-06-23 18:25:32 -0700137 private int mMaximumHeaderTextSize;
Wenyi Wangcaf26192016-05-09 15:00:25 -0700138 private int mMaximumPhoneticNameViewHeight;
139 private int mMaximumFullNameViewHeight;
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700140 private int mCollapsedTitleBottomMargin;
141 private int mCollapsedTitleStartMargin;
Brian Attwelld7980782014-06-24 18:52:26 -0700142 private int mMinimumPortraitHeaderHeight;
143 private int mMaximumPortraitHeaderHeight;
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700144 /**
145 * True once the header has touched the top of the screen at least once.
146 */
147 private boolean mHasEverTouchedTheTop;
Brian Attwell5fd86982014-12-04 16:25:33 -0800148 private boolean mIsTouchDisabledForDismissAnimation;
Brian Attwell245d3d22015-01-21 09:50:08 -0800149 private boolean mIsTouchDisabledForSuppressLayout;
Brian Attwellb7e43642014-06-02 14:33:04 -0700150
151 private final Scroller mScroller;
152 private final EdgeEffect mEdgeGlowBottom;
Brian Attwell5fd86982014-12-04 16:25:33 -0800153 private final EdgeEffect mEdgeGlowTop;
Brian Attwellb7e43642014-06-02 14:33:04 -0700154 private final int mTouchSlop;
155 private final int mMaximumVelocity;
156 private final int mMinimumVelocity;
Brian Attwell5fd86982014-12-04 16:25:33 -0800157 private final int mDismissDistanceOnScroll;
158 private final int mDismissDistanceOnRelease;
159 private final int mSnapToTopSlopHeight;
Brian Attwellb7e43642014-06-02 14:33:04 -0700160 private final int mTransparentStartHeight;
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700161 private final int mMaximumTitleMargin;
Brian Attwell019c71f2014-06-10 19:29:03 -0700162 private final float mToolbarElevation;
Brian Attwell9b889e62014-06-23 18:25:32 -0700163 private final boolean mIsTwoPanel;
Brian Attwell841c3332014-10-27 15:05:09 -0700164 private final float mLandscapePhotoRatio;
Brian Attwelle48a3b52014-07-22 17:11:34 -0700165 private final int mActionBarSize;
Brian Attwell9b889e62014-06-23 18:25:32 -0700166
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();
Brian Attwell9b889e62014-06-23 18:25:32 -0700170 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 };
Brian Attwellb7e43642014-06-02 14:33:04 -0700184
Wenyi Wang4c3d3e22015-12-17 14:30:02 -0800185 private final Interpolator mTextSizePathInterpolator =
186 PathInterpolatorCompat.create(0.16f, 0.4f, 0.2f, 1);
Brian Attwell7a47bb52014-07-31 11:19:46 -0700187
Brian Attwell7609b7d2014-11-19 18:01:29 -0800188 private final int[] mGradientColors = new int[] {0,0x88000000};
Brian Attwell4936a922014-07-02 16:16:59 -0700189 private GradientDrawable mTitleGradientDrawable = new GradientDrawable(
190 GradientDrawable.Orientation.TOP_BOTTOM, mGradientColors);
191 private GradientDrawable mActionBarGradientDrawable = new GradientDrawable(
192 GradientDrawable.Orientation.BOTTOM_TOP, mGradientColors);
193
Brian Attwellb7e43642014-06-02 14:33:04 -0700194 public interface MultiShrinkScrollerListener {
195 void onScrolledOffBottom();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700196
Brian Attwell8477eaf2014-06-18 15:39:50 -0700197 void onStartScrollOffBottom();
198
Brian Attwellf1076dc2014-08-12 21:28:21 -0700199 void onTransparentViewHeightChange(float ratio);
200
201 void onEntranceAnimationDone();
202
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700203 void onEnterFullscreen();
204
205 void onExitFullscreen();
Brian Attwellb7e43642014-06-02 14:33:04 -0700206 }
207
Brian Attwellb442dc72014-06-18 11:58:23 -0700208 private final AnimatorListener mSnapToBottomListener = new AnimatorListenerAdapter() {
Brian Attwell64d3abc2014-06-12 17:08:12 -0700209 @Override
Brian Attwellb442dc72014-06-18 11:58:23 -0700210 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();
Brian Attwell8477eaf2014-06-18 15:39:50 -0700215 // No other messages need to be sent to the listener.
216 mListener = null;
Brian Attwellb442dc72014-06-18 11:58:23 -0700217 }
218 }
Brian Attwell64d3abc2014-06-12 17:08:12 -0700219 };
220
221 /**
222 * Interpolator from android.support.v4.view.ViewPager. Snappier and more elastic feeling
223 * than the default interpolator.
224 */
Brian Attwellb7e43642014-06-02 14:33:04 -0700225 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);
Brian Attwell5fd86982014-12-04 16:25:33 -0800254 mEdgeGlowTop = new EdgeEffect(context);
Brian Attwellb7e43642014-06-02 14:33:04 -0700255 mScroller = new Scroller(context, sInterpolator);
256 mTouchSlop = configuration.getScaledTouchSlop();
257 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
Brian Attwell5fd86982014-12-04 16:25:33 -0800258 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Brian Attwellb7e43642014-06-02 14:33:04 -0700259 mTransparentStartHeight = (int) getResources().getDimension(
260 R.dimen.quickcontact_starting_empty_height);
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700261 mToolbarElevation = getResources().getDimension(
Brian Attwell019c71f2014-06-10 19:29:03 -0700262 R.dimen.quick_contact_toolbar_elevation);
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700263 mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel);
264 mMaximumTitleMargin = (int) getResources().getDimension(
265 R.dimen.quickcontact_title_initial_margin);
Brian Attwell64d3abc2014-06-12 17:08:12 -0700266
Brian Attwell5fd86982014-12-04 16:25:33 -0800267 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
Brian Attwell841c3332014-10-27 15:05:09 -0700274 final TypedValue photoRatio = new TypedValue();
275 getResources().getValue(R.dimen.quickcontact_landscape_photo_ratio, photoRatio,
276 /* resolveRefs = */ true);
277 mLandscapePhotoRatio = photoRatio.getFloat();
278
Brian Attwell64d3abc2014-06-12 17:08:12 -0700279 final TypedArray attributeArray = context.obtainStyledAttributes(
280 new int[]{android.R.attr.actionBarSize});
Brian Attwelle48a3b52014-07-22 17:11:34 -0700281 mActionBarSize = attributeArray.getDimensionPixelSize(0, 0);
282 mMinimumHeaderHeight = mActionBarSize;
Brian Attwelld7980782014-06-24 18:52:26 -0700283 // 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;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700286 attributeArray.recycle();
Brian Attwellb7e43642014-06-02 14:33:04 -0700287 }
288
289 /**
290 * This method must be called inside the Activity's OnCreate.
291 */
Wenyi Wangcaf26192016-05-09 15:00:25 -0700292 public void initialize(MultiShrinkScrollerListener listener, boolean isOpenContactSquare,
293 final int maximumHeaderTextSize, final boolean shouldUpdateNameViewHeight) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700294 mScrollView = (ScrollView) findViewById(R.id.content_scroller);
295 mScrollViewChild = findViewById(R.id.card_container);
296 mToolbar = findViewById(R.id.toolbar_parent);
Brian Attwell019c71f2014-06-10 19:29:03 -0700297 mPhotoViewContainer = findViewById(R.id.toolbar_parent);
Brian Attwell0d90afe2014-06-18 16:45:41 -0700298 mTransparentView = findViewById(R.id.transparent_view);
Brian Attwell9b889e62014-06-23 18:25:32 -0700299 mLargeTextView = (TextView) findViewById(R.id.large_title);
Tingting Wangdf65d162015-07-24 17:04:35 -0700300 mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
301 mTitleAndPhoneticNameView = findViewById(R.id.title_and_phonetic_name);
Brian Attwell9b889e62014-06-23 18:25:32 -0700302 mInvisiblePlaceholderTextView = (TextView) findViewById(R.id.placeholder_textview);
Paul Soulos1deb5322014-08-28 18:47:42 -0700303 mStartColumn = findViewById(R.id.empty_start_column);
Paul Soulosfffcf2a2014-09-11 18:15:52 +0000304 // 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 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700319 mListener = listener;
Brian Attwellc33ae172014-07-01 14:21:37 -0700320 mIsOpenContactSquare = isOpenContactSquare;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700321
Brian Attwell9b889e62014-06-23 18:25:32 -0700322 mPhotoView = (QuickContactImageView) findViewById(R.id.photo);
Brian Attwelled6cda62014-06-26 17:13:14 -0700323
Brian Attwell4936a922014-07-02 16:16:59 -0700324 mTitleGradientView = findViewById(R.id.title_gradient);
325 mTitleGradientView.setBackground(mTitleGradientDrawable);
326 mActionBarGradientView = findViewById(R.id.action_bar_gradient);
327 mActionBarGradientView.setBackground(mActionBarGradientDrawable);
Brian Attwell1c806542014-10-22 14:02:42 -0700328 mCollapsedTitleStartMargin = ((Toolbar) findViewById(R.id.toolbar)).getContentInsetStart();
Brian Attwell4936a922014-07-02 16:16:59 -0700329
Brian Attwell3d291452014-08-26 17:59:33 -0700330 mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay);
Brian Attwelled6cda62014-06-26 17:13:14 -0700331 if (!mIsTwoPanel) {
Brian Attwelled6cda62014-06-26 17:13:14 -0700332 mPhotoTouchInterceptOverlay.setOnClickListener(new OnClickListener() {
333 @Override
334 public void onClick(View v) {
Brian Attwellf37ba162014-09-23 14:44:17 -0700335 expandHeader();
Brian Attwelled6cda62014-06-26 17:13:14 -0700336 }
337 });
338 }
Brian Attwell64d3abc2014-06-12 17:08:12 -0700339
Brian Attwell9b889e62014-06-23 18:25:32 -0700340 SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ false, new Runnable() {
Brian Attwell64d3abc2014-06-12 17:08:12 -0700341 @Override
342 public void run() {
Brian Attwellac3ed8e2014-06-27 17:24:42 -0700343 if (!mIsTwoPanel) {
344 // We never want the height of the photo view to exceed its width.
Brian Attwell1e3d3a92014-08-29 11:55:07 -0700345 mMaximumHeaderHeight = mPhotoViewContainer.getWidth();
Brian Attwellac3ed8e2014-06-27 17:24:42 -0700346 mIntermediateHeaderHeight = (int) (mMaximumHeaderHeight
347 * INTERMEDIATE_HEADER_HEIGHT_RATIO);
348 }
Brian Attwell841c3332014-10-27 15:05:09 -0700349 mMaximumPortraitHeaderHeight = mIsTwoPanel ? getHeight()
Brian Attwell1e3d3a92014-08-29 11:55:07 -0700350 : mPhotoViewContainer.getWidth();
Brian Attwellc33ae172014-07-01 14:21:37 -0700351 setHeaderHeight(getMaximumScrollableHeaderHeight());
Wenyi Wangcaf26192016-05-09 15:00:25 -0700352 if (shouldUpdateNameViewHeight) {
353 mMaximumHeaderTextSize = mTitleAndPhoneticNameView.getHeight();
354 mMaximumFullNameViewHeight = mLargeTextView.getHeight();
355 // We cannot rely on mPhoneticNameView.getHeight() since it could be 0
356 final int phoneticNameSize = getResources().getDimensionPixelSize(
357 R.dimen.quickcontact_maximum_phonetic_name_size);
358 final int fullNameSize = getResources().getDimensionPixelSize(
359 R.dimen.quickcontact_maximum_title_size);
360 mMaximumPhoneticNameViewHeight =
361 mMaximumFullNameViewHeight * phoneticNameSize / fullNameSize;
362 }
363 if (maximumHeaderTextSize > 0) {
364 mMaximumHeaderTextSize = maximumHeaderTextSize;
365 }
Brian Attwell9b889e62014-06-23 18:25:32 -0700366 if (mIsTwoPanel) {
367 mMaximumHeaderHeight = getHeight();
368 mMinimumHeaderHeight = mMaximumHeaderHeight;
369 mIntermediateHeaderHeight = mMaximumHeaderHeight;
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700370
371 // Permanently set photo width and height.
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700372 final ViewGroup.LayoutParams photoLayoutParams
373 = mPhotoViewContainer.getLayoutParams();
374 photoLayoutParams.height = mMaximumHeaderHeight;
Brian Attwell841c3332014-10-27 15:05:09 -0700375 photoLayoutParams.width = (int) (mMaximumHeaderHeight * mLandscapePhotoRatio);
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700376 mPhotoViewContainer.setLayoutParams(photoLayoutParams);
377
378 // Permanently set title width and margin.
379 final FrameLayout.LayoutParams largeTextLayoutParams
Tingting Wangdf65d162015-07-24 17:04:35 -0700380 = (FrameLayout.LayoutParams) mTitleAndPhoneticNameView
381 .getLayoutParams();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700382 largeTextLayoutParams.width = photoLayoutParams.width -
383 largeTextLayoutParams.leftMargin - largeTextLayoutParams.rightMargin;
384 largeTextLayoutParams.gravity = Gravity.BOTTOM | Gravity.START;
Tingting Wangdf65d162015-07-24 17:04:35 -0700385 mTitleAndPhoneticNameView.setLayoutParams(largeTextLayoutParams);
Brian Attwellcf930c72014-09-05 15:22:23 -0700386 } else {
387 // Set the width of mLargeTextView as if it was nested inside
388 // mPhotoViewContainer.
389 mLargeTextView.setWidth(mPhotoViewContainer.getWidth()
390 - 2 * mMaximumTitleMargin);
Tingting Wangdf65d162015-07-24 17:04:35 -0700391 mPhoneticNameView.setWidth(mPhotoViewContainer.getWidth()
392 - 2 * mMaximumTitleMargin);
Brian Attwell9b889e62014-06-23 18:25:32 -0700393 }
394
Brian Attwellbe48e0c2014-06-25 10:31:59 -0700395 calculateCollapsedLargeTitlePadding();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700396 updateHeaderTextSizeAndMargin();
Brian Attwell4936a922014-07-02 16:16:59 -0700397 configureGradientViewHeights();
Brian Attwell64d3abc2014-06-12 17:08:12 -0700398 }
399 });
Brian Attwellb7e43642014-06-02 14:33:04 -0700400 }
401
Brian Attwell4936a922014-07-02 16:16:59 -0700402 private void configureGradientViewHeights() {
Brian Attwell4936a922014-07-02 16:16:59 -0700403 final FrameLayout.LayoutParams actionBarGradientLayoutParams
404 = (FrameLayout.LayoutParams) mActionBarGradientView.getLayoutParams();
Brian Attwell7609b7d2014-11-19 18:01:29 -0800405 actionBarGradientLayoutParams.height = mActionBarSize;
Brian Attwell4936a922014-07-02 16:16:59 -0700406 mActionBarGradientView.setLayoutParams(actionBarGradientLayoutParams);
407 final FrameLayout.LayoutParams titleGradientLayoutParams
408 = (FrameLayout.LayoutParams) mTitleGradientView.getLayoutParams();
Brian Attwell7609b7d2014-11-19 18:01:29 -0800409 final float TITLE_GRADIENT_SIZE_COEFFICIENT = 1.25f;
Brian Attwell4936a922014-07-02 16:16:59 -0700410 final FrameLayout.LayoutParams largeTextLayoutParms
Tingting Wangdf65d162015-07-24 17:04:35 -0700411 = (FrameLayout.LayoutParams) mTitleAndPhoneticNameView.getLayoutParams();
Wenyi Wangcaf26192016-05-09 15:00:25 -0700412 titleGradientLayoutParams.height = (int) ((mMaximumHeaderTextSize
Brian Attwell7609b7d2014-11-19 18:01:29 -0800413 + largeTextLayoutParms.bottomMargin) * TITLE_GRADIENT_SIZE_COEFFICIENT);
Brian Attwell4936a922014-07-02 16:16:59 -0700414 mTitleGradientView.setLayoutParams(titleGradientLayoutParams);
415 }
416
Tingting Wang9f153a02015-11-05 18:11:04 -0800417 public void setTitle(String title, boolean isPhoneNumber) {
Brian Attwell9b889e62014-06-23 18:25:32 -0700418 mLargeTextView.setText(title);
Tingting Wang9f153a02015-11-05 18:11:04 -0800419 // We have a phone number as "mLargeTextView" so make it always LTR.
420 if (isPhoneNumber) {
421 mLargeTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
422 }
Brian Attwell91bd3002014-08-25 15:24:22 -0700423 mPhotoTouchInterceptOverlay.setContentDescription(title);
Brian Attwell9b889e62014-06-23 18:25:32 -0700424 }
425
Tingting Wangdf65d162015-07-24 17:04:35 -0700426 public void setPhoneticName(String phoneticName) {
Wenyi Wang6b77ccf2016-02-29 11:05:20 -0800427 // 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 }
Tingting Wangdf65d162015-07-24 17:04:35 -0700432 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);
Wenyi Wang6b77ccf2016-02-29 11:05:20 -0800436 // TODO try not using initialize() to refresh phonetic name view: b/27410518
Wenyi Wangcaf26192016-05-09 15:00:25 -0700437 initialize(mListener, mIsOpenContactSquare, /* maximumHeaderTextSize */
438 (mMaximumFullNameViewHeight + mMaximumPhoneticNameViewHeight),
439 /* shouldUpdateNameViewHeight */ false);
Tingting Wangdf65d162015-07-24 17:04:35 -0700440 }
441
442 public void setPhoneticNameGone() {
Wenyi Wang6b77ccf2016-02-29 11:05:20 -0800443 // Remove phonetic name only when it was visible before.
444 if (mPhoneticNameView.getVisibility() == View.GONE) {
445 return;
446 }
Tingting Wangdf65d162015-07-24 17:04:35 -0700447 mPhoneticNameView.setVisibility(View.GONE);
448 // Initialize to make Visibility work.
Wenyi Wang6b77ccf2016-02-29 11:05:20 -0800449 // TODO try not using initialize() to refresh phonetic name view: b/27410518
Wenyi Wangcaf26192016-05-09 15:00:25 -0700450 initialize(mListener, mIsOpenContactSquare,
451 /* maximumHeaderTextSize */ mMaximumFullNameViewHeight,
452 /* shouldUpdateNameViewHeight */ false);
Tingting Wangdf65d162015-07-24 17:04:35 -0700453 }
454
Brian Attwellb7e43642014-06-02 14:33:04 -0700455 @Override
456 public boolean onInterceptTouchEvent(MotionEvent event) {
Brian Attwellbf2854d2014-10-08 16:12:11 -0700457 if (mVelocityTracker == null) {
458 mVelocityTracker = VelocityTracker.obtain();
459 }
460 mVelocityTracker.addMovement(event);
461
Brian Attwellb7e43642014-06-02 14:33:04 -0700462 // The only time we want to intercept touch events is when we are being dragged.
463 return shouldStartDrag(event);
464 }
465
466 private boolean shouldStartDrag(MotionEvent event) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800467 if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return false;
468
Brian Attwell5fd86982014-12-04 16:25:33 -0800469
Brian Attwellb7e43642014-06-02 14:33:04 -0700470 if (mIsBeingDragged) {
471 mIsBeingDragged = false;
472 return false;
473 }
474
475 switch (event.getAction()) {
476 // If we are in the middle of a fling and there is a down event, we'll steal it and
477 // start a drag.
478 case MotionEvent.ACTION_DOWN:
479 updateLastEventPosition(event);
480 if (!mScroller.isFinished()) {
481 startDrag();
482 return true;
483 } else {
484 mReceivedDown = true;
485 }
486 break;
487
488 // Otherwise, we will start a drag if there is enough motion in the direction we are
489 // capable of scrolling.
490 case MotionEvent.ACTION_MOVE:
491 if (motionShouldStartDrag(event)) {
492 updateLastEventPosition(event);
493 startDrag();
494 return true;
495 }
496 break;
497 }
498
499 return false;
500 }
501
502 @Override
503 public boolean onTouchEvent(MotionEvent event) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800504 if (mIsTouchDisabledForDismissAnimation || mIsTouchDisabledForSuppressLayout) return true;
Brian Attwell5fd86982014-12-04 16:25:33 -0800505
Brian Attwellb7e43642014-06-02 14:33:04 -0700506 final int action = event.getAction();
507
508 if (mVelocityTracker == null) {
509 mVelocityTracker = VelocityTracker.obtain();
510 }
511 mVelocityTracker.addMovement(event);
512
513 if (!mIsBeingDragged) {
514 if (shouldStartDrag(event)) {
515 return true;
516 }
517
518 if (action == MotionEvent.ACTION_UP && mReceivedDown) {
519 mReceivedDown = false;
520 return performClick();
521 }
522 return true;
523 }
524
525 switch (action) {
526 case MotionEvent.ACTION_MOVE:
527 final float delta = updatePositionAndComputeDelta(event);
528 scrollTo(0, getScroll() + (int) delta);
529 mReceivedDown = false;
530
531 if (mIsBeingDragged) {
Brian Attwell9b889e62014-06-23 18:25:32 -0700532 final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
533 if (delta > distanceFromMaxScrolling) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700534 // The ScrollView is being pulled upwards while there is no more
535 // content offscreen, and the view port is already fully expanded.
Wenyi Wangc85af282015-12-21 15:55:32 -0800536 EdgeEffectCompat.onPull(mEdgeGlowBottom, delta / getHeight(),
537 1 - event.getX() / getWidth());
Brian Attwellb7e43642014-06-02 14:33:04 -0700538 }
Brian Attwell9b889e62014-06-23 18:25:32 -0700539
Brian Attwellb7e43642014-06-02 14:33:04 -0700540 if (!mEdgeGlowBottom.isFinished()) {
541 postInvalidateOnAnimation();
542 }
543
Brian Attwell5fd86982014-12-04 16:25:33 -0800544 if (shouldDismissOnScroll()) {
545 scrollOffBottom();
546 }
547
Brian Attwellb7e43642014-06-02 14:33:04 -0700548 }
549 break;
550
551 case MotionEvent.ACTION_UP:
552 case MotionEvent.ACTION_CANCEL:
553 stopDrag(action == MotionEvent.ACTION_CANCEL);
554 mReceivedDown = false;
555 break;
556 }
557
558 return true;
559 }
560
Brian Attwell31b2d422014-06-05 00:14:58 -0700561 public void setHeaderTintColor(int color) {
562 mHeaderTintColor = color;
Brian Attwell019c71f2014-06-10 19:29:03 -0700563 updatePhotoTintAndDropShadow();
Wenyi Wang4c3d3e22015-12-17 14:30:02 -0800564 if (CompatUtils.isLollipopCompatible()) {
565 // Use the same amount of alpha on the new tint color as the previous tint color.
566 final int edgeEffectAlpha = Color.alpha(mEdgeGlowBottom.getColor());
567 mEdgeGlowBottom.setColor((color & 0xffffff) | Color.argb(edgeEffectAlpha, 0, 0, 0));
568 mEdgeGlowTop.setColor(mEdgeGlowBottom.getColor());
569 }
Brian Attwell31b2d422014-06-05 00:14:58 -0700570 }
571
Brian Attwell64d3abc2014-06-12 17:08:12 -0700572 /**
Brian Attwellf37ba162014-09-23 14:44:17 -0700573 * Expand to maximum size.
Brian Attwell64d3abc2014-06-12 17:08:12 -0700574 */
Brian Attwellf37ba162014-09-23 14:44:17 -0700575 private void expandHeader() {
Brian Attwell64d3abc2014-06-12 17:08:12 -0700576 if (getHeaderHeight() != mMaximumHeaderHeight) {
577 final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
578 mMaximumHeaderHeight);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700579 animator.setDuration(ExpandingEntryCardView.DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
Brian Attwell64d3abc2014-06-12 17:08:12 -0700580 animator.start();
Brian Attwell7fa11d12014-06-12 18:04:16 -0700581 // Scroll nested scroll view to its top
582 if (mScrollView.getScrollY() != 0) {
583 ObjectAnimator.ofInt(mScrollView, "scrollY", -mScrollView.getScrollY()).start();
584 }
Brian Attwell64d3abc2014-06-12 17:08:12 -0700585 }
586 }
587
Brian Attwellb7e43642014-06-02 14:33:04 -0700588 private void startDrag() {
589 mIsBeingDragged = true;
590 mScroller.abortAnimation();
591 }
592
593 private void stopDrag(boolean cancelled) {
594 mIsBeingDragged = false;
595 if (!cancelled && getChildCount() > 0) {
596 final float velocity = getCurrentVelocity();
597 if (velocity > mMinimumVelocity || velocity < -mMinimumVelocity) {
598 fling(-velocity);
599 onDragFinished(mScroller.getFinalY() - mScroller.getStartY());
600 } else {
601 onDragFinished(/* flingDelta = */ 0);
602 }
603 } else {
604 onDragFinished(/* flingDelta = */ 0);
605 }
606
607 if (mVelocityTracker != null) {
608 mVelocityTracker.recycle();
609 mVelocityTracker = null;
610 }
611
612 mEdgeGlowBottom.onRelease();
613 }
614
615 private void onDragFinished(int flingDelta) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800616 if (getTransparentViewHeight() <= 0) {
617 // Don't perform any snapping if quick contacts is full screen.
618 return;
619 }
620 if (!snapToTopOnDragFinished(flingDelta)) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700621 // The drag/fling won't result in the content at the top of the Window. Consider
622 // snapping the content to the bottom of the window.
Brian Attwell5fd86982014-12-04 16:25:33 -0800623 snapToBottomOnDragFinished();
Brian Attwellb7e43642014-06-02 14:33:04 -0700624 }
625 }
626
627 /**
628 * If needed, snap the subviews to the top of the Window.
Brian Attwell5fd86982014-12-04 16:25:33 -0800629 *
630 * @return TRUE if QuickContacts will snap/fling to to top after this method call.
Brian Attwellb7e43642014-06-02 14:33:04 -0700631 */
Brian Attwell5fd86982014-12-04 16:25:33 -0800632 private boolean snapToTopOnDragFinished(int flingDelta) {
633 if (!mHasEverTouchedTheTop) {
634 // If the current fling is predicted to scroll past the top, then we don't need to snap
635 // to the top. However, if the fling only flings past the top by a tiny amount,
636 // it will look nicer to snap than to fling.
637 final float predictedScrollPastTop = getTransparentViewHeight() - flingDelta;
638 if (predictedScrollPastTop < -mSnapToTopSlopHeight) {
639 return false;
640 }
641
642 if (getTransparentViewHeight() <= mTransparentStartHeight) {
643 // We are above the starting scroll position so snap to the top.
644 mScroller.forceFinished(true);
645 smoothScrollBy(getTransparentViewHeight());
646 return true;
647 }
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700648 return false;
649 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800650 if (getTransparentViewHeight() < mDismissDistanceOnRelease) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700651 mScroller.forceFinished(true);
Brian Attwell5fd86982014-12-04 16:25:33 -0800652 smoothScrollBy(getTransparentViewHeight());
Brian Attwellb7e43642014-06-02 14:33:04 -0700653 return true;
654 }
655 return false;
656 }
657
658 /**
659 * If needed, scroll all the subviews off the bottom of the Window.
660 */
Brian Attwell5fd86982014-12-04 16:25:33 -0800661 private void snapToBottomOnDragFinished() {
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700662 if (mHasEverTouchedTheTop) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800663 if (getTransparentViewHeight() > mDismissDistanceOnRelease) {
664 scrollOffBottom();
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700665 }
666 return;
667 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800668 if (getTransparentViewHeight() > mTransparentStartHeight) {
Brian Attwell8477eaf2014-06-18 15:39:50 -0700669 scrollOffBottom();
670 }
671 }
672
Brian Attwellf1076dc2014-08-12 21:28:21 -0700673 /**
Brian Attwell5fd86982014-12-04 16:25:33 -0800674 * Returns TRUE if we have scrolled far QuickContacts far enough that we should dismiss it
675 * without waiting for the user to finish their drag.
676 */
677 private boolean shouldDismissOnScroll() {
678 return mHasEverTouchedTheTop && getTransparentViewHeight() > mDismissDistanceOnScroll;
679 }
680
681 /**
Brian Attwellf1076dc2014-08-12 21:28:21 -0700682 * Return ratio of non-transparent:viewgroup-height for this viewgroup at the starting position.
683 */
684 public float getStartingTransparentHeightRatio() {
685 return getTransparentHeightRatio(mTransparentStartHeight);
686 }
687
688 private float getTransparentHeightRatio(int transparentHeight) {
689 final float heightRatio = (float) transparentHeight / getHeight();
690 // Clamp between [0, 1] in case this is called before height is initialized.
691 return 1.0f - Math.max(Math.min(1.0f, heightRatio), 0f);
692 }
693
Brian Attwell8477eaf2014-06-18 15:39:50 -0700694 public void scrollOffBottom() {
Brian Attwell5fd86982014-12-04 16:25:33 -0800695 mIsTouchDisabledForDismissAnimation = true;
Brian Attwell8477eaf2014-06-18 15:39:50 -0700696 final Interpolator interpolator = new AcceleratingFlingInterpolator(
697 EXIT_FLING_ANIMATION_DURATION_MS, getCurrentVelocity(),
698 getScrollUntilOffBottom());
699 mScroller.forceFinished(true);
700 ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll",
701 getScroll() - getScrollUntilOffBottom());
702 translateAnimation.setRepeatCount(0);
703 translateAnimation.setInterpolator(interpolator);
704 translateAnimation.setDuration(EXIT_FLING_ANIMATION_DURATION_MS);
705 translateAnimation.addListener(mSnapToBottomListener);
706 translateAnimation.start();
707 if (mListener != null) {
708 mListener.onStartScrollOffBottom();
Brian Attwellb7e43642014-06-02 14:33:04 -0700709 }
710 }
711
Brian Attwellc33ae172014-07-01 14:21:37 -0700712 /**
713 * @param scrollToCurrentPosition if true, will scroll from the bottom of the screen to the
714 * current position. Otherwise, will scroll from the bottom of the screen to the top of the
715 * screen.
716 */
717 public void scrollUpForEntranceAnimation(boolean scrollToCurrentPosition) {
718 final int currentPosition = getScroll();
719 final int bottomScrollPosition = currentPosition
720 - (getHeight() - getTransparentViewHeight()) + 1;
Brian Attwellbc3f2952014-07-07 16:05:50 -0700721 final Interpolator interpolator = AnimationUtils.loadInterpolator(getContext(),
722 android.R.interpolator.linear_out_slow_in);
Brian Attwellf1076dc2014-08-12 21:28:21 -0700723 final int desiredValue = currentPosition + (scrollToCurrentPosition ? currentPosition
724 : getTransparentViewHeight());
Brian Attwellbc3f2952014-07-07 16:05:50 -0700725 final ObjectAnimator animator = ObjectAnimator.ofInt(this, "scroll", bottomScrollPosition,
Brian Attwellf1076dc2014-08-12 21:28:21 -0700726 desiredValue);
Brian Attwellbc3f2952014-07-07 16:05:50 -0700727 animator.setInterpolator(interpolator);
Brian Attwellf1076dc2014-08-12 21:28:21 -0700728 animator.addUpdateListener(new AnimatorUpdateListener() {
729 @Override
730 public void onAnimationUpdate(ValueAnimator animation) {
731 if (animation.getAnimatedValue().equals(desiredValue) && mListener != null) {
732 mListener.onEntranceAnimationDone();
733 }
734 }
735 });
Brian Attwellbc3f2952014-07-07 16:05:50 -0700736 animator.start();
Brian Attwellc33ae172014-07-01 14:21:37 -0700737 }
738
Brian Attwellb7e43642014-06-02 14:33:04 -0700739 @Override
740 public void scrollTo(int x, int y) {
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700741 final int delta = y - getScroll();
742 boolean wasFullscreen = getScrollNeededToBeFullScreen() <= 0;
Brian Attwellb7e43642014-06-02 14:33:04 -0700743 if (delta > 0) {
744 scrollUp(delta);
745 } else {
746 scrollDown(delta);
747 }
Brian Attwell019c71f2014-06-10 19:29:03 -0700748 updatePhotoTintAndDropShadow();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700749 updateHeaderTextSizeAndMargin();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700750 final boolean isFullscreen = getScrollNeededToBeFullScreen() <= 0;
Brian Attwellfc00d0b2014-07-30 15:37:42 -0700751 mHasEverTouchedTheTop |= isFullscreen;
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700752 if (mListener != null) {
753 if (wasFullscreen && !isFullscreen) {
754 mListener.onExitFullscreen();
755 } else if (!wasFullscreen && isFullscreen) {
756 mListener.onEnterFullscreen();
757 }
Brian Attwellf1076dc2014-08-12 21:28:21 -0700758 if (!isFullscreen || !wasFullscreen) {
759 mListener.onTransparentViewHeightChange(
760 getTransparentHeightRatio(getTransparentViewHeight()));
761 }
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700762 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700763 }
764
Brian Attwell64d3abc2014-06-12 17:08:12 -0700765 /**
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700766 * Change the height of the header/toolbar. Do *not* use this outside animations. This was
767 * designed for use by {@link #prepareForShrinkingScrollChild}.
768 */
769 @NeededForReflection
770 public void setToolbarHeight(int delta) {
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700771 final ViewGroup.LayoutParams toolbarLayoutParams
772 = mToolbar.getLayoutParams();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700773 toolbarLayoutParams.height = delta;
774 mToolbar.setLayoutParams(toolbarLayoutParams);
775
776 updatePhotoTintAndDropShadow();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700777 updateHeaderTextSizeAndMargin();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700778 }
779
780 @NeededForReflection
781 public int getToolbarHeight() {
782 return mToolbar.getLayoutParams().height;
783 }
784
785 /**
Brian Attwell64d3abc2014-06-12 17:08:12 -0700786 * Set the height of the toolbar and update its tint accordingly.
787 */
788 @NeededForReflection
789 public void setHeaderHeight(int height) {
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700790 final ViewGroup.LayoutParams toolbarLayoutParams
791 = mToolbar.getLayoutParams();
Brian Attwell64d3abc2014-06-12 17:08:12 -0700792 toolbarLayoutParams.height = height;
793 mToolbar.setLayoutParams(toolbarLayoutParams);
794 updatePhotoTintAndDropShadow();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700795 updateHeaderTextSizeAndMargin();
Brian Attwell64d3abc2014-06-12 17:08:12 -0700796 }
797
798 @NeededForReflection
799 public int getHeaderHeight() {
800 return mToolbar.getLayoutParams().height;
801 }
802
Brian Attwellb7e43642014-06-02 14:33:04 -0700803 @NeededForReflection
804 public void setScroll(int scroll) {
805 scrollTo(0, scroll);
806 }
807
808 /**
809 * Returns the total amount scrolled inside the nested ScrollView + the amount of shrinking
Brian Attwell64d3abc2014-06-12 17:08:12 -0700810 * performed on the ToolBar. This is the value inspected by animators.
Brian Attwellb7e43642014-06-02 14:33:04 -0700811 */
Brian Attwell64d3abc2014-06-12 17:08:12 -0700812 @NeededForReflection
Brian Attwellb7e43642014-06-02 14:33:04 -0700813 public int getScroll() {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700814 return mTransparentStartHeight - getTransparentViewHeight()
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700815 + getMaximumScrollableHeaderHeight() - getToolbarHeight()
Brian Attwellc33ae172014-07-01 14:21:37 -0700816 + mScrollView.getScrollY();
817 }
818
819 private int getMaximumScrollableHeaderHeight() {
820 return mIsOpenContactSquare ? mMaximumHeaderHeight : mIntermediateHeaderHeight;
Brian Attwell64d3abc2014-06-12 17:08:12 -0700821 }
822
823 /**
824 * A variant of {@link #getScroll} that pretends the header is never larger than
825 * than mIntermediateHeaderHeight. This function is sometimes needed when making scrolling
826 * decisions that will not change the header size (ie, snapping to the bottom or top).
Brian Attwellc33ae172014-07-01 14:21:37 -0700827 *
828 * When mIsOpenContactSquare is true, this function considers mIntermediateHeaderHeight ==
829 * mMaximumHeaderHeight, since snapping decisions will be made relative the full header
830 * size when mIsOpenContactSquare = true.
831 *
832 * This value should never be used in conjunction with {@link #getScroll} values.
Brian Attwell64d3abc2014-06-12 17:08:12 -0700833 */
Brian Attwellc33ae172014-07-01 14:21:37 -0700834 private int getScroll_ignoreOversizedHeaderForSnapping() {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700835 return mTransparentStartHeight - getTransparentViewHeight()
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700836 + Math.max(getMaximumScrollableHeaderHeight() - getToolbarHeight(), 0)
Brian Attwell64d3abc2014-06-12 17:08:12 -0700837 + mScrollView.getScrollY();
Brian Attwellb7e43642014-06-02 14:33:04 -0700838 }
839
840 /**
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700841 * Amount of transparent space above the header/toolbar.
842 */
843 public int getScrollNeededToBeFullScreen() {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700844 return getTransparentViewHeight();
Brian Attwell8a6f4ad2014-06-06 21:54:53 -0700845 }
846
847 /**
Brian Attwellb7e43642014-06-02 14:33:04 -0700848 * Return amount of scrolling needed in order for all the visible subviews to scroll off the
849 * bottom.
850 */
Brian Attwellc33ae172014-07-01 14:21:37 -0700851 private int getScrollUntilOffBottom() {
852 return getHeight() + getScroll_ignoreOversizedHeaderForSnapping()
853 - mTransparentStartHeight;
Brian Attwellb7e43642014-06-02 14:33:04 -0700854 }
855
856 @Override
857 public void computeScroll() {
858 if (mScroller.computeScrollOffset()) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800859 // Examine the fling results in order to activate EdgeEffect and halt flings.
Brian Attwellb7e43642014-06-02 14:33:04 -0700860 final int oldScroll = getScroll();
861 scrollTo(0, mScroller.getCurrY());
862 final int delta = mScroller.getCurrY() - oldScroll;
863 final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
864 if (delta > distanceFromMaxScrolling && distanceFromMaxScrolling > 0) {
865 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
866 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800867 if (mIsFullscreenDownwardsFling && getTransparentViewHeight() > 0) {
868 // Halt the fling once QuickContact's top is on screen.
869 scrollTo(0, getScroll() + getTransparentViewHeight());
870 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
871 mScroller.abortAnimation();
872 mIsFullscreenDownwardsFling = false;
873 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700874 if (!awakenScrollBars()) {
875 // Keep on drawing until the animation has finished.
876 postInvalidateOnAnimation();
877 }
878 if (mScroller.getCurrY() >= getMaximumScrollUpwards()) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800879 // Halt the fling once QuickContact's bottom is on screen.
Brian Attwellb7e43642014-06-02 14:33:04 -0700880 mScroller.abortAnimation();
Brian Attwell5fd86982014-12-04 16:25:33 -0800881 mIsFullscreenDownwardsFling = false;
Brian Attwellb7e43642014-06-02 14:33:04 -0700882 }
883 }
884 }
885
886 @Override
887 public void draw(Canvas canvas) {
888 super.draw(canvas);
889
Brian Attwell5fd86982014-12-04 16:25:33 -0800890 final int width = getWidth() - getPaddingLeft() - getPaddingRight();
891 final int height = getHeight();
892
Brian Attwellb7e43642014-06-02 14:33:04 -0700893 if (!mEdgeGlowBottom.isFinished()) {
894 final int restoreCount = canvas.save();
Brian Attwellb7e43642014-06-02 14:33:04 -0700895
896 // Draw the EdgeEffect on the bottom of the Window (Or a little bit below the bottom
897 // of the Window if we start to scroll upwards while EdgeEffect is visible). This
898 // does not need to consider the case where this MultiShrinkScroller doesn't fill
899 // the Window, since the nested ScrollView should be set to fillViewport.
Brian Attwell90689f22014-07-24 14:31:26 -0700900 canvas.translate(-width + getPaddingLeft(),
Brian Attwellb7e43642014-06-02 14:33:04 -0700901 height + getMaximumScrollUpwards() - getScroll());
902
903 canvas.rotate(180, width, 0);
Brian Attwell9b889e62014-06-23 18:25:32 -0700904 if (mIsTwoPanel) {
905 // Only show the EdgeEffect on the bottom of the ScrollView.
906 mEdgeGlowBottom.setSize(mScrollView.getWidth(), height);
Brian Attwellf1402272014-12-16 16:00:08 -0800907 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Brian Attwell90689f22014-07-24 14:31:26 -0700908 canvas.translate(mPhotoViewContainer.getWidth(), 0);
909 }
Brian Attwell9b889e62014-06-23 18:25:32 -0700910 } else {
911 mEdgeGlowBottom.setSize(width, height);
912 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700913 if (mEdgeGlowBottom.draw(canvas)) {
914 postInvalidateOnAnimation();
915 }
916 canvas.restoreToCount(restoreCount);
917 }
Brian Attwell5fd86982014-12-04 16:25:33 -0800918
919 if (!mEdgeGlowTop.isFinished()) {
920 final int restoreCount = canvas.save();
921 if (mIsTwoPanel) {
922 mEdgeGlowTop.setSize(mScrollView.getWidth(), height);
Brian Attwellf1402272014-12-16 16:00:08 -0800923 if (getLayoutDirection() != View.LAYOUT_DIRECTION_RTL) {
Brian Attwell5fd86982014-12-04 16:25:33 -0800924 canvas.translate(mPhotoViewContainer.getWidth(), 0);
925 }
926 } else {
927 mEdgeGlowTop.setSize(width, height);
928 }
929 if (mEdgeGlowTop.draw(canvas)) {
930 postInvalidateOnAnimation();
931 }
932 canvas.restoreToCount(restoreCount);
933 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700934 }
935
936 private float getCurrentVelocity() {
Brian Attwell8477eaf2014-06-18 15:39:50 -0700937 if (mVelocityTracker == null) {
938 return 0;
939 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700940 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumVelocity);
941 return mVelocityTracker.getYVelocity();
942 }
943
944 private void fling(float velocity) {
945 // For reasons I do not understand, scrolling is less janky when maxY=Integer.MAX_VALUE
946 // then when maxY is set to an actual value.
947 mScroller.fling(0, getScroll(), 0, (int) velocity, 0, 0, -Integer.MAX_VALUE,
948 Integer.MAX_VALUE);
Brian Attwell5fd86982014-12-04 16:25:33 -0800949 if (velocity < 0 && mTransparentView.getHeight() <= 0) {
950 mIsFullscreenDownwardsFling = true;
951 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700952 invalidate();
953 }
954
955 private int getMaximumScrollUpwards() {
Brian Attwell9b889e62014-06-23 18:25:32 -0700956 if (!mIsTwoPanel) {
957 return mTransparentStartHeight
958 // How much the Header view can compress
Brian Attwellc33ae172014-07-01 14:21:37 -0700959 + getMaximumScrollableHeaderHeight() - getFullyCompressedHeaderHeight()
Brian Attwell9b889e62014-06-23 18:25:32 -0700960 // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
961 + Math.max(0, mScrollViewChild.getHeight() - getHeight()
Brian Attwelle838a442014-06-27 15:53:13 -0700962 + getFullyCompressedHeaderHeight());
Brian Attwell9b889e62014-06-23 18:25:32 -0700963 } else {
964 return mTransparentStartHeight
965 // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
966 + Math.max(0, mScrollViewChild.getHeight() - getHeight());
967 }
Brian Attwellb7e43642014-06-02 14:33:04 -0700968 }
969
Brian Attwell0d90afe2014-06-18 16:45:41 -0700970 private int getTransparentViewHeight() {
971 return mTransparentView.getLayoutParams().height;
972 }
973
974 private void setTransparentViewHeight(int height) {
975 mTransparentView.getLayoutParams().height = height;
976 mTransparentView.setLayoutParams(mTransparentView.getLayoutParams());
977 }
978
Brian Attwellb7e43642014-06-02 14:33:04 -0700979 private void scrollUp(int delta) {
Brian Attwell0d90afe2014-06-18 16:45:41 -0700980 if (getTransparentViewHeight() != 0) {
981 final int originalValue = getTransparentViewHeight();
982 setTransparentViewHeight(getTransparentViewHeight() - delta);
983 setTransparentViewHeight(Math.max(0, getTransparentViewHeight()));
984 delta -= originalValue - getTransparentViewHeight();
Brian Attwellb7e43642014-06-02 14:33:04 -0700985 }
Brian Attwell0d3fd2e2014-07-30 22:23:34 -0700986 final ViewGroup.LayoutParams toolbarLayoutParams
987 = mToolbar.getLayoutParams();
Brian Attwelle838a442014-06-27 15:53:13 -0700988 if (toolbarLayoutParams.height > getFullyCompressedHeaderHeight()) {
Brian Attwellb7e43642014-06-02 14:33:04 -0700989 final int originalValue = toolbarLayoutParams.height;
990 toolbarLayoutParams.height -= delta;
Brian Attwelle838a442014-06-27 15:53:13 -0700991 toolbarLayoutParams.height = Math.max(toolbarLayoutParams.height,
992 getFullyCompressedHeaderHeight());
Brian Attwellb7e43642014-06-02 14:33:04 -0700993 mToolbar.setLayoutParams(toolbarLayoutParams);
994 delta -= originalValue - toolbarLayoutParams.height;
995 }
996 mScrollView.scrollBy(0, delta);
997 }
998
Brian Attwelle838a442014-06-27 15:53:13 -0700999 /**
1000 * Returns the minimum size that we want to compress the header to, given that we don't want to
1001 * allow the the ScrollView to scroll unless there is new content off of the edge of ScrollView.
1002 */
1003 private int getFullyCompressedHeaderHeight() {
Brian Attwellf37ba162014-09-23 14:44:17 -07001004 return Math.min(Math.max(mToolbar.getLayoutParams().height - getOverflowingChildViewSize(),
Brian Attwell7d13d9c2014-09-08 18:25:58 -07001005 mMinimumHeaderHeight), getMaximumScrollableHeaderHeight());
Brian Attwelle8ce6ee2014-06-27 18:26:32 -07001006 }
1007
1008 /**
1009 * Returns the amount of mScrollViewChild that doesn't fit inside its parent.
1010 */
1011 private int getOverflowingChildViewSize() {
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001012 final int usedScrollViewSpace = mScrollViewChild.getHeight();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -07001013 return -getHeight() + usedScrollViewSpace + mToolbar.getLayoutParams().height;
Brian Attwelle838a442014-06-27 15:53:13 -07001014 }
1015
Brian Attwellb7e43642014-06-02 14:33:04 -07001016 private void scrollDown(int delta) {
Brian Attwellb7e43642014-06-02 14:33:04 -07001017 if (mScrollView.getScrollY() > 0) {
1018 final int originalValue = mScrollView.getScrollY();
1019 mScrollView.scrollBy(0, delta);
1020 delta -= mScrollView.getScrollY() - originalValue;
1021 }
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001022 final ViewGroup.LayoutParams toolbarLayoutParams = mToolbar.getLayoutParams();
Brian Attwellc33ae172014-07-01 14:21:37 -07001023 if (toolbarLayoutParams.height < getMaximumScrollableHeaderHeight()) {
Brian Attwellb7e43642014-06-02 14:33:04 -07001024 final int originalValue = toolbarLayoutParams.height;
1025 toolbarLayoutParams.height -= delta;
Brian Attwell64d3abc2014-06-12 17:08:12 -07001026 toolbarLayoutParams.height = Math.min(toolbarLayoutParams.height,
Brian Attwellc33ae172014-07-01 14:21:37 -07001027 getMaximumScrollableHeaderHeight());
Brian Attwellb7e43642014-06-02 14:33:04 -07001028 mToolbar.setLayoutParams(toolbarLayoutParams);
1029 delta -= originalValue - toolbarLayoutParams.height;
1030 }
Brian Attwell0d90afe2014-06-18 16:45:41 -07001031 setTransparentViewHeight(getTransparentViewHeight() - delta);
Brian Attwellb7e43642014-06-02 14:33:04 -07001032
Brian Attwell8477eaf2014-06-18 15:39:50 -07001033 if (getScrollUntilOffBottom() <= 0) {
Brian Attwellb7e43642014-06-02 14:33:04 -07001034 post(new Runnable() {
1035 @Override
1036 public void run() {
Brian Attwell8477eaf2014-06-18 15:39:50 -07001037 if (mListener != null) {
1038 mListener.onScrolledOffBottom();
1039 // No other messages need to be sent to the listener.
1040 mListener = null;
1041 }
Brian Attwellb7e43642014-06-02 14:33:04 -07001042 }
1043 });
1044 }
1045 }
1046
Brian Attwell9b889e62014-06-23 18:25:32 -07001047 /**
1048 * Set the header size and padding, based on the current scroll position.
1049 */
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001050 private void updateHeaderTextSizeAndMargin() {
Brian Attwell9b889e62014-06-23 18:25:32 -07001051 if (mIsTwoPanel) {
Brian Attwell2c47b342014-11-13 00:34:32 +00001052 // The text size stays at a constant size & location in two panel layouts.
Brian Attwell9b889e62014-06-23 18:25:32 -07001053 return;
1054 }
1055
1056 // The pivot point for scaling should be middle of the starting side.
Brian Attwellf1402272014-12-16 16:00:08 -08001057 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Tingting Wangdf65d162015-07-24 17:04:35 -07001058 mTitleAndPhoneticNameView.setPivotX(mTitleAndPhoneticNameView.getWidth());
Brian Attwell9b889e62014-06-23 18:25:32 -07001059 } else {
Tingting Wangdf65d162015-07-24 17:04:35 -07001060 mTitleAndPhoneticNameView.setPivotX(0);
Brian Attwell9b889e62014-06-23 18:25:32 -07001061 }
Wenyi Wangcaf26192016-05-09 15:00:25 -07001062 mTitleAndPhoneticNameView.setPivotY(mMaximumHeaderTextSize / 2);
Brian Attwell9b889e62014-06-23 18:25:32 -07001063
Brian Attwell7a47bb52014-07-31 11:19:46 -07001064 final int toolbarHeight = mToolbar.getLayoutParams().height;
Brian Attwellf37ba162014-09-23 14:44:17 -07001065 mPhotoTouchInterceptOverlay.setClickable(toolbarHeight != mMaximumHeaderHeight);
1066
Brian Attwell7a47bb52014-07-31 11:19:46 -07001067 if (toolbarHeight >= mMaximumHeaderHeight) {
1068 // Everything is full size when the header is fully expanded.
Tingting Wangdf65d162015-07-24 17:04:35 -07001069 mTitleAndPhoneticNameView.setScaleX(1);
1070 mTitleAndPhoneticNameView.setScaleY(1);
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001071 setInterpolatedTitleMargins(1);
Brian Attwell9b889e62014-06-23 18:25:32 -07001072 return;
1073 }
Brian Attwell7a47bb52014-07-31 11:19:46 -07001074
Brian Attwell15195ee2014-08-06 16:51:33 -07001075 final float ratio = (toolbarHeight - mMinimumHeaderHeight)
Brian Attwell7a47bb52014-07-31 11:19:46 -07001076 / (float)(mMaximumHeaderHeight - mMinimumHeaderHeight);
Brian Attwell9b889e62014-06-23 18:25:32 -07001077 final float minimumSize = mInvisiblePlaceholderTextView.getHeight();
Brian Attwell15195ee2014-08-06 16:51:33 -07001078 float bezierOutput = mTextSizePathInterpolator.getInterpolation(ratio);
Brian Attwell7a47bb52014-07-31 11:19:46 -07001079 float scale = (minimumSize + (mMaximumHeaderTextSize - minimumSize) * bezierOutput)
Brian Attwell9b889e62014-06-23 18:25:32 -07001080 / mMaximumHeaderTextSize;
1081
Brian Attwell15195ee2014-08-06 16:51:33 -07001082 // Clamp to reasonable/finite values before passing into framework. The values
1083 // can be wacky before the first pre-render.
1084 bezierOutput = (float) Math.min(bezierOutput, 1.0f);
1085 scale = (float) Math.min(scale, 1.0f);
1086
Tingting Wangdf65d162015-07-24 17:04:35 -07001087 mTitleAndPhoneticNameView.setScaleX(scale);
1088 mTitleAndPhoneticNameView.setScaleY(scale);
Brian Attwell7a47bb52014-07-31 11:19:46 -07001089 setInterpolatedTitleMargins(bezierOutput);
Brian Attwell9b889e62014-06-23 18:25:32 -07001090 }
1091
1092 /**
Tingting Wangdf65d162015-07-24 17:04:35 -07001093 * Calculate the padding around mTitleAndPhoneticNameView so that it will look appropriate once it
Brian Attwell9b889e62014-06-23 18:25:32 -07001094 * finishes moving into its target location/size.
1095 */
Brian Attwellbe48e0c2014-06-25 10:31:59 -07001096 private void calculateCollapsedLargeTitlePadding() {
Brian Attwell2a3bf462014-12-16 17:24:15 -08001097 int invisiblePlaceHolderLocation[] = new int[2];
1098 int largeTextViewRectLocation[] = new int[2];
1099 mInvisiblePlaceholderTextView.getLocationOnScreen(invisiblePlaceHolderLocation);
1100 mToolbar.getLocationOnScreen(largeTextViewRectLocation);
Brian Attwell9b889e62014-06-23 18:25:32 -07001101 // Distance between top of toolbar to the center of the target rectangle.
Brian Attwell2a3bf462014-12-16 17:24:15 -08001102 final int desiredTopToCenter = invisiblePlaceHolderLocation[1]
1103 + mInvisiblePlaceholderTextView.getHeight() / 2
1104 - largeTextViewRectLocation[1];
Tingting Wangdf65d162015-07-24 17:04:35 -07001105 // Padding needed on the mTitleAndPhoneticNameView so that it has the same amount of
Brian Attwell9b889e62014-06-23 18:25:32 -07001106 // padding as the target rectangle.
Tingting Wangdf65d162015-07-24 17:04:35 -07001107 mCollapsedTitleBottomMargin =
Wenyi Wangcaf26192016-05-09 15:00:25 -07001108 desiredTopToCenter - mMaximumHeaderTextSize / 2;
Brian Attwellbe48e0c2014-06-25 10:31:59 -07001109 }
Brian Attwell9b889e62014-06-23 18:25:32 -07001110
Brian Attwellbe48e0c2014-06-25 10:31:59 -07001111 /**
1112 * Interpolate the title's margin size. When {@param x}=1, use the maximum title margins.
1113 * When {@param x}=0, use the margin values taken from {@link #mInvisiblePlaceholderTextView}.
1114 */
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001115 private void setInterpolatedTitleMargins(float x) {
1116 final FrameLayout.LayoutParams titleLayoutParams
Tingting Wangdf65d162015-07-24 17:04:35 -07001117 = (FrameLayout.LayoutParams) mTitleAndPhoneticNameView.getLayoutParams();
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001118 final LinearLayout.LayoutParams toolbarLayoutParams
1119 = (LinearLayout.LayoutParams) mToolbar.getLayoutParams();
Paul Soulos1deb5322014-08-28 18:47:42 -07001120
1121 // Need to add more to margin start if there is a start column
1122 int startColumnWidth = mStartColumn == null ? 0 : mStartColumn.getWidth();
1123
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001124 titleLayoutParams.setMarginStart((int) (mCollapsedTitleStartMargin * (1 - x)
Paul Soulos1deb5322014-08-28 18:47:42 -07001125 + mMaximumTitleMargin * x) + startColumnWidth);
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001126 // How offset the title should be from the bottom of the toolbar
1127 final int pretendBottomMargin = (int) (mCollapsedTitleBottomMargin * (1 - x)
1128 + mMaximumTitleMargin * x) ;
Brian Attwell77b4a592014-08-13 12:22:36 -07001129 // Calculate how offset the title should be from the top of the screen. Instead of
Tingting Wangdf65d162015-07-24 17:04:35 -07001130 // calling mTitleAndPhoneticNameView.getHeight() use the mMaximumHeaderTextSize for this
1131 // calculation. The getHeight() value acts unexpectedly when mTitleAndPhoneticNameView is
1132 // partially clipped by its parent.
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001133 titleLayoutParams.topMargin = getTransparentViewHeight()
1134 + toolbarLayoutParams.height - pretendBottomMargin
Brian Attwell77b4a592014-08-13 12:22:36 -07001135 - mMaximumHeaderTextSize;
1136 titleLayoutParams.bottomMargin = 0;
Tingting Wangdf65d162015-07-24 17:04:35 -07001137 mTitleAndPhoneticNameView.setLayoutParams(titleLayoutParams);
Brian Attwell9b889e62014-06-23 18:25:32 -07001138 }
1139
Brian Attwell019c71f2014-06-10 19:29:03 -07001140 private void updatePhotoTintAndDropShadow() {
Brian Attwell7609b7d2014-11-19 18:01:29 -08001141 // Let's keep an eye on how long this method takes to complete.
Brian Attwelld7980782014-06-24 18:52:26 -07001142 Trace.beginSection("updatePhotoTintAndDropShadow");
1143
yaolub120cd32016-10-30 15:28:01 -07001144 // Tell the photo view what tint we are trying to achieve. Depending on the type of
1145 // drawable used, the photo view may or may not use this tint.
1146 mPhotoView.setTint(mHeaderTintColor);
1147
Brian Attwelle48a3b52014-07-22 17:11:34 -07001148 if (mIsTwoPanel && !mPhotoView.isBasedOffLetterTile()) {
1149 // When in two panel mode, UX considers photo tinting unnecessary for non letter
1150 // tile photos.
1151 mTitleGradientDrawable.setAlpha(0xFF);
1152 mActionBarGradientDrawable.setAlpha(0xFF);
1153 return;
1154 }
1155
Brian Attwelldcb938f2014-06-03 23:05:59 -07001156 // We need to use toolbarLayoutParams to determine the height, since the layout
1157 // params can be updated before the height change is reflected inside the View#getHeight().
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001158 final int toolbarHeight = getToolbarHeight();
Brian Attwell9b889e62014-06-23 18:25:32 -07001159
1160 if (toolbarHeight <= mMinimumHeaderHeight && !mIsTwoPanel) {
Wenyi Wang4c3d3e22015-12-17 14:30:02 -08001161 ViewCompat.setElevation(mPhotoViewContainer, mToolbarElevation);
Brian Attwell9b889e62014-06-23 18:25:32 -07001162 } else {
Wenyi Wang4c3d3e22015-12-17 14:30:02 -08001163 ViewCompat.setElevation(mPhotoViewContainer, 0);
Brian Attwell9b889e62014-06-23 18:25:32 -07001164 }
1165
Brian Attwelldcb938f2014-06-03 23:05:59 -07001166 // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
1167 mPhotoView.clearColorFilter();
Brian Attwell9b889e62014-06-23 18:25:32 -07001168 mColorMatrix.reset();
Brian Attwell9b889e62014-06-23 18:25:32 -07001169
Brian Attwell7609b7d2014-11-19 18:01:29 -08001170 final int gradientAlpha;
1171 if (!mPhotoView.isBasedOffLetterTile()) {
1172 // Constants and equations were arbitrarily picked to choose values for saturation,
1173 // whiteness, tint and gradient alpha. There were four main objectives:
1174 // 1) The transition period between the unmodified image and fully colored image should
1175 // be very short.
1176 // 2) The tinting should be fully applied even before the background image is fully
1177 // faded out and desaturated. Why? A half tinted photo looks bad and results in
1178 // unappealing colors.
1179 // 3) The function should have a derivative of 0 at ratio = 1 to avoid discontinuities.
1180 // 4) The entire process should look awesome.
1181 final float ratio = calculateHeightRatioToBlendingStartHeight(toolbarHeight);
1182 final float alpha = 1.0f - (float) Math.min(Math.pow(ratio, 1.5f) * 2f, 1f);
1183 final float tint = (float) Math.min(Math.pow(ratio, 1.5f) * 3f, 1f);
1184 mColorMatrix.setSaturation(alpha);
1185 mColorMatrix.postConcat(alphaMatrix(alpha, Color.WHITE));
1186 mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, tint));
1187 gradientAlpha = (int) (255 * alpha);
1188 } else if (mIsTwoPanel) {
1189 mColorMatrix.reset();
1190 mColorMatrix.postConcat(alphaMatrix(DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA,
1191 mHeaderTintColor));
1192 gradientAlpha = 0;
Brian Attwell9b889e62014-06-23 18:25:32 -07001193 } else {
Brian Attwell7609b7d2014-11-19 18:01:29 -08001194 // We want a function that has DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA value
1195 // at the intermediate position and uses TILE_EXPONENT. Finding an equation
1196 // that satisfies this condition requires the following arithmetic.
1197 final float ratio = calculateHeightRatioToFullyOpen(toolbarHeight);
1198 final float intermediateRatio = calculateHeightRatioToFullyOpen((int)
1199 (mMaximumPortraitHeaderHeight * INTERMEDIATE_HEADER_HEIGHT_RATIO));
1200 final float TILE_EXPONENT = 3f;
1201 final float slowingFactor = (float) ((1 - intermediateRatio) / intermediateRatio
1202 / (1 - Math.pow(1 - DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA, 1/TILE_EXPONENT)));
1203 float linearBeforeIntermediate = Math.max(1 - (1 - ratio) / intermediateRatio
1204 / slowingFactor, 0);
1205 float colorAlpha = 1 - (float) Math.pow(linearBeforeIntermediate, TILE_EXPONENT);
1206 mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor));
1207 gradientAlpha = 0;
Brian Attwelldcb938f2014-06-03 23:05:59 -07001208 }
Brian Attwell9b889e62014-06-23 18:25:32 -07001209
Brian Attwell7609b7d2014-11-19 18:01:29 -08001210 // TODO: remove re-allocation of ColorMatrixColorFilter objects (b/17627000)
Chris Craikc95fbc92014-09-19 09:59:10 -07001211 mPhotoView.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
Brian Attwell7609b7d2014-11-19 18:01:29 -08001212
Brian Attwell4936a922014-07-02 16:16:59 -07001213 mTitleGradientDrawable.setAlpha(gradientAlpha);
1214 mActionBarGradientDrawable.setAlpha(gradientAlpha);
1215
Brian Attwelld7980782014-06-24 18:52:26 -07001216 Trace.endSection();
1217 }
1218
Brian Attwell7609b7d2014-11-19 18:01:29 -08001219 private float calculateHeightRatioToFullyOpen(int height) {
Brian Attwelld7980782014-06-24 18:52:26 -07001220 return (height - mMinimumPortraitHeaderHeight)
1221 / (float) (mMaximumPortraitHeaderHeight - mMinimumPortraitHeaderHeight);
Brian Attwell9b889e62014-06-23 18:25:32 -07001222 }
1223
Brian Attwell7609b7d2014-11-19 18:01:29 -08001224 private float calculateHeightRatioToBlendingStartHeight(int height) {
1225 final float intermediateHeight = mMaximumPortraitHeaderHeight
1226 * COLOR_BLENDING_START_RATIO;
1227 final float interpolatingHeightRange = intermediateHeight - mMinimumPortraitHeaderHeight;
1228 if (height > intermediateHeight) {
1229 return 0;
1230 }
1231 return (intermediateHeight - height) / interpolatingHeightRange;
1232 }
1233
Brian Attwell9b889e62014-06-23 18:25:32 -07001234 /**
1235 * Simulates alpha blending an image with {@param color}.
1236 */
1237 private ColorMatrix alphaMatrix(float alpha, int color) {
1238 mAlphaMatrixValues[0] = Color.red(color) * alpha / 255;
1239 mAlphaMatrixValues[6] = Color.green(color) * alpha / 255;
1240 mAlphaMatrixValues[12] = Color.blue(color) * alpha / 255;
1241 mAlphaMatrixValues[4] = 255 * (1 - alpha);
1242 mAlphaMatrixValues[9] = 255 * (1 - alpha);
1243 mAlphaMatrixValues[14] = 255 * (1 - alpha);
1244 mWhitenessColorMatrix.set(mAlphaMatrixValues);
1245 return mWhitenessColorMatrix;
1246 }
1247
1248 /**
1249 * Simulates multiply blending an image with a single {@param color}.
1250 *
1251 * Multiply blending is [Sa * Da, Sc * Dc]. See {@link android.graphics.PorterDuff}.
1252 */
1253 private ColorMatrix multiplyBlendMatrix(int color, float alpha) {
1254 mMultiplyBlendMatrixValues[0] = multiplyBlend(Color.red(color), alpha);
1255 mMultiplyBlendMatrixValues[6] = multiplyBlend(Color.green(color), alpha);
1256 mMultiplyBlendMatrixValues[12] = multiplyBlend(Color.blue(color), alpha);
1257 mMultiplyBlendMatrix.set(mMultiplyBlendMatrixValues);
1258 return mMultiplyBlendMatrix;
1259 }
1260
1261 private float multiplyBlend(int color, float alpha) {
1262 return color * alpha / 255.0f + (1 - alpha);
Brian Attwelldcb938f2014-06-03 23:05:59 -07001263 }
1264
Brian Attwellb7e43642014-06-02 14:33:04 -07001265 private void updateLastEventPosition(MotionEvent event) {
1266 mLastEventPosition[0] = event.getX();
1267 mLastEventPosition[1] = event.getY();
1268 }
1269
1270 private boolean motionShouldStartDrag(MotionEvent event) {
Brian Attwellb7e43642014-06-02 14:33:04 -07001271 final float deltaY = event.getY() - mLastEventPosition[1];
Brian Attwellbf2854d2014-10-08 16:12:11 -07001272 return deltaY > mTouchSlop || deltaY < -mTouchSlop;
Brian Attwellb7e43642014-06-02 14:33:04 -07001273 }
1274
1275 private float updatePositionAndComputeDelta(MotionEvent event) {
1276 final int VERTICAL = 1;
1277 final float position = mLastEventPosition[VERTICAL];
1278 updateLastEventPosition(event);
Brian Attwell5fd86982014-12-04 16:25:33 -08001279 float elasticityFactor = 1;
1280 if (position < mLastEventPosition[VERTICAL] && mHasEverTouchedTheTop) {
1281 // As QuickContacts is dragged from the top of the window, its rate of movement will
1282 // slow down in proportion to its distance from the top. This will feel springy.
1283 elasticityFactor += mTransparentView.getHeight() * SPRING_DAMPENING_FACTOR;
1284 }
1285 return (position - mLastEventPosition[VERTICAL]) / elasticityFactor;
Brian Attwellb7e43642014-06-02 14:33:04 -07001286 }
1287
1288 private void smoothScrollBy(int delta) {
Brian Attwell64d3abc2014-06-12 17:08:12 -07001289 if (delta == 0) {
1290 // Delta=0 implies the code calling smoothScrollBy is sloppy. We should avoid doing
1291 // this, since it prevents Views from being able to register any clicks for 250ms.
1292 throw new IllegalArgumentException("Smooth scrolling by delta=0 is "
1293 + "pointless and harmful");
1294 }
Brian Attwellb7e43642014-06-02 14:33:04 -07001295 mScroller.startScroll(0, getScroll(), 0, delta);
1296 invalidate();
1297 }
Brian Attwellb442dc72014-06-18 11:58:23 -07001298
1299 /**
1300 * Interpolator that enforces a specific starting velocity. This is useful to avoid a
1301 * discontinuity between dragging speed and flinging speed.
1302 *
1303 * Similar to a {@link android.view.animation.AccelerateInterpolator} in the sense that
1304 * getInterpolation() is a quadratic function.
1305 */
Brian Attwell2a3bf462014-12-16 17:24:15 -08001306 private class AcceleratingFlingInterpolator implements Interpolator {
Brian Attwellb442dc72014-06-18 11:58:23 -07001307
1308 private final float mStartingSpeedPixelsPerFrame;
1309 private final float mDurationMs;
1310 private final int mPixelsDelta;
1311 private final float mNumberFrames;
1312
1313 public AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond,
1314 int pixelsDelta) {
1315 mStartingSpeedPixelsPerFrame = startingSpeedPixelsPerSecond / getRefreshRate();
1316 mDurationMs = durationMs;
1317 mPixelsDelta = pixelsDelta;
1318 mNumberFrames = mDurationMs / getFrameIntervalMs();
1319 }
1320
1321 @Override
1322 public float getInterpolation(float input) {
1323 final float animationIntervalNumber = mNumberFrames * input;
1324 final float linearDelta = (animationIntervalNumber * mStartingSpeedPixelsPerFrame)
1325 / mPixelsDelta;
1326 // Add the results of a linear interpolator (with the initial speed) with the
1327 // results of a AccelerateInterpolator.
1328 if (mStartingSpeedPixelsPerFrame > 0) {
1329 return Math.min(input * input + linearDelta, 1);
1330 } else {
1331 // Initial fling was in the wrong direction, make sure that the quadratic component
1332 // grows faster in order to make up for this.
1333 return Math.min(input * (input - linearDelta) + linearDelta, 1);
1334 }
1335 }
1336
1337 private float getRefreshRate() {
Brian Attwell2a3bf462014-12-16 17:24:15 -08001338 final DisplayManager displayManager = (DisplayManager) MultiShrinkScroller
1339 .this.getContext().getSystemService(Context.DISPLAY_SERVICE);
1340 return displayManager.getDisplay(Display.DEFAULT_DISPLAY).getRefreshRate();
Brian Attwellb442dc72014-06-18 11:58:23 -07001341 }
1342
1343 public long getFrameIntervalMs() {
1344 return (long)(1000 / getRefreshRate());
1345 }
1346 }
Brian Attwelle8ce6ee2014-06-27 18:26:32 -07001347
1348 /**
1349 * Expand the header if the mScrollViewChild is about to shrink by enough to create new empty
1350 * space at the bottom of this ViewGroup.
1351 */
1352 public void prepareForShrinkingScrollChild(int heightDelta) {
1353 final int newEmptyScrollViewSpace = -getOverflowingChildViewSize() + heightDelta;
1354 if (newEmptyScrollViewSpace > 0 && !mIsTwoPanel) {
Brian Attwell0d3fd2e2014-07-30 22:23:34 -07001355 final int newDesiredToolbarHeight = Math.min(getToolbarHeight()
Brian Attwellc33ae172014-07-01 14:21:37 -07001356 + newEmptyScrollViewSpace, getMaximumScrollableHeaderHeight());
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001357 ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).setDuration(
1358 ExpandingEntryCardView.DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS).start();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -07001359 }
1360 }
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001361
Brian Attwell245d3d22015-01-21 09:50:08 -08001362 /**
1363 * If {@param areTouchesDisabled} is TRUE, ignore all of the user's touches.
1364 */
1365 public void setDisableTouchesForSuppressLayout(boolean areTouchesDisabled) {
1366 // The card expansion animation uses the Transition framework's ChangeBounds API. This
1367 // invokes suppressLayout(true) on the MultiShrinkScroller. As a result, we need to avoid
1368 // all layout changes during expansion in order to avoid weird layout artifacts.
1369 mIsTouchDisabledForSuppressLayout = areTouchesDisabled;
Paul Soulos0cda9ae2014-07-23 11:27:28 -07001370 }
Brian Attwellb7e43642014-06-02 14:33:04 -07001371}