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