Chromium Code Reviews| Index: chrome/android/java/src/org/chromium/chrome/browser/banners/SwipableOverlayView.java |
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/banners/SwipableOverlayView.java b/chrome/android/java/src/org/chromium/chrome/browser/banners/SwipableOverlayView.java |
| index 0f9ce79edd79f7a16096c5ae29cabbb8c8328c4c..dc6728c713aa97b0e21de9f5d28b8c0af773db7d 100644 |
| --- a/chrome/android/java/src/org/chromium/chrome/browser/banners/SwipableOverlayView.java |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/banners/SwipableOverlayView.java |
| @@ -17,7 +17,8 @@ import android.view.Gravity; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| -import android.view.animation.AccelerateInterpolator; |
| +import android.view.animation.DecelerateInterpolator; |
| +import android.view.animation.Interpolator; |
| import android.widget.FrameLayout; |
| import org.chromium.content.browser.ContentView; |
| @@ -52,7 +53,7 @@ import org.chromium.ui.UiUtils; |
| * As the scroll offset or the viewport height are updated via a scroll or fling, the difference |
| * from the initial value is used to determine the View's Y-translation. If a gesture is stopped, |
| * the View will be snapped back into the center of the screen or entirely off of the screen, based |
| - * on how much of the banner is visible, or where the user is currently located on the page. |
| + * on how much of the View is visible, or where the user is currently located on the page. |
| * |
| * HORIZONTAL SCROLL CALCULATIONS |
| * Horizontal drags and swipes are used to dismiss the View. Translating the View far enough |
| @@ -68,7 +69,6 @@ import org.chromium.ui.UiUtils; |
| */ |
| public abstract class SwipableOverlayView extends FrameLayout { |
| private static final float ALPHA_THRESHOLD = 0.25f; |
| - private static final float DISMISS_FLING_THRESHOLD = 0.1f; |
| private static final float DISMISS_SWIPE_THRESHOLD = 0.75f; |
| private static final float FULL_THRESHOLD = 0.5f; |
| private static final float VERTICAL_FLING_SHOW_THRESHOLD = 0.2f; |
| @@ -83,7 +83,8 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| private static final int DRAGGED_CANCEL = 0; |
| private static final int DRAGGED_RIGHT = 1; |
| - protected static final long MS_ANIMATION_DURATION = 300; |
| + protected static final long MS_ANIMATION_DURATION = 250; |
| + private static final long MS_DISMISS_FLING_THRESHOLD = MS_ANIMATION_DURATION * 2; |
| // Detects when the user is dragging the View. |
| private final GestureDetector mGestureDetector; |
| @@ -91,18 +92,30 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| // Detects when the user is dragging the ContentView. |
| private final GestureStateListener mGestureStateListener; |
| + // Monitors for animation completions and resets the state. |
| + private final AnimatorListenerAdapter mAnimatorListenerAdapter; |
| + |
| + // Interpolator used for the animation. |
| + private final Interpolator mInterpolator; |
| + |
| // Tracks whether the user is scrolling or flinging. |
| private int mGestureState; |
| // Animation currently being used to translate the View. |
| private AnimatorSet mCurrentAnimation; |
| - // Whether or not the current animation is dismissing the View. |
| - private boolean mIsAnimationRemovingView; |
| + // Whether or not the current animation is adding or removing the View. |
| + private boolean mIsAnimationAddingOrRemovingView; |
| // Direction the user is horizontally dragging. |
| private int mDragDirection; |
| + // How quickly the user is horizontally dragging. |
| + private float mDragXPerMs; |
| + |
| + // WHen the user first started dragging. |
| + private long mDragStartMs; |
| + |
| // Used to determine when the layout has changed and the Viewport must be updated. |
| private int mParentHeight; |
| @@ -126,6 +139,8 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| mGestureDetector = new GestureDetector(context, gestureListener); |
| mGestureStateListener = createGestureStateListener(); |
| mGestureState = GESTURE_NONE; |
| + mAnimatorListenerAdapter = createAnimatorListenerAdapter(); |
| + mInterpolator = new DecelerateInterpolator(1.0f); |
| } |
| /** |
| @@ -136,7 +151,7 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| contentView.addView(this, 0, createLayoutParams()); |
| contentView.getContentViewCore().addGestureStateListener(mGestureStateListener); |
| - // Listen for the layout to know when to animate the banner coming onto the screen. |
| + // Listen for the layout to know when to animate the View coming onto the screen. |
| addOnLayoutChangeListener(createLayoutChangeListener()); |
| } |
| @@ -168,7 +183,7 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| */ |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| - // Hide the banner when the keyboard is showing. |
| + // Hide the View when the keyboard is showing. |
| boolean keyboardIsShowing = UiUtils.isKeyboardShowing(getContext(), this); |
| setVisibility(keyboardIsShowing ? INVISIBLE : VISIBLE); |
| @@ -207,27 +222,28 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| } |
| /** |
| - * Creates a listener that monitors horizontal dismissal gestures performed on the View. |
| + * Creates a listener that monitors horizontal gestures performed on the View. |
| * @return The SimpleOnGestureListener that will monitor the View. |
| */ |
| private SimpleOnGestureListener createGestureListener() { |
| return new SimpleOnGestureListener() { |
| @Override |
| public boolean onDown(MotionEvent e) { |
| + mGestureState = GESTURE_SCROLLING; |
| mDragDirection = DRAGGED_CANCEL; |
| - mGestureState = GESTURE_NONE; |
| + mDragXPerMs = 0; |
| + mDragStartMs = e.getEventTime(); |
| return true; |
| } |
| @Override |
| public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY) { |
| - mGestureState = GESTURE_SCROLLING; |
| float distance = e2.getX() - e1.getX(); |
| setTranslationX(getTranslationX() + distance); |
| setAlpha(calculateAnimationAlpha()); |
| - // Because the fling velocity is unreliable, we track what direction the user is |
| - // dragging the View from here. |
| + // Because the Android-calculated fling velocity is highly unreliable, we track what |
| + // direction the user is dragging the View from here. |
| mDragDirection = distance < 0 ? DRAGGED_LEFT : DRAGGED_RIGHT; |
|
Ted C
2014/03/06 23:37:50
if the direction changes, should you reset the dra
gone
2014/03/07 01:09:49
Unsure... I think that could lead to some really w
|
| return true; |
| } |
| @@ -235,6 +251,17 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| @Override |
| public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) { |
| mGestureState = GESTURE_FLINGING; |
| + |
| + // The direction and speed of the Android-given velocity feels completely disjoint |
| + // from what the user actually perceives. |
| + float androidXPerMs = Math.abs(vX) / 1000.0f; |
| + |
| + // Track how quickly the user has translated the view to this point. |
| + float dragXPerMs = Math.abs(getTranslationX()) / (e2.getEventTime() - mDragStartMs); |
| + |
| + // Check if the velocity from the user's drag is higher; if so, use that one |
| + // instead since that often feels more correct. |
| + mDragXPerMs = mDragDirection * Math.max(androidXPerMs, dragXPerMs); |
| createHorizontalAnimation(); |
| return true; |
| } |
| @@ -244,6 +271,11 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| onViewClicked(); |
| return true; |
| } |
| + |
| + @Override |
| + public void onShowPress(MotionEvent e) { |
| + onViewPressed(e); |
| + } |
| }; |
| } |
| @@ -277,7 +309,7 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| boolean show = !isScrollingDownward; |
| if (isVisibleInitially) { |
| - // Check if the banner was moving off-screen. |
| + // Check if the View was moving off-screen. |
| boolean isHiding = getTranslationY() > mInitialTranslationY; |
| show &= isVisibleEnough || !isHiding; |
| } else { |
| @@ -330,8 +362,9 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| removeOnLayoutChangeListener(this); |
| - // Animate the banner coming in from the bottom of the screen. |
| + // Animate the View coming in from the bottom of the screen. |
| setTranslationY(mTotalHeight); |
| + mIsAnimationAddingOrRemovingView = true; |
| createVerticalSnapAnimation(true); |
| mCurrentAnimation.start(); |
| } |
| @@ -352,7 +385,7 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| } |
| /** |
| - * Dismisses the banner, animating the banner moving vertically off of the screen if needed. |
| + * Dismisses the View, animating it moving vertically off of the screen if needed. |
| */ |
| void dismiss() { |
| if (getParent() == null) return; |
| @@ -367,7 +400,7 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| * Calculates how transparent the View should be. |
| * |
| * The transparency value is proportional to how far the View has been swiped away from the |
| - * center of the screen. The {@link ALPHA_THRESHOLD} determines at what point the banner should |
| + * center of the screen. The {@link ALPHA_THRESHOLD} determines at what point the View should |
| * start fading away. |
| * @return The alpha value to use for the View. |
| */ |
| @@ -388,29 +421,38 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| * and marked as a manual removal. |
| */ |
| private void createHorizontalAnimation() { |
| - // Determine where the banner needs to move. Because of the unreliability of the fling |
| + long duration = MS_ANIMATION_DURATION; |
| + |
| + // Determine where the View needs to move. Because of the unreliability of the fling |
| // velocity, we ignore it and instead rely on the direction the user was last dragging the |
| // View. Moreover, we lower the translation threshold for dismissal. |
| boolean isFlinging = mGestureState == GESTURE_FLINGING; |
| - float dismissThreshold = |
| - getWidth() * (isFlinging ? DISMISS_FLING_THRESHOLD : DISMISS_SWIPE_THRESHOLD); |
| - boolean isSwipedFarEnough = Math.abs(getTranslationX()) > dismissThreshold; |
| - boolean isFlungInSameDirection = |
| - (getTranslationX() < 0.0f) == (mDragDirection == DRAGGED_LEFT); |
| - if (!isSwipedFarEnough || (isFlinging && !isFlungInSameDirection)) { |
| - mDragDirection = DRAGGED_CANCEL; |
| + |
| + // If the user hasn't tried hard enough to dismiss the View, move it back to the center. |
| + if (isFlinging) { |
| + // Assuming a linear velocity, allow flinging the View away if it would fly off the |
| + // screen in a reasonable time frame. |
| + float remainingDifference = mDragDirection * getWidth() - getTranslationX(); |
|
Ted C
2014/03/06 23:37:50
should this not be a function of the parent's size
gone
2014/03/07 01:09:49
The banner doesn't span the whole screen, and are
|
| + float msRequired = Math.abs(remainingDifference / mDragXPerMs); |
| + |
| + if (msRequired < MS_DISMISS_FLING_THRESHOLD) { |
| + duration = (long) msRequired; |
| + } else { |
| + mDragDirection = DRAGGED_CANCEL; |
| + } |
| + } else { |
| + // Check if the user has dragged the View far enough to be dismissed. |
| + float dismissPercentage = DISMISS_SWIPE_THRESHOLD; |
| + float dismissThreshold = getWidth() * dismissPercentage; |
| + if (Math.abs(getTranslationX()) < dismissThreshold) mDragDirection = DRAGGED_CANCEL; |
| } |
| // Set up the animation parameters. |
| float finalAlpha = mDragDirection == DRAGGED_CANCEL ? 1.0f : 0.0f; |
| float finalX = mDragDirection * getWidth(); |
| - float xDifference = Math.abs(finalX - getTranslationX()) / getWidth(); |
| - long duration = (long) (MS_ANIMATION_DURATION * xDifference); |
| - |
| - // Dismiss the banner if it's going off the screen. |
| - boolean translatedOffScreen = mDragDirection != DRAGGED_CANCEL; |
| + boolean isSwipedAway = mDragDirection != DRAGGED_CANCEL; |
| - createAnimation(finalAlpha, finalX, getTranslationY(), duration, translatedOffScreen); |
| + createAnimation(finalAlpha, finalX, getTranslationY(), duration, isSwipedAway); |
| } |
| /** |
| @@ -421,8 +463,7 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| * @param duration How long the animation should run for. |
| * @param remove If true, remove the View from its parent ViewGroup. |
| */ |
| - private void createAnimation(float alpha, float x, float y, long duration, |
| - final boolean remove) { |
| + private void createAnimation(float alpha, float x, float y, long duration, boolean remove) { |
| Animator alphaAnimator = |
| ObjectAnimator.ofPropertyValuesHolder(this, |
| PropertyValuesHolder.ofFloat("alpha", getAlpha(), alpha)); |
| @@ -433,21 +474,30 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| ObjectAnimator.ofPropertyValuesHolder(this, |
| PropertyValuesHolder.ofFloat("translationY", getTranslationY(), y)); |
| - mIsAnimationRemovingView = remove; |
| + mIsAnimationAddingOrRemovingView = remove; |
| mCurrentAnimation = new AnimatorSet(); |
| mCurrentAnimation.setDuration(duration); |
| mCurrentAnimation.playTogether(alphaAnimator, translationXAnimator, translationYAnimator); |
| - mCurrentAnimation.addListener(new AnimatorListenerAdapter() { |
| + mCurrentAnimation.addListener(mAnimatorListenerAdapter); |
| + mCurrentAnimation.setInterpolator(mInterpolator); |
| + mCurrentAnimation.start(); |
| + } |
| + |
| + /** |
| + * Creates an AnimatorListenerAdapter that cleans up after an animation is completed. |
| + * @return {@link AnimatorListenerAdapter} to use for animations. |
| + */ |
| + private AnimatorListenerAdapter createAnimatorListenerAdapter() { |
| + return new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
|
Ted C
2014/03/06 23:37:50
You might want to handle onAnimationCancel because
gone
2014/03/07 01:09:49
Shouldn't happen: cancelCurrentAnimation() explici
|
| mGestureState = GESTURE_NONE; |
| mCurrentAnimation = null; |
| - mIsAnimationRemovingView = false; |
| - if (remove) removeFromParent(); |
| + |
| + if (mIsAnimationAddingOrRemovingView) removeFromParent(); |
| + mIsAnimationAddingOrRemovingView = false; |
| } |
| - }); |
| - mCurrentAnimation.setInterpolator(new AccelerateInterpolator()); |
| - mCurrentAnimation.start(); |
| + }; |
| } |
| /** |
| @@ -465,7 +515,7 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| * @return True if the animation was canceled or wasn't running, false otherwise. |
| */ |
| private boolean cancelCurrentAnimation() { |
| - if (mIsAnimationRemovingView) return false; |
| + if (mIsAnimationAddingOrRemovingView) return false; |
| if (mCurrentAnimation != null) mCurrentAnimation.cancel(); |
| return true; |
| } |
| @@ -474,4 +524,9 @@ public abstract class SwipableOverlayView extends FrameLayout { |
| * Called when the View has been clicked. |
| */ |
| protected abstract void onViewClicked(); |
| + |
| + /** |
| + * Called when the View needs to show that it's been pressed. |
| + */ |
| + protected abstract void onViewPressed(MotionEvent event); |
| } |