This source file includes following definitions.
- addToView
- createLayoutParams
- removeFromParent
- onLayout
- onTouchEvent
- createGestureListener
- onFinishHorizontalGesture
- createGestureStateListener
- createLayoutChangeListener
- createVerticalSnapAnimation
- createHorizontalSnapAnimation
- dismiss
- isDismissed
- calculateAnimationAlpha
- computeScrollDifference
- determineFinalHorizontalLocation
- calculateMsRequiredToFlingOffScreen
- createAnimation
- createAnimatorListenerAdapter
- beginGesture
- cancelCurrentAnimation
- mayCancelCurrentAnimation
- onViewSwipedAway
- onViewClicked
- onViewPressed
package org.chromium.chrome.browser.banners;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import org.chromium.content.browser.ContentView;
import org.chromium.content_public.browser.GestureStateListener;
import org.chromium.ui.UiUtils;
public abstract class SwipableOverlayView extends FrameLayout {
private static final float ALPHA_THRESHOLD = 0.25f;
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;
private static final float VERTICAL_FLING_HIDE_THRESHOLD = 0.9f;
protected static final float ZERO_THRESHOLD = 0.001f;
private static final int GESTURE_NONE = 0;
private static final int GESTURE_SCROLLING = 1;
private static final int GESTURE_FLINGING = 2;
private static final int DRAGGED_LEFT = -1;
private static final int DRAGGED_CANCEL = 0;
private static final int DRAGGED_RIGHT = 1;
protected static final long MS_ANIMATION_DURATION = 250;
private static final long MS_DISMISS_FLING_THRESHOLD = MS_ANIMATION_DURATION * 2;
private static final long MS_SLOW_DISMISS = MS_ANIMATION_DURATION * 3;
private final GestureDetector mGestureDetector;
private final GestureStateListener mGestureStateListener;
private final AnimatorListenerAdapter mAnimatorListenerAdapter;
private final Interpolator mInterpolator;
private int mGestureState;
private AnimatorSet mCurrentAnimation;
private int mDragDirection;
private float mDragXPerMs;
private long mDragStartMs;
private int mParentHeight;
private float mInitialTranslationY;
private int mInitialOffsetY;
private int mTotalHeight;
private boolean mIsBeingDisplayedForFirstTime;
private boolean mIsDismissed;
public SwipableOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
SimpleOnGestureListener gestureListener = createGestureListener();
mGestureDetector = new GestureDetector(context, gestureListener);
mGestureStateListener = createGestureStateListener();
mGestureState = GESTURE_NONE;
mAnimatorListenerAdapter = createAnimatorListenerAdapter();
mInterpolator = new DecelerateInterpolator(1.0f);
}
protected void addToView(ContentView contentView) {
contentView.addView(this, 0, createLayoutParams());
contentView.getContentViewCore().addGestureStateListener(mGestureStateListener);
addOnLayoutChangeListener(createLayoutChangeListener());
}
protected ViewGroup.MarginLayoutParams createLayoutParams() {
return new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
}
boolean removeFromParent() {
if (getParent() instanceof ContentView) {
ContentView parent = (ContentView) getParent();
parent.removeView(this);
parent.getContentViewCore().removeGestureStateListener(mGestureStateListener);
return true;
}
return false;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
boolean keyboardIsShowing = UiUtils.isKeyboardShowing(getContext(), this);
setVisibility(keyboardIsShowing ? INVISIBLE : VISIBLE);
int currentParentHeight = getParent() == null ? 0 : ((View) getParent()).getHeight();
if (mParentHeight != currentParentHeight) {
mParentHeight = currentParentHeight;
mGestureState = GESTURE_NONE;
if (mCurrentAnimation != null) mCurrentAnimation.end();
}
mTotalHeight = getMeasuredHeight();
if (getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
mTotalHeight += params.topMargin + params.bottomMargin;
}
super.onLayout(changed, l, t, r, b);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mGestureDetector.onTouchEvent(event)) return true;
if (mCurrentAnimation != null) return true;
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
onFinishHorizontalGesture();
return true;
}
return false;
}
private SimpleOnGestureListener createGestureListener() {
return new SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mGestureState = GESTURE_SCROLLING;
mDragDirection = DRAGGED_CANCEL;
mDragXPerMs = 0;
mDragStartMs = e.getEventTime();
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY) {
float distance = e2.getX() - e1.getX();
setTranslationX(getTranslationX() + distance);
setAlpha(calculateAnimationAlpha());
mDragDirection = distance < 0 ? DRAGGED_LEFT : DRAGGED_RIGHT;
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) {
mGestureState = GESTURE_FLINGING;
float androidXPerMs = Math.abs(vX) / 1000.0f;
float dragXPerMs = Math.abs(getTranslationX()) / (e2.getEventTime() - mDragStartMs);
mDragXPerMs = mDragDirection * Math.max(androidXPerMs, dragXPerMs);
onFinishHorizontalGesture();
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
onViewClicked();
return true;
}
@Override
public void onShowPress(MotionEvent e) {
onViewPressed(e);
}
};
}
private void onFinishHorizontalGesture() {
mDragDirection = determineFinalHorizontalLocation();
if (mDragDirection == DRAGGED_CANCEL) {
createHorizontalSnapAnimation(true);
} else {
onViewSwipedAway();
dismiss(true);
}
}
private GestureStateListener createGestureStateListener() {
return new GestureStateListener() {
@Override
public void onFlingStartGesture(int vx, int vy, int scrollOffsetY, int scrollExtentY) {
if (!cancelCurrentAnimation()) return;
if (mGestureState == GESTURE_NONE) beginGesture(scrollOffsetY, scrollExtentY);
mGestureState = GESTURE_FLINGING;
}
@Override
public void onFlingEndGesture(int scrollOffsetY, int scrollExtentY) {
if (mGestureState != GESTURE_FLINGING) return;
mGestureState = GESTURE_NONE;
int finalOffsetY = computeScrollDifference(scrollOffsetY, scrollExtentY);
updateTranslation(scrollOffsetY, scrollExtentY);
boolean isScrollingDownward = finalOffsetY > 0;
boolean isVisibleInitially = mInitialTranslationY < mTotalHeight;
float percentageVisible = 1.0f - (getTranslationY() / mTotalHeight);
float visibilityThreshold = isVisibleInitially
? VERTICAL_FLING_HIDE_THRESHOLD : VERTICAL_FLING_SHOW_THRESHOLD;
boolean isVisibleEnough = percentageVisible > visibilityThreshold;
boolean show = !isScrollingDownward;
if (isVisibleInitially) {
boolean isHiding = getTranslationY() > mInitialTranslationY;
show &= isVisibleEnough || !isHiding;
} else {
boolean isNearTopOfPage = finalOffsetY < (mTotalHeight * FULL_THRESHOLD);
show &= isVisibleEnough || isNearTopOfPage;
}
createVerticalSnapAnimation(show);
}
@Override
public void onScrollStarted(int scrollOffsetY, int scrollExtentY) {
if (!cancelCurrentAnimation()) return;
if (mGestureState == GESTURE_NONE) beginGesture(scrollOffsetY, scrollExtentY);
mGestureState = GESTURE_SCROLLING;
}
@Override
public void onScrollEnded(int scrollOffsetY, int scrollExtentY) {
if (mGestureState != GESTURE_SCROLLING) return;
mGestureState = GESTURE_NONE;
int finalOffsetY = computeScrollDifference(scrollOffsetY, scrollExtentY);
updateTranslation(scrollOffsetY, scrollExtentY);
boolean isNearTopOfPage = finalOffsetY < (mTotalHeight * FULL_THRESHOLD);
boolean isVisibleEnough = getTranslationY() < mTotalHeight * FULL_THRESHOLD;
createVerticalSnapAnimation(isNearTopOfPage || isVisibleEnough);
}
@Override
public void onScrollOffsetOrExtentChanged(int scrollOffsetY, int scrollExtentY) {
if (mGestureState == GESTURE_NONE || !cancelCurrentAnimation()) return;
updateTranslation(scrollOffsetY, scrollExtentY);
}
private void updateTranslation(int scrollOffsetY, int scrollExtentY) {
float translation = mInitialTranslationY
+ computeScrollDifference(scrollOffsetY, scrollExtentY);
translation = Math.max(0.0f, Math.min(mTotalHeight, translation));
setTranslationY(translation);
}
};
}
private View.OnLayoutChangeListener createLayoutChangeListener() {
return new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
removeOnLayoutChangeListener(this);
setTranslationY(mTotalHeight);
mIsBeingDisplayedForFirstTime = true;
createVerticalSnapAnimation(true);
mCurrentAnimation.start();
}
};
}
void createVerticalSnapAnimation(boolean visible) {
float translationY = visible ? 0.0f : mTotalHeight;
float yDifference = Math.abs(translationY - getTranslationY()) / mTotalHeight;
long duration = (long) (MS_ANIMATION_DURATION * yDifference);
createAnimation(1.0f, 0, translationY, duration);
}
private void createHorizontalSnapAnimation(boolean visible) {
if (visible) {
createAnimation(1.0f, 0.0f, getTranslationY(), MS_ANIMATION_DURATION);
} else {
if (mDragDirection == DRAGGED_CANCEL) {
mDragDirection = DRAGGED_LEFT;
}
float finalX = mDragDirection * getWidth();
long duration = MS_ANIMATION_DURATION;
switch (mGestureState) {
case GESTURE_FLINGING:
duration = (long) calculateMsRequiredToFlingOffScreen();
break;
case GESTURE_NONE:
duration = MS_SLOW_DISMISS;
break;
default:
break;
}
createAnimation(0.0f, finalX, getTranslationY(), duration);
}
}
protected boolean dismiss(boolean horizontally) {
if (getParent() == null || mIsDismissed) return false;
mIsDismissed = true;
if (horizontally) {
createHorizontalSnapAnimation(false);
} else {
createVerticalSnapAnimation(false);
}
return true;
}
protected boolean isDismissed() {
return mIsDismissed;
}
private float calculateAnimationAlpha() {
float percentageSwiped = Math.abs(getTranslationX() / getWidth());
float percentageAdjusted = Math.max(0.0f, percentageSwiped - ALPHA_THRESHOLD);
float alphaRange = 1.0f - ALPHA_THRESHOLD;
return 1.0f - percentageAdjusted / alphaRange;
}
private int computeScrollDifference(int scrollOffsetY, int scrollExtentY) {
return scrollOffsetY + scrollExtentY - mInitialOffsetY;
}
private int determineFinalHorizontalLocation() {
if (mGestureState == GESTURE_FLINGING) {
float msRequired = calculateMsRequiredToFlingOffScreen();
if (msRequired > MS_DISMISS_FLING_THRESHOLD) return DRAGGED_CANCEL;
} else if (mGestureState == GESTURE_SCROLLING) {
float dismissPercentage = DISMISS_SWIPE_THRESHOLD;
float dismissThreshold = getWidth() * dismissPercentage;
if (Math.abs(getTranslationX()) < dismissThreshold) return DRAGGED_CANCEL;
}
return mDragDirection;
}
private float calculateMsRequiredToFlingOffScreen() {
float remainingDifference = mDragDirection * getWidth() - getTranslationX();
return Math.abs(remainingDifference / mDragXPerMs);
}
private void createAnimation(float alpha, float x, float y, long duration) {
Animator alphaAnimator =
ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat("alpha", getAlpha(), alpha));
Animator translationXAnimator =
ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat("translationX", getTranslationX(), x));
Animator translationYAnimator =
ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat("translationY", getTranslationY(), y));
mCurrentAnimation = new AnimatorSet();
mCurrentAnimation.setDuration(duration);
mCurrentAnimation.playTogether(alphaAnimator, translationXAnimator, translationYAnimator);
mCurrentAnimation.addListener(mAnimatorListenerAdapter);
mCurrentAnimation.setInterpolator(mInterpolator);
mCurrentAnimation.start();
}
private AnimatorListenerAdapter createAnimatorListenerAdapter() {
return new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mIsDismissed) removeFromParent();
mGestureState = GESTURE_NONE;
mCurrentAnimation = null;
mIsBeingDisplayedForFirstTime = false;
}
};
}
private void beginGesture(int scrollOffsetY, int scrollExtentY) {
mInitialTranslationY = getTranslationY();
boolean isInitiallyVisible = mInitialTranslationY < mTotalHeight;
int startingY = isInitiallyVisible ? scrollOffsetY : Math.min(scrollOffsetY, mTotalHeight);
mInitialOffsetY = startingY + scrollExtentY;
}
private boolean cancelCurrentAnimation() {
if (!mayCancelCurrentAnimation()) return false;
if (mCurrentAnimation != null) mCurrentAnimation.cancel();
return true;
}
private boolean mayCancelCurrentAnimation() {
return !mIsBeingDisplayedForFirstTime && !mIsDismissed;
}
protected abstract void onViewSwipedAway();
protected abstract void onViewClicked();
protected abstract void onViewPressed(MotionEvent event);
}