Merge "Don't accidentally scroll QC off screen." into lmp-mr1-dev
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index b31b5b9..d9e541a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -24,6 +24,14 @@
<dimen name="quickcontact_title_initial_margin">16dp</dimen>
<!-- The ratio of width:height for the contact's photo in landscape -->
<item name="quickcontact_landscape_photo_ratio" type="dimen" format="float">0.7</item>
+ <!-- How far QuickContacts can be dragged and released from the top of the window before we dismiss it. -->
+ <dimen name="quickcontact_dismiss_distance_on_release">40dp</dimen>
+ <!-- How far QuickContacts can be dragged from the top of the window before we dismiss it. -->
+ <dimen name="quickcontact_dismiss_distance_on_scroll">100dp</dimen>
+ <!-- When first flinging QuickContacts towards the top of the window if the fling is
+ predicted to scroll past the window top by less than this amount, then QuickContacts
+ snaps to the top of the window. -->
+ <dimen name="quickcontact_snap_to_top_slop_height">33dp</dimen>
<!-- Padding of the rounded plus/minus/expand/collapse buttons in the editor -->
<dimen name="editor_round_button_padding_left">16dip</dimen>
diff --git a/src/com/android/contacts/widget/MultiShrinkScroller.java b/src/com/android/contacts/widget/MultiShrinkScroller.java
index f7f0c7b..095198a 100644
--- a/src/com/android/contacts/widget/MultiShrinkScroller.java
+++ b/src/com/android/contacts/widget/MultiShrinkScroller.java
@@ -12,7 +12,6 @@
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
-import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -60,8 +59,8 @@
* customized will work for you. For example, see the re-usable StickyHeaderListView used by
* WifiSetupActivity (very nice). Alternatively, check out Google+'s cover photo scrolling or
* Android L's built in nested scrolling support. I thought I needed a more custom ViewGroup in
- * order to track velocity, modify EdgeEffect color & perform specific animations such as the ones
- * inside snapToBottom(). As a result this ViewGroup has non-standard talkback and keyboard support.
+ * order to track velocity, modify EdgeEffect color & perform the originally specified animations.
+ * As a result this ViewGroup has non-standard talkback and keyboard support.
*/
public class MultiShrinkScroller extends FrameLayout {
@@ -73,7 +72,7 @@
/**
* Length of the acceleration animations. This value was taken from ValueAnimator.java.
*/
- private static final int EXIT_FLING_ANIMATION_DURATION_MS = 300;
+ private static final int EXIT_FLING_ANIMATION_DURATION_MS = 250;
/**
* In portrait mode, the height:width ratio of the photo's starting height.
@@ -86,21 +85,22 @@
*/
private static final float COLOR_BLENDING_START_RATIO = 0.5f;
+ private static final float SPRING_DAMPENING_FACTOR = 0.01f;
+
/**
* When displaying a letter tile drawable, this alpha value should be used at the intermediate
* toolbar height.
*/
private static final float DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA = 0.8f;
- /**
- * Maximum velocity for flings in dips per second. Picked via non-rigorous experimentation.
- */
- private static final float MAXIMUM_FLING_VELOCITY = 2000;
-
private float[] mLastEventPosition = { 0, 0 };
private VelocityTracker mVelocityTracker;
private boolean mIsBeingDragged = false;
private boolean mReceivedDown = false;
+ /**
+ * Did the current downwards fling/scroll-animation start while we were fullscreen?
+ */
+ private boolean mIsFullscreenDownwardsFling = false;
private ScrollView mScrollView;
private View mScrollViewChild;
@@ -140,12 +140,17 @@
* True once the header has touched the top of the screen at least once.
*/
private boolean mHasEverTouchedTheTop;
+ private boolean mIsTouchDisabledForDismissAnimation;
private final Scroller mScroller;
private final EdgeEffect mEdgeGlowBottom;
+ private final EdgeEffect mEdgeGlowTop;
private final int mTouchSlop;
private final int mMaximumVelocity;
private final int mMinimumVelocity;
+ private final int mDismissDistanceOnScroll;
+ private final int mDismissDistanceOnRelease;
+ private final int mSnapToTopSlopHeight;
private final int mTransparentStartHeight;
private final int mMaximumTitleMargin;
private final float mToolbarElevation;
@@ -240,12 +245,11 @@
setWillNotDraw(/* willNotDraw = */ false);
mEdgeGlowBottom = new EdgeEffect(context);
+ mEdgeGlowTop = new EdgeEffect(context);
mScroller = new Scroller(context, sInterpolator);
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
- mMaximumVelocity = (int)TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, MAXIMUM_FLING_VELOCITY,
- getResources().getDisplayMetrics());
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mTransparentStartHeight = (int) getResources().getDimension(
R.dimen.quickcontact_starting_empty_height);
mToolbarElevation = getResources().getDimension(
@@ -254,6 +258,13 @@
mMaximumTitleMargin = (int) getResources().getDimension(
R.dimen.quickcontact_title_initial_margin);
+ mDismissDistanceOnScroll = (int) getResources().getDimension(
+ R.dimen.quickcontact_dismiss_distance_on_scroll);
+ mDismissDistanceOnRelease = (int) getResources().getDimension(
+ R.dimen.quickcontact_dismiss_distance_on_release);
+ mSnapToTopSlopHeight = (int) getResources().getDimension(
+ R.dimen.quickcontact_snap_to_top_slop_height);
+
final TypedValue photoRatio = new TypedValue();
getResources().getValue(R.dimen.quickcontact_landscape_photo_ratio, photoRatio,
/* resolveRefs = */ true);
@@ -395,6 +406,8 @@
}
private boolean shouldStartDrag(MotionEvent event) {
+ if (mIsTouchDisabledForDismissAnimation) return false;
+
if (mIsBeingDragged) {
mIsBeingDragged = false;
return false;
@@ -429,6 +442,8 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
+ if (mIsTouchDisabledForDismissAnimation) return true;
+
final int action = event.getAction();
if (mVelocityTracker == null) {
@@ -466,6 +481,10 @@
postInvalidateOnAnimation();
}
+ if (shouldDismissOnScroll()) {
+ scrollOffBottom();
+ }
+
}
break;
@@ -485,6 +504,7 @@
// We want to use the same amount of alpha on the new tint color as the previous tint color.
final int edgeEffectAlpha = Color.alpha(mEdgeGlowBottom.getColor());
mEdgeGlowBottom.setColor((color & 0xffffff) | Color.argb(edgeEffectAlpha, 0, 0, 0));
+ mEdgeGlowTop.setColor(mEdgeGlowBottom.getColor());
}
/**
@@ -531,31 +551,43 @@
}
private void onDragFinished(int flingDelta) {
- if (!snapToTop(flingDelta)) {
+ if (getTransparentViewHeight() <= 0) {
+ // Don't perform any snapping if quick contacts is full screen.
+ return;
+ }
+ if (!snapToTopOnDragFinished(flingDelta)) {
// The drag/fling won't result in the content at the top of the Window. Consider
// snapping the content to the bottom of the window.
- snapToBottom(flingDelta);
+ snapToBottomOnDragFinished();
}
}
/**
* If needed, snap the subviews to the top of the Window.
+ *
+ * @return TRUE if QuickContacts will snap/fling to to top after this method call.
*/
- private boolean snapToTop(int flingDelta) {
- if (mHasEverTouchedTheTop) {
- // Only when first interacting with QuickContacts should QuickContacts snap to the top
- // of the screen. After this, QuickContacts can be placed most anywhere on the screen.
+ private boolean snapToTopOnDragFinished(int flingDelta) {
+ if (!mHasEverTouchedTheTop) {
+ // If the current fling is predicted to scroll past the top, then we don't need to snap
+ // to the top. However, if the fling only flings past the top by a tiny amount,
+ // it will look nicer to snap than to fling.
+ final float predictedScrollPastTop = getTransparentViewHeight() - flingDelta;
+ if (predictedScrollPastTop < -mSnapToTopSlopHeight) {
+ return false;
+ }
+
+ if (getTransparentViewHeight() <= mTransparentStartHeight) {
+ // We are above the starting scroll position so snap to the top.
+ mScroller.forceFinished(true);
+ smoothScrollBy(getTransparentViewHeight());
+ return true;
+ }
return false;
}
- final int requiredScroll = -getScroll_ignoreOversizedHeaderForSnapping()
- + mTransparentStartHeight;
- if (-getScroll_ignoreOversizedHeaderForSnapping() - flingDelta < 0
- && -getScroll_ignoreOversizedHeaderForSnapping() - flingDelta >
- -mTransparentStartHeight && requiredScroll != 0) {
- // We finish scrolling above the empty starting height, and aren't projected
- // to fling past the top of the Window, so elastically snap the empty space shut.
+ if (getTransparentViewHeight() < mDismissDistanceOnRelease) {
mScroller.forceFinished(true);
- smoothScrollBy(requiredScroll);
+ smoothScrollBy(getTransparentViewHeight());
return true;
}
return false;
@@ -564,36 +596,27 @@
/**
* If needed, scroll all the subviews off the bottom of the Window.
*/
- private void snapToBottom(int flingDelta) {
+ private void snapToBottomOnDragFinished() {
if (mHasEverTouchedTheTop) {
- // If QuickContacts has touched the top of the screen previously, then we
- // will less aggressively snap to the bottom of the screen.
- final float predictedScrollPastTop = -getScroll() + mTransparentStartHeight
- - flingDelta;
- final boolean isLandscape = getResources().getConfiguration().orientation
- == Configuration.ORIENTATION_LANDSCAPE;
- if (isLandscape) {
- // In landscape orientation, we dismiss the QC once it goes below the starting
- // starting offset that is used when QC starts in collapsed mode.
- if (predictedScrollPastTop > mTransparentStartHeight) {
- scrollOffBottom();
- }
- } else {
- // In portrait orientation, we dismiss the QC once it goes below
- // mIntermediateHeaderHeight within the bottom of the screen.
- final float heightMinusHeader = getHeight() - mIntermediateHeaderHeight;
- if (predictedScrollPastTop > heightMinusHeader) {
- scrollOffBottom();
- }
+ if (getTransparentViewHeight() > mDismissDistanceOnRelease) {
+ scrollOffBottom();
}
return;
}
- if (-getScroll() - flingDelta > 0) {
+ if (getTransparentViewHeight() > mTransparentStartHeight) {
scrollOffBottom();
}
}
/**
+ * Returns TRUE if we have scrolled far QuickContacts far enough that we should dismiss it
+ * without waiting for the user to finish their drag.
+ */
+ private boolean shouldDismissOnScroll() {
+ return mHasEverTouchedTheTop && getTransparentViewHeight() > mDismissDistanceOnScroll;
+ }
+
+ /**
* Return ratio of non-transparent:viewgroup-height for this viewgroup at the starting position.
*/
public float getStartingTransparentHeightRatio() {
@@ -607,6 +630,7 @@
}
public void scrollOffBottom() {
+ mIsTouchDisabledForDismissAnimation = true;
final Interpolator interpolator = new AcceleratingFlingInterpolator(
EXIT_FLING_ANIMATION_DURATION_MS, getCurrentVelocity(),
getScrollUntilOffBottom());
@@ -770,7 +794,7 @@
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
- // Examine the fling results in order to activate EdgeEffect when we fling to the end.
+ // Examine the fling results in order to activate EdgeEffect and halt flings.
final int oldScroll = getScroll();
scrollTo(0, mScroller.getCurrY());
final int delta = mScroller.getCurrY() - oldScroll;
@@ -778,13 +802,21 @@
if (delta > distanceFromMaxScrolling && distanceFromMaxScrolling > 0) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
-
+ if (mIsFullscreenDownwardsFling && getTransparentViewHeight() > 0) {
+ // Halt the fling once QuickContact's top is on screen.
+ scrollTo(0, getScroll() + getTransparentViewHeight());
+ mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
+ mScroller.abortAnimation();
+ mIsFullscreenDownwardsFling = false;
+ }
if (!awakenScrollBars()) {
// Keep on drawing until the animation has finished.
postInvalidateOnAnimation();
}
if (mScroller.getCurrY() >= getMaximumScrollUpwards()) {
+ // Halt the fling once QuickContact's bottom is on screen.
mScroller.abortAnimation();
+ mIsFullscreenDownwardsFling = false;
}
}
}
@@ -793,10 +825,11 @@
public void draw(Canvas canvas) {
super.draw(canvas);
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight();
+
if (!mEdgeGlowBottom.isFinished()) {
final int restoreCount = canvas.save();
- final int width = getWidth() - getPaddingLeft() - getPaddingRight();
- final int height = getHeight();
// Draw the EdgeEffect on the bottom of the Window (Or a little bit below the bottom
// of the Window if we start to scroll upwards while EdgeEffect is visible). This
@@ -820,6 +853,22 @@
}
canvas.restoreToCount(restoreCount);
}
+
+ if (!mEdgeGlowTop.isFinished()) {
+ final int restoreCount = canvas.save();
+ if (mIsTwoPanel) {
+ mEdgeGlowTop.setSize(mScrollView.getWidth(), height);
+ if (!isLayoutRtl()) {
+ canvas.translate(mPhotoViewContainer.getWidth(), 0);
+ }
+ } else {
+ mEdgeGlowTop.setSize(width, height);
+ }
+ if (mEdgeGlowTop.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
}
private float getCurrentVelocity() {
@@ -831,13 +880,13 @@
}
private void fling(float velocity) {
- if (Math.abs(mMaximumVelocity) < Math.abs(velocity)) {
- velocity = -mMaximumVelocity * Math.signum(velocity);
- }
// For reasons I do not understand, scrolling is less janky when maxY=Integer.MAX_VALUE
// then when maxY is set to an actual value.
mScroller.fling(0, getScroll(), 0, (int) velocity, 0, 0, -Integer.MAX_VALUE,
Integer.MAX_VALUE);
+ if (velocity < 0 && mTransparentView.getHeight() <= 0) {
+ mIsFullscreenDownwardsFling = true;
+ }
invalidate();
}
@@ -1163,7 +1212,13 @@
final int VERTICAL = 1;
final float position = mLastEventPosition[VERTICAL];
updateLastEventPosition(event);
- return position - mLastEventPosition[VERTICAL];
+ float elasticityFactor = 1;
+ if (position < mLastEventPosition[VERTICAL] && mHasEverTouchedTheTop) {
+ // As QuickContacts is dragged from the top of the window, its rate of movement will
+ // slow down in proportion to its distance from the top. This will feel springy.
+ elasticityFactor += mTransparentView.getHeight() * SPRING_DAMPENING_FACTOR;
+ }
+ return (position - mLastEventPosition[VERTICAL]) / elasticityFactor;
}
private void smoothScrollBy(int delta) {