From ce75c6a67cc35b5411d5f614edf265d323c90a0e Mon Sep 17 00:00:00 2001 From: seven332 Date: Wed, 3 Jun 2015 17:02:36 +0800 Subject: [PATCH] RecyclerView version bump --- .../support/v7/widget/LayoutState.java | 23 +- .../v7/widget/LinearLayoutManager.java | 73 ++- .../support/v7/widget/OpReorderer.java | 3 +- .../support/v7/widget/RecyclerView.java | 591 ++++++++++++++++-- .../v7/widget/StaggeredGridLayoutManager.java | 198 +++--- .../main/res/values/recyclerview_values.xml | 10 + 6 files changed, 748 insertions(+), 150 deletions(-) create mode 100644 app/src/main/res/values/recyclerview_values.xml diff --git a/app/src/main/java/android/support/v7/widget/LayoutState.java b/app/src/main/java/android/support/v7/widget/LayoutState.java index e62a80a28..f58a4a76e 100644 --- a/app/src/main/java/android/support/v7/widget/LayoutState.java +++ b/app/src/main/java/android/support/v7/widget/LayoutState.java @@ -60,11 +60,14 @@ class LayoutState { int mLayoutDirection; /** - * Used if you want to pre-layout items that are not yet visible. - * The difference with {@link #mAvailable} is that, when recycling, distance rendered for - * {@link #mExtra} is not considered not to recycle visible children. + * This is the target pixel closest to the start of the layout that we are trying to fill */ - int mExtra = 0; + int mStartLine = 0; + + /** + * This is the target pixel closest to the end of the layout that we are trying to fill + */ + int mEndLine = 0; /** * @return true if there are more items in the data adapter @@ -84,4 +87,16 @@ class LayoutState { mCurrentPosition += mItemDirection; return view; } + + @Override + public String toString() { + return "LayoutState{" + + "mAvailable=" + mAvailable + + ", mCurrentPosition=" + mCurrentPosition + + ", mItemDirection=" + mItemDirection + + ", mLayoutDirection=" + mLayoutDirection + + ", mStartLine=" + mStartLine + + ", mEndLine=" + mEndLine + + '}'; + } } diff --git a/app/src/main/java/android/support/v7/widget/LinearLayoutManager.java b/app/src/main/java/android/support/v7/widget/LinearLayoutManager.java index 04d5c2d21..f6a12eb3d 100644 --- a/app/src/main/java/android/support/v7/widget/LinearLayoutManager.java +++ b/app/src/main/java/android/support/v7/widget/LinearLayoutManager.java @@ -23,6 +23,8 @@ import android.os.Parcelable; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.util.AttributeSet; +import android.support.v7.widget.helper.ItemTouchHelper; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -36,7 +38,8 @@ import static android.support.v7.widget.RecyclerView.NO_POSITION; * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides * similar functionality to {@link android.widget.ListView}. */ -public class LinearLayoutManager extends RecyclerView.LayoutManager { +public class LinearLayoutManager extends RecyclerView.LayoutManager implements + ItemTouchHelper.ViewDropHandler { private static final String TAG = "LinearLayoutManager"; @@ -130,7 +133,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager { * Re-used variable to keep anchor information on re-layout. * Anchor position and coordinate defines the reference point for LLM while doing a layout. * */ - final AnchorInfo mAnchorInfo; + final AnchorInfo mAnchorInfo = new AnchorInfo(); /** * Creates a vertical LinearLayoutManager @@ -148,11 +151,26 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager { * @param reverseLayout When set to true, layouts from end to start. */ public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) { - mAnchorInfo = new AnchorInfo(); setOrientation(orientation); setReverseLayout(reverseLayout); } + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". Defaults to vertical orientation. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd + */ + public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); + setOrientation(properties.orientation); + setReverseLayout(properties.reverseLayout); + setStackFromEnd(properties.stackFromEnd); + } + /** * {@inheritDoc} */ @@ -371,9 +389,13 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager { final int firstChild = getPosition(getChildAt(0)); final int viewPosition = position - firstChild; if (viewPosition >= 0 && viewPosition < childCount) { - return getChildAt(viewPosition); + final View child = getChildAt(viewPosition); + if (getPosition(child) == position) { + return child; // in pre-layout, this may not match + } } - return null; + // fallback to traversal. This might be necessary in pre-layout. + return super.findViewByPosition(position); } /** @@ -796,6 +818,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager { } // override layout from end values for consistency anchorInfo.mLayoutFromEnd = mShouldReverseLayout; + // if this changes, we should update prepareForDrop as well if (mShouldReverseLayout) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingScrollPositionOffset; @@ -941,7 +964,6 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager { * item[10]'s bottom is 20 pixels above the RecyclerView's bottom. *

* Note that scroll position change will not be reflected until the next layout call. - * *

* If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. * @@ -1177,9 +1199,8 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager { * * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} * @param dt This can be used to add additional padding to the visible area. This is used - * to - * detect children that will go out of bounds after scrolling, without actually - * moving them. + * to detect children that will go out of bounds after scrolling, without + * actually moving them. */ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { if (dt < 0) { @@ -1788,6 +1809,40 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager { return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd; } + /** + * @hide This method should be called by ItemTouchHelper only. + */ + @Override + public void prepareForDrop(View view, View target, int x, int y) { + assertNotInLayoutOrScroll("Cannot drop a view during a scroll or layout calculation"); + ensureLayoutState(); + resolveShouldLayoutReverse(); + final int myPos = getPosition(view); + final int targetPos = getPosition(target); + final int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL : + LayoutState.ITEM_DIRECTION_HEAD; + if (mShouldReverseLayout) { + if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getEndAfterPadding() - + (mOrientationHelper.getDecoratedStart(target) + + mOrientationHelper.getDecoratedMeasurement(view))); + } else { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getEndAfterPadding() - + mOrientationHelper.getDecoratedEnd(target)); + } + } else { + if (dropDirection == LayoutState.ITEM_DIRECTION_HEAD) { + scrollToPositionWithOffset(targetPos, mOrientationHelper.getDecoratedStart(target)); + } else { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getDecoratedEnd(target) - + mOrientationHelper.getDecoratedMeasurement(view)); + } + } + } + /** * Helper class that keeps temporary state while {LayoutManager} is filling out the empty * space. diff --git a/app/src/main/java/android/support/v7/widget/OpReorderer.java b/app/src/main/java/android/support/v7/widget/OpReorderer.java index 1a34cba3e..e123ce8dc 100644 --- a/app/src/main/java/android/support/v7/widget/OpReorderer.java +++ b/app/src/main/java/android/support/v7/widget/OpReorderer.java @@ -16,10 +16,9 @@ package android.support.v7.widget; -import android.support.v7.widget.AdapterHelper.UpdateOp; - import java.util.List; +import android.support.v7.widget.AdapterHelper.UpdateOp; import static android.support.v7.widget.AdapterHelper.UpdateOp.ADD; import static android.support.v7.widget.AdapterHelper.UpdateOp.MOVE; import static android.support.v7.widget.AdapterHelper.UpdateOp.REMOVE; diff --git a/app/src/main/java/android/support/v7/widget/RecyclerView.java b/app/src/main/java/android/support/v7/widget/RecyclerView.java index c610c480b..d856323b6 100644 --- a/app/src/main/java/android/support/v7/widget/RecyclerView.java +++ b/app/src/main/java/android/support/v7/widget/RecyclerView.java @@ -18,6 +18,7 @@ package android.support.v7.widget; import android.content.Context; +import android.content.res.TypedArray; import android.database.Observable; import android.graphics.Canvas; import android.graphics.PointF; @@ -31,6 +32,8 @@ import android.support.v4.os.TraceCompat; import android.support.v4.util.ArrayMap; import android.support.v4.view.InputDeviceCompat; import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.NestedScrollingChild; +import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.ScrollingView; import android.support.v4.view.VelocityTrackerCompat; import android.support.v4.view.ViewCompat; @@ -40,9 +43,6 @@ import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v4.widget.EdgeEffectCompat; import android.support.v4.widget.ScrollerCompat; -import static android.support.v7.widget.AdapterHelper.UpdateOp; -import static android.support.v7.widget.AdapterHelper.Callback; - import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -59,10 +59,17 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.Interpolator; +import com.hippo.ehviewer.R; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static android.support.v7.widget.AdapterHelper.Callback; +import static android.support.v7.widget.AdapterHelper.UpdateOp; + /** * A flexible view for providing a limited window into a large data set. * @@ -127,8 +134,10 @@ import java.util.List; *

* When writing a {@link LayoutManager} you almost always want to use layout positions whereas when * writing an {@link Adapter}, you probably want to use adapter positions. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_layoutManager */ -public class RecyclerView extends ViewGroup implements ScrollingView { +public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild { private static final String TAG = "RecyclerView"; @@ -220,6 +229,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView { * >Try increasing your pool size and item cache size. */ private static final String TRACE_CREATE_VIEW_TAG = "RV CreateView"; + private static final Class[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE = + new Class[]{Context.class, AttributeSet.class, int.class, int.class}; private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); @@ -286,6 +297,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView { private boolean mAdapterUpdateDuringMeasure; private final boolean mPostUpdatesOnAnimation; private final AccessibilityManager mAccessibilityManager; + private List mOnChildAttachStateListeners; /** * Set to true when an adapter data set changed notification is received. @@ -357,11 +369,17 @@ public class RecyclerView extends ViewGroup implements ScrollingView { new ItemAnimatorRestoreListener(); private boolean mPostedAnimatorRunner = false; private RecyclerViewAccessibilityDelegate mAccessibilityDelegate; + private ChildDrawingOrderCallback mChildDrawingOrderCallback; // simple array to keep min and max child position during a layout calculation // preserved not to create a new one in each layout pass private final int[] mMinMaxLayoutPositions = new int[2]; + private final NestedScrollingChildHelper mScrollingChildHelper; + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private final int[] mNestedOffsets = new int[2]; + private Runnable mItemAnimatorRunner = new Runnable() { @Override public void run() { @@ -411,6 +429,18 @@ public class RecyclerView extends ViewGroup implements ScrollingView { mAccessibilityManager = (AccessibilityManager) getContext() .getSystemService(Context.ACCESSIBILITY_SERVICE); setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this)); + // Create the layoutManager if specified. + if (attrs != null) { + int defStyleRes = 0; + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyle, defStyleRes); + String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager); + a.recycle(); + createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes); + } + + mScrollingChildHelper = new NestedScrollingChildHelper(this); + setNestedScrollingEnabled(true); } /** @@ -431,6 +461,72 @@ public class RecyclerView extends ViewGroup implements ScrollingView { ViewCompat.setAccessibilityDelegate(this, mAccessibilityDelegate); } + /** + * Instantiate and set a LayoutManager, if specified in the attributes. + */ + private void createLayoutManager(Context context, String className, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + if (className != null) { + className = className.trim(); + if (className.length() != 0) { // Can't use isEmpty since it was added in API 9. + className = getFullClassName(context, className); + try { + ClassLoader classLoader; + if (isInEditMode()) { + // Stupid layoutlib cannot handle simple class loaders. + classLoader = this.getClass().getClassLoader(); + } else { + classLoader = context.getClassLoader(); + } + Class layoutManagerClass = + classLoader.loadClass(className).asSubclass(LayoutManager.class); + Constructor constructor; + Object[] constructorArgs = null; + try { + constructor = layoutManagerClass + .getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE); + constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes}; + } catch (NoSuchMethodException e) { + try { + constructor = layoutManagerClass.getConstructor(); + } catch (NoSuchMethodException e1) { + e1.initCause(e); + throw new IllegalStateException(attrs.getPositionDescription() + + ": Error creating LayoutManager " + className, e1); + } + } + constructor.setAccessible(true); + setLayoutManager(constructor.newInstance(constructorArgs)); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Unable to find LayoutManager " + className, e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (InstantiationException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Cannot access non-public constructor " + className, e); + } catch (ClassCastException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Class is not a LayoutManager " + className, e); + } + } + } + } + + private String getFullClassName(Context context, String className) { + if (className.charAt(0) == '.') { + return context.getPackageName() + className; + } + if (className.contains(".")) { + return className; + } + return RecyclerView.class.getPackage().getName() + '.' + className; + } + private void initChildrenHelper() { mChildHelper = new ChildHelper(new ChildHelper.Callback() { @Override @@ -772,6 +868,46 @@ public class RecyclerView extends ViewGroup implements ScrollingView { } } + /** + * Register a listener that will be notified whenever a child view is attached to or detached + * from RecyclerView. + * + *

This listener will be called when a LayoutManager or the RecyclerView decides + * that a child view is no longer needed. If an application associates expensive + * or heavyweight data with item views, this may be a good place to release + * or free those resources.

+ * + * @param listener Listener to register + */ + public void addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) { + if (mOnChildAttachStateListeners == null) { + mOnChildAttachStateListeners = new ArrayList(); + } + mOnChildAttachStateListeners.add(listener); + } + + /** + * Removes the provided listener from child attached state listeners list. + * + * @param listener Listener to unregister + */ + public void removeOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) { + if (mOnChildAttachStateListeners == null) { + return; + } + mOnChildAttachStateListeners.remove(listener); + } + + /** + * Removes all listeners that were added via + * {@link #addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener)}. + */ + public void clearOnChildAttachStateChangeListeners() { + if (mOnChildAttachStateListeners != null) { + mOnChildAttachStateListeners.clear(); + } + } + /** * Set the {@link LayoutManager} that this RecyclerView will use. * @@ -1048,6 +1184,26 @@ public class RecyclerView extends ViewGroup implements ScrollingView { requestLayout(); } + /** + * Sets the {@link ChildDrawingOrderCallback} to be used for drawing children. + *

+ * See {@link ViewGroup#getChildDrawingOrder(int, int)} for details. Calling this method will + * always call {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean)}. The parameter will be + * true if childDrawingOrderCallback is not null, false otherwise. + *

+ * Note that child drawing order may be overridden by View's elevation. + * + * @param childDrawingOrderCallback The ChildDrawingOrderCallback to be used by the drawing + * system. + */ + public void setChildDrawingOrderCallback(ChildDrawingOrderCallback childDrawingOrderCallback) { + if (childDrawingOrderCallback == mChildDrawingOrderCallback) { + return; + } + mChildDrawingOrderCallback = childDrawingOrderCallback; + setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null); + } + /** * Set a listener that will be notified of any changes in scroll state or position. * @@ -1156,7 +1312,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView { final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); final boolean canScrollVertical = mLayout.canScrollVertically(); if (canScrollHorizontal || canScrollVertical) { - scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, false, 0, 0); + scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null); } } @@ -1179,30 +1335,26 @@ public class RecyclerView extends ViewGroup implements ScrollingView { * * @param x The amount of horizontal scroll request * @param y The amount of vertical scroll request - * @param fromMotionEvent If request is originated from a MotionEvent, this should be set to - * true and motionX/motionY should be provided, false otherwise. - * @param motionX The x coordinate of the MotionEvent which triggered this scroll. Unused if - * fromMotionEvent is false. - * @param motionY The y coordinate of the MotionEvent which triggered this scroll. Unused if - * fromMotionEvent is false. + * @param ev The originating MotionEvent, or null if not from a touch event. * * @return Whether any scroll was consumed in either direction. */ - boolean scrollByInternal(int x, int y, boolean fromMotionEvent, int motionX, int motionY) { - int overscrollX = 0, overscrollY = 0; - int hresult = 0, vresult = 0; + boolean scrollByInternal(int x, int y, MotionEvent ev) { + int unconsumedX = 0, unconsumedY = 0; + int consumedX = 0, consumedY = 0; + consumePendingUpdateOperations(); if (mAdapter != null) { eatRequestLayout(); onEnterLayoutOrScroll(); TraceCompat.beginSection(TRACE_SCROLL_TAG); if (x != 0) { - hresult = mLayout.scrollHorizontallyBy(x, mRecycler, mState); - overscrollX = x - hresult; + consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState); + unconsumedX = x - consumedX; } if (y != 0) { - vresult = mLayout.scrollVerticallyBy(y, mRecycler, mState); - overscrollY = y - vresult; + consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); + unconsumedY = y - consumedY; } TraceCompat.endSection(); if (supportsChangeAnimations()) { @@ -1232,19 +1384,27 @@ public class RecyclerView extends ViewGroup implements ScrollingView { if (!mItemDecorations.isEmpty()) { invalidate(); } - if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) { - if (fromMotionEvent) { - pullGlows(motionX, overscrollX, motionY, overscrollY); + + if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) { + // Update the last touch co-ords, taking any scroll offset into account + mLastTouchX -= mScrollOffset[0]; + mLastTouchY -= mScrollOffset[1]; + ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + } else if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) { + if (ev != null) { + pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY); } considerReleasingGlowsOnScroll(x, y); } - if (hresult != 0 || vresult != 0) { - dispatchOnScrolled(hresult, vresult); + if (consumedX != 0 || consumedY != 0) { + dispatchOnScrolled(consumedX, consumedY); } if (!awakenScrollBars()) { invalidate(); } - return hresult != 0 || vresult != 0; + return consumedX != 0 || consumedY != 0; } /** @@ -1437,19 +1597,31 @@ public class RecyclerView extends ViewGroup implements ScrollingView { "Call setLayoutManager with a non-null argument."); return false; } + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); final boolean canScrollVertical = mLayout.canScrollVertically(); + if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { velocityX = 0; } if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) { velocityY = 0; } - velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); - velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); - if (velocityX != 0 || velocityY != 0) { - mViewFlinger.fling(velocityX, velocityY); - return true; + if (velocityX == 0 && velocityY == 0) { + // If we don't have any velocity, return false + return false; + } + + if (!dispatchNestedPreFling(velocityX, velocityY)) { + final boolean canScroll = canScrollHorizontal || canScrollVertical; + dispatchNestedFling(velocityX, velocityY, canScroll); + + if (canScroll) { + velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); + velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); + mViewFlinger.fling(velocityX, velocityY); + return true; + } } return false; } @@ -1473,29 +1645,52 @@ public class RecyclerView extends ViewGroup implements ScrollingView { } } + /** + * Returns the minimum velocity to start a fling. + * + * @return The minimum velocity to start a fling + */ + public int getMinFlingVelocity() { + return mMinFlingVelocity; + } + + + /** + * Returns the maximum fling velocity used by this RecyclerView. + * + * @return The maximum fling velocity used by this RecyclerView. + */ + public int getMaxFlingVelocity() { + return mMaxFlingVelocity; + } + /** * Apply a pull to relevant overscroll glow effects */ - private void pullGlows(int x, int overscrollX, int y, int overscrollY) { + private void pullGlows(float x, float overscrollX, float y, float overscrollY) { boolean invalidate = false; if (overscrollX < 0) { ensureLeftGlow(); - invalidate = mLeftGlow.onPull(-overscrollX / (float) getWidth(), - 1f - y / (float) getHeight()) || invalidate; + if (mLeftGlow.onPull(-overscrollX / getWidth(), 1f - y / getHeight())) { + invalidate = true; + } } else if (overscrollX > 0) { ensureRightGlow(); - invalidate = mRightGlow.onPull(overscrollX / (float) getWidth(), - y / (float) getHeight()) || invalidate; + if (mRightGlow.onPull(overscrollX / getWidth(), y / getHeight())) { + invalidate = true; + } } if (overscrollY < 0) { ensureTopGlow(); - invalidate = mTopGlow.onPull(-overscrollY / (float) getHeight(), - x / (float) getWidth()) || invalidate; + if (mTopGlow.onPull(-overscrollY / getHeight(), x / getWidth())) { + invalidate = true; + } } else if (overscrollY > 0) { ensureBottomGlow(); - invalidate = mBottomGlow.onPull(overscrollY / (float) getHeight(), - 1f - x / (float) getWidth()) || invalidate; + if (mBottomGlow.onPull(overscrollY / getHeight(), 1f - x / getWidth())) { + invalidate = true; + } } if (invalidate || overscrollX != 0 || overscrollY != 0) { @@ -1698,6 +1893,14 @@ public class RecyclerView extends ViewGroup implements ScrollingView { removeCallbacks(mItemAnimatorRunner); } + /** + * Returns true if RecyclerView is attached to window. + */ + // @override + public boolean isAttachedToWindow() { + return mIsAttached; + } + /** * Checks if RecyclerView is in the middle of a layout or scroll and throws an * {@link IllegalStateException} if it is not. @@ -1744,6 +1947,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView { * for each incoming MotionEvent until the end of the gesture.

* * @param listener Listener to add + * @see SimpleOnItemTouchListener */ public void addOnItemTouchListener(OnItemTouchListener listener) { mOnItemTouchListeners.add(listener); @@ -1837,6 +2041,15 @@ public class RecyclerView extends ViewGroup implements ScrollingView { getParent().requestDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } + + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); break; case MotionEventCompat.ACTION_POINTER_DOWN: @@ -1879,6 +2092,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView { case MotionEvent.ACTION_UP: { mVelocityTracker.clear(); + stopNestedScroll(); } break; case MotionEvent.ACTION_CANCEL: { @@ -1888,6 +2102,16 @@ public class RecyclerView extends ViewGroup implements ScrollingView { return mScrollState == SCROLL_STATE_DRAGGING; } + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + final int listenerCount = mOnItemTouchListeners.size(); + for (int i = 0; i < listenerCount; i++) { + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); + listener.onRequestDisallowInterceptTouchEvent(disallowIntercept); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + @Override public boolean onTouchEvent(MotionEvent e) { if (dispatchOnItemTouch(e)) { @@ -1903,14 +2127,29 @@ public class RecyclerView extends ViewGroup implements ScrollingView { } mVelocityTracker.addMovement(e); + final MotionEvent vtev = MotionEvent.obtain(e); final int action = MotionEventCompat.getActionMasked(e); final int actionIndex = MotionEventCompat.getActionIndex(e); + if (action == MotionEvent.ACTION_DOWN) { + mNestedOffsets[0] = mNestedOffsets[1] = 0; + } + vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]); + switch (action) { case MotionEvent.ACTION_DOWN: { mScrollPointerId = MotionEventCompat.getPointerId(e, 0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); + + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); } break; case MotionEventCompat.ACTION_POINTER_DOWN: { @@ -1929,32 +2168,52 @@ public class RecyclerView extends ViewGroup implements ScrollingView { final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f); final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f); + int dx = mLastTouchX - x; + int dy = mLastTouchY - y; + + if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { + dx -= mScrollConsumed[0]; + dy -= mScrollConsumed[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + // Updated the nested offsets + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + } + if (mScrollState != SCROLL_STATE_DRAGGING) { - final int dx = x - mInitialTouchX; - final int dy = y - mInitialTouchY; boolean startScroll = false; if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { - mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1); + if (dx > 0) { + dx -= mTouchSlop; + } else { + dx += mTouchSlop; + } startScroll = true; } if (canScrollVertically && Math.abs(dy) > mTouchSlop) { - mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1); + if (dy > 0) { + dy -= mTouchSlop; + } else { + dy += mTouchSlop; + } startScroll = true; } if (startScroll) { setScrollState(SCROLL_STATE_DRAGGING); } } + if (mScrollState == SCROLL_STATE_DRAGGING) { - final int dx = x - mLastTouchX; - final int dy = y - mLastTouchY; - if (scrollByInternal(canScrollHorizontally ? -dx : 0, - canScrollVertically ? -dy : 0, true, x, y)) { + mLastTouchX = x - mScrollOffset[0]; + mLastTouchY = y - mScrollOffset[1]; + + if (scrollByInternal( + canScrollHorizontally ? dx : 0, + canScrollVertically ? dy : 0, + vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } } - mLastTouchX = x; - mLastTouchY = y; } break; case MotionEventCompat.ACTION_POINTER_UP: { @@ -1970,6 +2229,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView { if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } + mVelocityTracker.clear(); releaseGlows(); } break; @@ -1979,6 +2239,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView { } break; } + vtev.recycle(); + return true; } @@ -1986,6 +2248,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView { if (mVelocityTracker != null) { mVelocityTracker.clear(); } + stopNestedScroll(); releaseGlows(); setScrollState(SCROLL_STATE_IDLE); } @@ -2830,6 +3093,18 @@ public class RecyclerView extends ViewGroup implements ScrollingView { return mLayout.generateLayoutParams(p); } + /** + * Returns true if RecyclerView is currently running some animations. + *

+ * If you want to be notified when animations are finished, use + * {@link ItemAnimator#isRunning(ItemAnimator.ItemAnimatorFinishedListener)}. + * + * @return True if there are some item animations currently running or waiting to be started. + */ + public boolean isAnimating() { + return mItemAnimator != null && mItemAnimator.isRunning(); + } + void saveOldPositions() { final int childCount = mChildHelper.getUnfilteredChildCount(); for (int i = 0; i < childCount; i++) { @@ -3239,6 +3514,11 @@ public class RecyclerView extends ViewGroup implements ScrollingView { return null; } + @Override + public boolean drawChild(Canvas canvas, View child, long drawingTime) { + return super.drawChild(canvas, child, drawingTime); + } + /** * Offset the bounds of all child views by dy pixels. * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}. @@ -5166,17 +5446,32 @@ public class RecyclerView extends ViewGroup implements ScrollingView { } private void dispatchChildDetached(View child) { - if (mAdapter != null) { - mAdapter.onViewDetachedFromWindow(getChildViewHolderInt(child)); - } + final ViewHolder viewHolder = getChildViewHolderInt(child); onChildDetachedFromWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewDetachedFromWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child); + } + } } private void dispatchChildAttached(View child) { - if (mAdapter != null) { - mAdapter.onViewAttachedToWindow(getChildViewHolderInt(child)); - } + final ViewHolder viewHolder = getChildViewHolderInt(child); onChildAttachedToWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewAttachedToWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child); + } + } + } /** @@ -5186,6 +5481,14 @@ public class RecyclerView extends ViewGroup implements ScrollingView { * a RecyclerView can be used to implement a standard vertically scrolling list, * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock * layout managers are provided for general use. + *

+ * If the LayoutManager specifies a default constructor or one with the signature + * ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will + * instantiate and set the LayoutManager when being inflated. Most used properties can + * be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case + * a LayoutManager specifies both constructors, the non-default constructor will take + * precedence. + * */ public static abstract class LayoutManager { ChildHelper mChildHelper; @@ -7272,6 +7575,43 @@ public class RecyclerView extends ViewGroup implements ScrollingView { int action, Bundle args) { return false; } + + /** + * Parse the xml attributes to get the most common properties used by layout managers. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd + * + * @return an object containing the properties as specified in the attrs. + */ + public static Properties getProperties(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + Properties properties = new Properties(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyleAttr, defStyleRes); + properties.orientation = a.getInt(R.styleable.RecyclerView_android_orientation, VERTICAL); + properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1); + properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false); + properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false); + a.recycle(); + return properties; + } + + /** + * Some general properties that a LayoutManager may want to use. + */ + public static class Properties { + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation */ + public int orientation; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount */ + public int spanCount; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout */ + public boolean reverseLayout; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd */ + public boolean stackFromEnd; + } } /** @@ -7372,8 +7712,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView { * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept * a touch interaction already in progress even if the RecyclerView is already handling that * gesture stream itself for the purposes of scrolling.

+ * + * @see SimpleOnItemTouchListener */ - public interface OnItemTouchListener { + public static interface OnItemTouchListener { /** * Silently observe and/or take over touch events sent to the RecyclerView * before they are handled by either the RecyclerView itself or its child views. @@ -7398,8 +7740,44 @@ public class RecyclerView extends ViewGroup implements ScrollingView { * the RecyclerView's coordinate system. */ public void onTouchEvent(RecyclerView rv, MotionEvent e); + + /** + * Called when a child of RecyclerView does not want RecyclerView and its ancestors to + * intercept touch events with + * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. + * + * @param disallowIntercept True if the child does not want the parent to + * intercept touch events. + * @see ViewParent#requestDisallowInterceptTouchEvent(boolean) + */ + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept); } + /** + * An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies and + * default return values. + *

+ * You may prefer to extend this class if you don't need to override all methods. Another + * benefit of using this class is future compatibility. As the interface may change, we'll + * always provide a default implementation on this class so that your code won't break when + * you update to a new version of the support library. + */ + public class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener { + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + return false; + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent e) { + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } + } + + /** * An OnScrollListener can be set on a RecyclerView to receive messages * when a scrolling event has occurred on that RecyclerView. @@ -7412,7 +7790,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView { * want your components to be able to easily replace the listener use * RecyclerView#setOnScrollListener. */ - abstract static public class OnScrollListener { + public abstract static class OnScrollListener { /** * Callback method to be invoked when RecyclerView's scroll state changes. * @@ -7457,6 +7835,27 @@ public class RecyclerView extends ViewGroup implements ScrollingView { public void onViewRecycled(ViewHolder holder); } + /** + * A Listener interface that can be attached to a RecylcerView to get notified + * whenever a ViewHolder is attached to or detached from RecyclerView. + */ + public interface OnChildAttachStateChangeListener { + + /** + * Called when a view is attached to the RecyclerView. + * + * @param view The View which is attached to the RecyclerView + */ + public void onChildViewAttachedToWindow(View view); + + /** + * Called when a view is detached from RecyclerView. + * + * @param view The View which is being detached from the RecyclerView + */ + public void onChildViewDetachedFromWindow(View view); + } + /** * A ViewHolder describes an item view and metadata about its place within the RecyclerView. * @@ -7875,6 +8274,55 @@ public class RecyclerView extends ViewGroup implements ScrollingView { return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition); } + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mScrollingChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mScrollingChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mScrollingChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mScrollingChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mScrollingChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + /** * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged @@ -9331,4 +9779,35 @@ public class RecyclerView extends ViewGroup implements ScrollingView { this.bottom = bottom; } } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (mChildDrawingOrderCallback == null) { + return super.getChildDrawingOrder(childCount, i); + } else { + return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i); + } + } + + /** + * A callback interface that can be used to alter the drawing order of RecyclerView children. + *

+ * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case + * that applies to that method also applies to this callback. For example, changing the drawing + * order of two views will not have any effect if their elevation values are different since + * elevation overrides the result of this callback. + */ + public static interface ChildDrawingOrderCallback { + /** + * Returns the index of the child to draw for this iteration. Override this + * if you want to change the drawing order of children. By default, it + * returns i. + * + * @param i The current iteration. + * @return The index of the child to draw this iteration. + * + * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback) + */ + public int onGetChildDrawingOrder(int childCount, int i); + } } diff --git a/app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java b/app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java index f98b7f2cd..dcadb5fcc 100644 --- a/app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java +++ b/app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java @@ -201,6 +201,18 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { } }; + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". Defaults to single column and vertical. + */ + public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); + setOrientation(properties.orientation); + setSpanCount(properties.spanCount); + setReverseLayout(properties.reverseLayout); + } + /** * Creates a StaggeredGridLayoutManager with given parameters. * @@ -537,7 +549,6 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ensureOrientationHelper(); - final AnchorInfo anchorInfo = mAnchorInfo; anchorInfo.reset(); @@ -577,21 +588,22 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { detachAndScrapAttachedViews(recycler); mLaidOutInvalidFullSpan = false; updateMeasureSpecs(); + updateLayoutState(anchorInfo.mPosition, state); if (anchorInfo.mLayoutFromEnd) { // Layout start. - updateLayoutStateToFillStart(anchorInfo.mPosition, state); + setLayoutStateDirection(LAYOUT_START); fill(recycler, mLayoutState, state); // Layout end. - updateLayoutStateToFillEnd(anchorInfo.mPosition, state); - mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + setLayoutStateDirection(LAYOUT_END); + mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; fill(recycler, mLayoutState, state); } else { // Layout end. - updateLayoutStateToFillEnd(anchorInfo.mPosition, state); + setLayoutStateDirection(LAYOUT_END); fill(recycler, mLayoutState, state); // Layout start. - updateLayoutStateToFillStart(anchorInfo.mPosition, state); - mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + setLayoutStateDirection(LAYOUT_START); + mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; fill(recycler, mLayoutState, state); } @@ -1254,40 +1266,37 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { } } - private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) { + private void updateLayoutState(int anchorPosition, RecyclerView.State state) { mLayoutState.mAvailable = 0; mLayoutState.mCurrentPosition = anchorPosition; + int startExtra = 0; + int endExtra = 0; if (isSmoothScrolling()) { final int targetPos = state.getTargetScrollPosition(); - if (mShouldReverseLayout == targetPos < anchorPosition) { - mLayoutState.mExtra = 0; - } else { - mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); + if (targetPos != NO_POSITION) { + if (mShouldReverseLayout == targetPos < anchorPosition) { + endExtra = mPrimaryOrientation.getTotalSpace(); + } else { + startExtra = mPrimaryOrientation.getTotalSpace(); + } } - } else { - mLayoutState.mExtra = 0; } - mLayoutState.mLayoutDirection = LAYOUT_START; - mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL - : ITEM_DIRECTION_HEAD; + + // Line of the furthest row. + final boolean clipToPadding = getClipToPadding(); + if (clipToPadding) { + mLayoutState.mStartLine = mPrimaryOrientation.getStartAfterPadding() - startExtra; + mLayoutState.mEndLine = mPrimaryOrientation.getEndAfterPadding() + endExtra; + } else { + mLayoutState.mEndLine = mPrimaryOrientation.getEnd() + endExtra; + mLayoutState.mStartLine = -startExtra; + } } - private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) { - mLayoutState.mAvailable = 0; - mLayoutState.mCurrentPosition = anchorPosition; - if (isSmoothScrolling()) { - final int targetPos = state.getTargetScrollPosition(); - if (mShouldReverseLayout == targetPos > anchorPosition) { - mLayoutState.mExtra = 0; - } else { - mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); - } - } else { - mLayoutState.mExtra = 0; - } - mLayoutState.mLayoutDirection = LAYOUT_END; - mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD - : ITEM_DIRECTION_TAIL; + private void setLayoutStateDirection(int direction) { + mLayoutState.mLayoutDirection = direction; + mLayoutState.mItemDirection = (mShouldReverseLayout == (direction == LAYOUT_START)) ? + ITEM_DIRECTION_TAIL : ITEM_DIRECTION_HEAD; } @Override @@ -1383,31 +1392,25 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { mRemainingSpans.set(0, mSpanCount, true); // The target position we are trying to reach. final int targetLine; - /* - * The line until which we can recycle, as long as we add views. - * Keep in mind, it is still the line in layout direction which means; to calculate the - * actual recycle line, we should subtract/add the size in orientation. - */ - final int recycleLine; + // Line of the furthest row. if (layoutState.mLayoutDirection == LAYOUT_END) { - // ignore padding for recycler - recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable; - targetLine = recycleLine + mLayoutState.mExtra + mPrimaryOrientation.getEndPadding(); - + targetLine = layoutState.mEndLine + layoutState.mAvailable; } else { // LAYOUT_START - // ignore padding for recycler - recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable; - targetLine = recycleLine - mLayoutState.mExtra - - mPrimaryOrientation.getStartAfterPadding(); + targetLine = layoutState.mStartLine - layoutState.mAvailable; } + updateAllRemainingSpans(layoutState.mLayoutDirection, targetLine); + if (DEBUG) { + Log.d(TAG, "FILLING targetLine: " + targetLine + "," + + "remaining spans:" + mRemainingSpans + ", state: " + layoutState); + } // the default coordinate to add new view. final int defaultNewViewLine = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() : mPrimaryOrientation.getStartAfterPadding(); - + boolean added = false; while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) { View view = layoutState.next(recycler); LayoutParams lp = ((LayoutParams) view.getLayoutParams()); @@ -1500,18 +1503,21 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { } else { updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); } - recycle(recycler, mLayoutState, currentSpan, recycleLine); + recycle(recycler, mLayoutState); + added = true; } - if (DEBUG) { - Log.d(TAG, "fill, " + getChildCount()); + if (!added) { + recycle(recycler, mLayoutState); } + final int diff; if (mLayoutState.mLayoutDirection == LAYOUT_START) { final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); - return Math.max(0, mLayoutState.mAvailable + (recycleLine - minStart)); + diff = mPrimaryOrientation.getStartAfterPadding() - minStart; } else { - final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); - return Math.max(0, mLayoutState.mAvailable + (max - recycleLine)); + final int maxEnd = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); + diff = maxEnd - mPrimaryOrientation.getEndAfterPadding(); } + return diff > 0 ? Math.min(layoutState.mAvailable, diff) : 0; } private LazySpanLookup.FullSpanItem createFullSpanItemFromEnd(int newItemTop) { @@ -1548,19 +1554,40 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { } } - private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState, - Span updatedSpan, int recycleLine) { - if (layoutState.mLayoutDirection == LAYOUT_START) { - // calculate recycle line - int maxStart = getMaxStart(updatedSpan.getStartLine()); - recycleFromEnd(recycler, Math.max(recycleLine, maxStart) + - (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); + private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) { + if (layoutState.mAvailable == 0) { + // easy, recycle line is still valid + if (layoutState.mLayoutDirection == LAYOUT_START) { + recycleFromEnd(recycler, layoutState.mEndLine); + } else { + recycleFromStart(recycler, layoutState.mStartLine); + } } else { - // calculate recycle line - int minEnd = getMinEnd(updatedSpan.getEndLine()); - recycleFromStart(recycler, Math.min(recycleLine, minEnd) - - (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); + // scrolling case, recycle line can be shifted by how much space we could cover + // by adding new views + if (layoutState.mLayoutDirection == LAYOUT_START) { + // calculate recycle line + int scrolled = layoutState.mStartLine - getMaxStart(layoutState.mStartLine); + final int line; + if (scrolled < 0) { + line = layoutState.mEndLine; + } else { + line = layoutState.mEndLine - Math.min(scrolled, layoutState.mAvailable); + } + recycleFromEnd(recycler, line); + } else { + // calculate recycle line + int scrolled = getMinEnd(layoutState.mEndLine) - layoutState.mEndLine; + final int line; + if (scrolled < 0) { + line = layoutState.mStartLine; + } else { + line = layoutState.mStartLine + Math.min(scrolled, layoutState.mAvailable); + } + recycleFromStart(recycler, line); + } } + } private void appendViewToAllSpans(View view) { @@ -1602,12 +1629,12 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { final int deletedSize = span.getDeletedSize(); if (layoutDir == LAYOUT_START) { final int line = span.getStartLine(); - if (line + deletedSize < targetLine) { + if (line + deletedSize <= targetLine) { mRemainingSpans.set(span.mIndex, false); } } else { final int line = span.getEndLine(); - if (line - deletedSize > targetLine) { + if (line - deletedSize >= targetLine) { mRemainingSpans.set(span.mIndex, false); } } @@ -1678,18 +1705,24 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { } private void recycleFromStart(RecyclerView.Recycler recycler, int line) { - if (DEBUG) { - Log.d(TAG, "recycling from start for line " + line); - } while (getChildCount() > 0) { View child = getChildAt(0); - if (mPrimaryOrientation.getDecoratedEnd(child) < line) { + if (mPrimaryOrientation.getDecoratedEnd(child) <= line) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); + // Don't recycle the last View in a span not to lose span's start/end lines if (lp.mFullSpan) { + for (int j = 0; j < mSpanCount; j++) { + if (mSpans[j].mViews.size() == 1) { + return; + } + } for (int j = 0; j < mSpanCount; j++) { mSpans[j].popStart(); } } else { + if (lp.mSpan.mViews.size() == 1) { + return; + } lp.mSpan.popStart(); } removeAndRecycleView(child, recycler); @@ -1704,13 +1737,22 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { int i; for (i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); - if (mPrimaryOrientation.getDecoratedStart(child) > line) { + if (mPrimaryOrientation.getDecoratedStart(child) >= line) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); + // Don't recycle the last View in a span not to lose span's start/end lines if (lp.mFullSpan) { + for (int j = 0; j < mSpanCount; j++) { + if (mSpans[j].mViews.size() == 1) { + return; + } + } for (int j = 0; j < mSpanCount; j++) { mSpans[j].popEnd(); } } else { + if (lp.mSpan.mViews.size() == 1) { + return; + } lp.mSpan.popEnd(); } removeAndRecycleView(child, recycler); @@ -1860,21 +1902,19 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { ensureOrientationHelper(); final int referenceChildPosition; + final int layoutDir; if (dt > 0) { // layout towards end - mLayoutState.mLayoutDirection = LAYOUT_END; - mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD - : ITEM_DIRECTION_TAIL; + layoutDir = LAYOUT_END; referenceChildPosition = getLastChildPosition(); } else { - mLayoutState.mLayoutDirection = LAYOUT_START; - mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL - : ITEM_DIRECTION_HEAD; + layoutDir = LAYOUT_START; referenceChildPosition = getFirstChildPosition(); } + updateLayoutState(referenceChildPosition, state); + setLayoutStateDirection(layoutDir); mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; final int absDt = Math.abs(dt); mLayoutState.mAvailable = absDt; - mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0; int consumed = fill(recycler, mLayoutState, state); final int totalScroll; if (absDt < consumed) { diff --git a/app/src/main/res/values/recyclerview_values.xml b/app/src/main/res/values/recyclerview_values.xml new file mode 100644 index 000000000..79b78b7d3 --- /dev/null +++ b/app/src/main/res/values/recyclerview_values.xml @@ -0,0 +1,10 @@ + + + + + + 20dp + + + + \ No newline at end of file