diff --git a/core/java/com/android/internal/widget/RecyclerView.java b/core/java/com/android/internal/widget/RecyclerView.java index 89a90e944bdfc..17ce75e63b6fc 100644 --- a/core/java/com/android/internal/widget/RecyclerView.java +++ b/core/java/com/android/internal/widget/RecyclerView.java @@ -16,10 +16,16 @@ package com.android.internal.widget; +import static android.widget.EdgeEffect.TYPE_GLOW; +import static android.widget.EdgeEffect.TYPE_STRETCH; +import static android.widget.EdgeEffect.USE_STRETCH_EDGE_EFFECT_BY_DEFAULT; +import static android.widget.EdgeEffect.USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED; + import android.annotation.CallSuper; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.compat.Compatibility; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.TypedArray; @@ -460,6 +466,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro private final int[] mScrollConsumed = new int[2]; private final int[] mNestedOffsets = new int[2]; + private int mEdgeEffectType; + /** * These are views that had their a11y importance changed during a layout. We defer these events * until the end of the layout because a11y service may make sync calls back to the RV while @@ -587,6 +595,14 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); } + boolean defaultToStretch = Compatibility.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_BY_DEFAULT) + || Compatibility.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED); + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.EdgeEffect); + mEdgeEffectType = a.getInt(com.android.internal.R.styleable.EdgeEffect_edgeEffectType, + defaultToStretch ? TYPE_STRETCH : TYPE_GLOW); + a.recycle(); + // Re-set whether nested scrolling is enabled so that it is set on all API levels setNestedScrollingEnabled(nestedScrollingEnabled); } @@ -609,6 +625,28 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro setAccessibilityDelegate(mAccessibilityDelegate); } + /** + * Returns the {@link EdgeEffect#getType()} used for all EdgeEffects. + * + * @return @link EdgeEffect#getType()} used for all EdgeEffects. + */ + @EdgeEffect.EdgeEffectType + public int getEdgeEffectType() { + return mEdgeEffectType; + } + + /** + * Sets the {@link EdgeEffect#getType()} used in all EdgeEffects. + * Any existing over-scroll effects are cleared and new effects are created as needed. + * + * @param type the {@link EdgeEffect#getType()} used in all EdgeEffects. + */ + public void setEdgeEffectType(@EdgeEffect.EdgeEffectType int type) { + mEdgeEffectType = type; + invalidateGlows(); + invalidate(); + } + /** * Instantiate and set a LayoutManager, if specified in the attributes. */ @@ -2183,6 +2221,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return; } mLeftGlow = new EdgeEffect(getContext()); + mLeftGlow.setType(mEdgeEffectType); if (mClipToPadding) { mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); @@ -2196,6 +2235,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return; } mRightGlow = new EdgeEffect(getContext()); + mRightGlow.setType(mEdgeEffectType); if (mClipToPadding) { mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); @@ -2209,6 +2249,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return; } mTopGlow = new EdgeEffect(getContext()); + mTopGlow.setType(mEdgeEffectType); if (mClipToPadding) { mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); @@ -2223,6 +2264,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return; } mBottomGlow = new EdgeEffect(getContext()); + mBottomGlow.setType(mEdgeEffectType); if (mClipToPadding) { mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); @@ -2663,7 +2705,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); - if (mScrollState == SCROLL_STATE_SETTLING) { + if (stopGlowAnimations(e) || mScrollState == SCROLL_STATE_SETTLING) { getParent().requestDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } @@ -2731,6 +2773,38 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return mScrollState == SCROLL_STATE_DRAGGING; } + /** + * This stops any edge glow animation that is currently running by applying a + * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices, + * this method does nothing, allowing any animating edge effect to continue animating and + * returning false always. + * + * @param e The motion event to use to indicate the finger position for the displacement of + * the current pull. + * @return true if any edge effect had an existing effect to be drawn ond the + * animation was stopped or false if no edge effect had a value to display. + */ + private boolean stopGlowAnimations(MotionEvent e) { + boolean stopped = false; + if (mLeftGlow != null && mLeftGlow.getDistance() != 0) { + mLeftGlow.onPullDistance(0, 1 - (e.getY() / getHeight())); + stopped = true; + } + if (mRightGlow != null && mRightGlow.getDistance() != 0) { + mRightGlow.onPullDistance(0, e.getY() / getHeight()); + stopped = true; + } + if (mTopGlow != null && mTopGlow.getDistance() != 0) { + mTopGlow.onPullDistance(0, e.getX() / getWidth()); + stopped = true; + } + if (mBottomGlow != null && mBottomGlow.getDistance() != 0) { + mBottomGlow.onPullDistance(0, 1 - e.getX() / getWidth()); + stopped = true; + } + return stopped; + } + @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { final int listenerCount = mOnItemTouchListeners.size(); @@ -2807,6 +2881,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro final int y = (int) (e.getY(index) + 0.5f); int dx = mLastTouchX - x; int dy = mLastTouchY - y; + dx -= releaseHorizontalGlow(dx, e.getY()); + dy -= releaseVerticalGlow(dy, e.getX()); if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { dx -= mScrollConsumed[0]; @@ -2887,6 +2963,72 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return true; } + /** + * If either of the horizontal edge glows are currently active, this consumes part or all of + * deltaX on the edge glow. + * + * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive + * for moving down and negative for moving up. + * @param y The vertical position of the pointer. + * @return The amount of deltaX that has been consumed by the + * edge glow. + */ + private int releaseHorizontalGlow(int deltaX, float y) { + // First allow releasing existing overscroll effect: + float consumed = 0; + float displacement = y / getHeight(); + float pullDistance = (float) deltaX / getWidth(); + if (mLeftGlow != null && mLeftGlow.getDistance() != 0) { + consumed = -mLeftGlow.onPullDistance(-pullDistance, 1 - displacement); + if (mLeftGlow.getDistance() == 0) { + mLeftGlow.onRelease(); + } + } else if (mRightGlow != null && mRightGlow.getDistance() != 0) { + consumed = mRightGlow.onPullDistance(pullDistance, displacement); + if (mRightGlow.getDistance() == 0) { + mRightGlow.onRelease(); + } + } + int pixelsConsumed = Math.round(consumed * getWidth()); + if (pixelsConsumed != 0) { + invalidate(); + } + return pixelsConsumed; + } + + /** + * If either of the vertical edge glows are currently active, this consumes part or all of + * deltaY on the edge glow. + * + * @param deltaY The pointer motion, in pixels, in the vertical direction, positive + * for moving down and negative for moving up. + * @param x The vertical position of the pointer. + * @return The amount of deltaY that has been consumed by the + * edge glow. + */ + private int releaseVerticalGlow(int deltaY, float x) { + // First allow releasing existing overscroll effect: + float consumed = 0; + float displacement = x / getWidth(); + float pullDistance = (float) deltaY / getHeight(); + if (mTopGlow != null && mTopGlow.getDistance() != 0) { + consumed = -mTopGlow.onPullDistance(-pullDistance, displacement); + if (mTopGlow.getDistance() == 0) { + mTopGlow.onRelease(); + } + } else if (mBottomGlow != null && mBottomGlow.getDistance() != 0) { + consumed = mBottomGlow.onPullDistance(pullDistance, 1 - displacement); + if (mBottomGlow.getDistance() == 0) { + mBottomGlow.onRelease(); + } + } + int pixelsConsumed = Math.round(consumed * getHeight()); + if (pixelsConsumed != 0) { + invalidate(); + } + return pixelsConsumed; + } + private void resetTouch() { if (mVelocityTracker != null) { mVelocityTracker.clear();