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