public class DampingReboundLayout extends ViewGroup implements NestedScrollingParent3,
NestedScrollingParent2, NestedScrollingChild3, NestedScrollingChild2, NestedScrollingParent,
NestedScrollingChild {
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
private float DRAG_RATE = .35f;
private ValueAnimator valueAnimator;
public void setDragRate(int dragRate) {
this.DRAG_RATE = dragRate;
}
// Default offset in dips from the top of the view to where the progress spinner should stop
private static final int DEFAULT_CIRCLE_TARGET = 200;
private View mTarget; // the target of the gesture
// If nested scrolling is enabled, the total amount that needed to be
// consumed by this as the nested scrolling parent is used in place of the
// overscroll determined by MOVE events in the onTouch handler
private float mTotalUnconsumed;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final NestedScrollingChildHelper mNestedScrollingChildHelper;
private final int[] mParentScrollConsumed = new int[2];
private final int[] mParentOffsetInWindow = new int[2];
// Used for calls from old versions of onNestedScroll to v3 version of onNestedScroll. This only
// exists to prevent GC costs that are present before API 21.
private final int[] mNestedScrollingV2ConsumedCompat = new int[2];
private boolean mNestedScrollInProgress;
private float mInitialScroll;
// damping rebound orientation
// 0: horizontal
// 1: vertical
private int dampingReboundOrientation = LinearLayoutCompat.HORIZONTAL;
public void setOrientation(@LinearLayoutCompat.OrientationMode int orientation) {
dampingReboundOrientation = orientation;
}
// Target is returning to its start offset because it was cancelled or a
// refresh was triggered.
private final DecelerateInterpolator mDecelerateInterpolator;
private static final int[] LAYOUT_ATTRS = new int[]{
android.R.attr.enabled
};
private int mLinearLayoutViewIndex = -1;
/**
* @see #setLegacyRequestDisallowInterceptTouchEventEnabled
*/
private boolean mEnableLegacyRequestDisallowInterceptTouch;
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
/**
* Simple constructor to use when creating a SwipeRefreshLayout from code.
*
* @param context
*/
public DampingReboundLayout(@NonNull Context context) {
this(context, null);
}
/**
* Constructor that is called when inflating SwipeRefreshLayout from XML.
*
* @param context
* @param attrs
*/
public DampingReboundLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DampingReboundLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
final DisplayMetrics metrics = getResources().getDisplayMetrics();
setChildrenDrawingOrderEnabled(true);
// the absolute offset has to take into account that the circle starts at an offset
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
if (mLinearLayoutViewIndex < 0) {
return i;
} else if (i == childCount - 1) {
// Draw the selected child last
return mLinearLayoutViewIndex;
} else if (i >= mLinearLayoutViewIndex) {
// Move the children after the selected child earlier one
return i + 1;
} else {
// Keep the children before the selected child the same
return i;
}
}
private void ensureTarget() {
// Don't bother getting the parent height if the parent hasn't been laid
// out yet.
if (mTarget == null) {
mTarget = getChildAt(0);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
}
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*/
public boolean canChildScrollUp() {
if (mTarget instanceof ListView) {
return ListViewCompat.canScrollList((ListView) mTarget, -1);
}
return dampingReboundOrientation == 1 ? mTarget.canScrollVertically(-1) : mTarget.canScrollHorizontally(-1);
}
public boolean canChildScrollDown() {
if (mTarget instanceof ListView) {
return ListViewCompat.canScrollList((ListView) mTarget, 1);
}
return dampingReboundOrientation == 1 ? mTarget.canScrollVertically(1) : mTarget.canScrollHorizontally(1);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = ev.getActionMasked();
int pointerIndex;
if (!isEnabled() || canChildScrollUp() || canChildScrollDown() || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
if (dampingReboundOrientation == 1) {
mInitialScroll = ev.getY();
} else {
mInitialScroll = ev.getX();
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
default:
}
return super.onInterceptTouchEvent(ev);
}
/**
* Enables the legacy behavior of {@link #requestDisallowInterceptTouchEvent} from before
* 1.1.0-alpha03, where the request is neither honored, nor propagated up to its parents,
* in either of the following two cases:
*
* - The child as an {@link AbsListView} and the runtime is API < 21
* - The child has nested scrolling disabled
*
* Use this method
only if your application:
*
* - is upgrading SwipeRefreshLayout from < 1.1.1 to >= 1.1.0-alpha03
* - has a SwipeRefreshLayout, or its parent, that no longer responds to touch events
* when it should
* - setting this method to {@code true} fixes that issue
*
*
* @param enabled {@code true} to enable the legacy behavior, {@code false} for default behavior
* @deprecated Only use this method if the changes introduced in
* {@link #requestDisallowInterceptTouchEvent} in version 1.1.0-alpha03 and 1.1.1
* are breaking your application.
*/
@Deprecated
public void setLegacyRequestDisallowInterceptTouchEventEnabled(boolean enabled) {
mEnableLegacyRequestDisallowInterceptTouch = enabled;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
if (mEnableLegacyRequestDisallowInterceptTouch
&& ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
|| (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget)))) {
// Legacy behavior: if this is a List < L or another view that doesn't support
// nested scrolling, ignore this request so that the vertical scroll event
// isn't stolen
return;
}
super.requestDisallowInterceptTouchEvent(b);
}
// NestedScrollingParent 3
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type,
@NonNull int[] consumed) {
if (type != ViewCompat.TYPE_TOUCH) {
return;
}
int consumedBeforeParents = dampingReboundOrientation == 1 ? consumed[1] : consumed[0];
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow, type, consumed);
int consumedByParents = (dampingReboundOrientation == 1 ? consumed[1] : consumed[0]) - consumedBeforeParents;
int unconsumedAfterParents = dampingReboundOrientation == 1 ? dyUnconsumed : dxUnconsumed - consumedByParents;
// There are two reasons why scroll distance may be totally consumed. 1) All of the nested
// scrolling parents up the hierarchy implement NestedScrolling3 and consumed all of the
// distance or 2) at least 1 nested scrolling parent doesn't implement NestedScrolling3 and
// for comparability reasons, we are supposed to act like they have.
//
// We must assume 2) is the case because we have no way of determining that it isn't, and
// therefore must fallback to a previous hack that was done before nested scrolling 3
// existed.
int remainingDistanceToScroll;
if (unconsumedAfterParents == 0) {
// The previously implemented hack is to see how far we were offset and assume that that
// distance is equal to how much all of our parents consumed.
remainingDistanceToScroll = dampingReboundOrientation == 1 ? (dyUnconsumed + mParentOffsetInWindow[1]) : dxUnconsumed + mParentOffsetInWindow[0];
} else {
remainingDistanceToScroll = unconsumedAfterParents;
}
// Not sure why we have to make sure the child can't scroll up... but seems dangerous to
// remove.
if (remainingDistanceToScroll < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(remainingDistanceToScroll);
moveSpinner(mTotalUnconsumed);
// If we've gotten here, we need to consume whatever is left to consume, which at this
// point is either equal to 0, or remainingDistanceToScroll.
if (dampingReboundOrientation == 1) {
consumed[1] += unconsumedAfterParents;
} else {
consumed[0] += unconsumedAfterParents;
}
}
if (remainingDistanceToScroll > 0 && !canChildScrollDown()) {
mTotalUnconsumed -= Math.abs(remainingDistanceToScroll);
moveSpinner(mTotalUnconsumed);
// If we've gotten here, we need to consume whatever is left to consume, which at this
// point is either equal to 0, or remainingDistanceToScroll.
if (dampingReboundOrientation == 1) {
consumed[1] -= unconsumedAfterParents;
} else {
consumed[0] -= unconsumedAfterParents;
}
}
}
// NestedScrollingParent 2
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
if (type == ViewCompat.TYPE_TOUCH) {
return onStartNestedScroll(child, target, axes);
} else {
return false;
}
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes, int type) {
// Should always be true because onStartNestedScroll returns false for all type !=
// ViewCompat.TYPE_TOUCH, but check just in case.
if (type == ViewCompat.TYPE_TOUCH) {
onNestedScrollAccepted(child, target, axes);
}
}
@Override
public void onStopNestedScroll(View target, int type) {
// Should always be true because onStartNestedScroll returns false for all type !=
// ViewCompat.TYPE_TOUCH, but check just in case.
if (type == ViewCompat.TYPE_TOUCH) {
onStopNestedScroll(target);
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int type) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type,
mNestedScrollingV2ConsumedCompat);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
// Should always be true because onStartNestedScroll returns false for all type !=
// ViewCompat.TYPE_TOUCH, but check just in case.
if (type == ViewCompat.TYPE_TOUCH) {
onNestedPreScroll(target, dx, dy, consumed);
}
}
// NestedScrollingParent 1
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && dampingReboundOrientation == 1 ? (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 1 : (nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 2;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
// Reset the counter of how much leftover scroll needs to be consumed.
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
// Dispatch up to the nested parent
startNestedScroll(axes & (dampingReboundOrientation == 1 ? ViewCompat.SCROLL_AXIS_VERTICAL : ViewCompat.SCROLL_AXIS_HORIZONTAL));
mTotalUnconsumed = 0;
mNestedScrollInProgress = true;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// If we are in the middle of consuming, a scroll, then we want to move the spinner back up
// before allowing the list to scroll
if (dampingReboundOrientation == 1) {
if (dy > 0 && mTotalUnconsumed > 0 || dy < 0 && mTotalUnconsumed < 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;
}
moveSpinner(mTotalUnconsumed);
}
} else {
if (dx > 0 && mTotalUnconsumed > 0 || dx < 0 && mTotalUnconsumed < 0) {
if (dx > mTotalUnconsumed) {
consumed[0] = (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dx;
consumed[0] = dx;
}
moveSpinner(mTotalUnconsumed);
}
}
// If a client layout is using a custom start position for the circle
// view, they mean to hide it again before scrolling the child view
// If we get back to mTotalUnconsumed == 0 and there is more to go, hide
// the circle so it isn't exposed if its blocking content is moved
// Now let our nested parent consume the leftovers
final int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
mNestedScrollInProgress = false;
// Finish the spinner for nested scrolling if we ever consumed any
// unconsumed nested scroll
if (mTotalUnconsumed != 0) {
finishSpinner();
mTotalUnconsumed = 0;
}
// Dispatch up our nested parent
stopNestedScroll();
}
@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
ViewCompat.TYPE_TOUCH, mNestedScrollingV2ConsumedCompat);
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY,
boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}
// NestedScrollingChild 3
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
@NonNull int[] consumed) {
if (type == ViewCompat.TYPE_TOUCH) {
mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed,
dyUnconsumed, offsetInWindow, type, consumed);
}
}
// NestedScrollingChild 2
@Override
public boolean startNestedScroll(int axes, int type) {
return type == ViewCompat.TYPE_TOUCH && startNestedScroll(axes);
}
@Override
public void stopNestedScroll(int type) {
if (type == ViewCompat.TYPE_TOUCH) {
stopNestedScroll();
}
}
@Override
public boolean hasNestedScrollingParent(int type) {
return type == ViewCompat.TYPE_TOUCH && hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type) {
return type == ViewCompat.TYPE_TOUCH && mNestedScrollingChildHelper.dispatchNestedScroll(
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return type == ViewCompat.TYPE_TOUCH && dispatchNestedPreScroll(dx, dy, consumed,
offsetInWindow);
}
// NestedScrollingChild 1
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(
dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
private void moveSpinner(float overScrollTop) {
float scroll = overScrollTop * DRAG_RATE;
if (dampingReboundOrientation == 1) {
if (scroll > getHeight() / 2f) {
scroll = getHeight() / 2f;
}
if (scroll < -getHeight() / 2f) {
scroll = -getHeight() / 2f;
}
mTarget.setTranslationY(scroll);
} else {
if (scroll > getWidth() / 2f) {
scroll = getWidth() / 2f;
}
if (scroll < -getWidth() / 2f) {
scroll = -getWidth() / 2f;
}
mTarget.setTranslationX(scroll);
}
}
private void finishSpinner() {
animateOffsetToStartPosition();
}
private void animateOffsetToStartPosition() {
valueAnimator = ValueAnimator.ofFloat(dampingReboundOrientation == 1 ? mTarget.getTranslationY() : mTarget.getTranslationX(), 0);
valueAnimator.setDuration(500);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (mTarget != null && animation != null) {
if (dampingReboundOrientation == 1) {
mTarget.setTranslationY((Float) animation.getAnimatedValue());
} else {
mTarget.setTranslationX((Float) animation.getAnimatedValue());
}
}
}
});
valueAnimator.setInterpolator(mDecelerateInterpolator);
valueAnimator.start();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
int pointerIndex = -1;
if (!isEnabled() || canChildScrollUp() || canChildScrollDown() || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE: {
final float y = ev.getY();
final float overScrollTop = (y - mInitialScroll);
// While the spinner is being dragged down, our parent shouldn't try
// to intercept touch events. It will stop the drag gesture abruptly.
getParent().requestDisallowInterceptTouchEvent(true);
moveSpinner(overScrollTop);
break;
}
case MotionEvent.ACTION_UP: {
finishSpinner();
return false;
}
case MotionEvent.ACTION_CANCEL:
return false;
}
return true;
}
}