am 7356285f: Merge "Full animation support for LinearLayoutManager" into klp-modular-dev
* commit '7356285ff2081f14d83997aa0498c0874326ad72':
Full animation support for LinearLayoutManager
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index 51a448a..2e3b739 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -26,6 +26,8 @@
import android.view.ViewGroup;
import android.widget.LinearLayout;
+import java.util.List;
+
/**
* A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides
* similar functionality to {@link android.widget.ListView}.
@@ -366,13 +368,16 @@
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create render state
-
+ if (DEBUG) {
+ Log.d(TAG, "is pre layout:" + state.isPreLayout());
+ }
if (mPendingSavedState != null) {
setOrientation(mPendingSavedState.mOrientation);
setReverseLayout(mPendingSavedState.mReverseLayout);
setStackFromEnd(mPendingSavedState.mStackFromEnd);
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
+
ensureRenderState();
// resolve layout direction
resolveShouldLayoutReverse();
@@ -495,7 +500,7 @@
mRenderState.mCurrentPosition += mRenderState.mItemDirection;
}
fill(recycler, mRenderState, state, false);
-
+ int startOffset = mRenderState.mOffset;
// fill towards end
updateRenderStateToFillEnd(anchorItemPosition, anchorCoordinate);
mRenderState.mExtra = extraForEnd;
@@ -503,69 +508,135 @@
mRenderState.mCurrentPosition += mRenderState.mItemDirection;
}
fill(recycler, mRenderState, state, false);
-
+ int endOffset = mRenderState.mOffset;
// changes may cause gaps on the UI, try to fix them.
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
if (mShouldReverseLayout ^ mStackFromEnd) {
- fixLayoutEndGap(recycler, state, true);
- fixLayoutStartGap(recycler, state, false);
+ int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
+ fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
} else {
- fixLayoutStartGap(recycler, state, true);
- fixLayoutEndGap(recycler, state, false);
+ int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
+ fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
}
}
+ // If there are scrap children that we did not layout, we need to find where they did go
+ // and layout them accordingly so that animations can work as expected.
+ // This case may happen if new views are added or an existing view expands and pushes
+ // another view out of bounds.
+ if (getChildCount() > 0 && !state.isPreLayout() && supportsItemAnimations()) {
+ // to make the logic simpler, we calculate the size of children and call fill.
+ int scrapExtraStart = 0, scrapExtraEnd = 0;
+ final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
+ final int scrapSize = scrapList.size();
+ final int firstChildPos = getPosition(getChildAt(0));
+ for (int i = 0; i < scrapSize; i++) {
+ RecyclerView.ViewHolder scrap = scrapList.get(i);
+ final int position = scrap.getPosition();
+ final int direction = position < firstChildPos != mShouldReverseLayout
+ ? RenderState.LAYOUT_START : RenderState.LAYOUT_END;
+ if (direction == RenderState.LAYOUT_START) {
+ scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+ } else {
+ scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart
+ + " towards start and " + scrapExtraEnd + " towards end");
+ }
+ mRenderState.mScrapList = scrapList;
+ if (scrapExtraStart > 0) {
+ View anchor = getChildClosestToStart();
+ updateRenderStateToFillStart(getPosition(anchor), startOffset);
+ mRenderState.mExtra = scrapExtraStart;
+ mRenderState.mAvailable = 0;
+ mRenderState.mCurrentPosition += mShouldReverseLayout ? 1 : -1;
+ fill(recycler, mRenderState, state, false);
+ }
+
+ if (scrapExtraEnd > 0) {
+ View anchor = getChildClosestToEnd();
+ updateRenderStateToFillEnd(getPosition(anchor),
+ endOffset);
+ mRenderState.mExtra = scrapExtraEnd;
+ mRenderState.mAvailable = 0;
+ mRenderState.mCurrentPosition += mShouldReverseLayout ? -1 : 1;
+ fill(recycler, mRenderState, state, false);
+ }
+ mRenderState.mScrapList = null;
+ }
+
mPendingScrollPosition = RecyclerView.NO_POSITION;
mPendingScrollPositionOffset = INVALID_OFFSET;
mLastStackFromEnd = mStackFromEnd;
mPendingSavedState = null; // we don't need this anymore
+
if (DEBUG) {
validateChildOrder();
}
}
- private void fixLayoutEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
- boolean canOffsetChildren) {
- View endChild = getChildClosestToEnd();
- int gap = mOrientationHelper.getEndAfterPadding()
- - mOrientationHelper.getDecoratedEnd(endChild);
+ /**
+ * @return The final offset amount for children
+ */
+ private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler,
+ RecyclerView.State state, boolean canOffsetChildren) {
+ int gap = mOrientationHelper.getEndAfterPadding() - endOffset;
+ int fixOffset = 0;
if (gap > 0) {
- scrollBy(-gap, recycler, state);
+ fixOffset = -scrollBy(-gap, recycler, state);
} else {
- return; // nothing to fix
+ return 0; // nothing to fix
}
+ // move offset according to scroll amount
+ endOffset += fixOffset;
if (canOffsetChildren) {
// re-calculate gap, see if we could fix it
- gap = mOrientationHelper.getEndAfterPadding()
- - mOrientationHelper.getDecoratedEnd(endChild);
+ gap = mOrientationHelper.getEndAfterPadding() - endOffset;
if (gap > 0) {
mOrientationHelper.offsetChildren(gap);
+ return gap + fixOffset;
}
}
+ return fixOffset;
}
- private void fixLayoutStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
- boolean canOffsetChildren) {
- View startChild = getChildClosestToStart();
- int gap = mOrientationHelper.getDecoratedStart(startChild) -
- mOrientationHelper.getStartAfterPadding();
+ /**
+ * @return The final offset amount for children
+ */
+ private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler,
+ RecyclerView.State state, boolean canOffsetChildren) {
+ int gap = startOffset - mOrientationHelper.getStartAfterPadding();
+ int fixOffset = 0;
if (gap > 0) {
// check if we should fix this gap.
- scrollBy(gap, recycler, state);
+ fixOffset = -scrollBy(gap, recycler, state);
} else {
- return; // nothing to fix
+ return 0; // nothing to fix
}
+ startOffset += fixOffset;
if (canOffsetChildren) {
// re-calculate gap, see if we could fix it
- gap = mOrientationHelper.getDecoratedStart(startChild) -
- mOrientationHelper.getStartAfterPadding();
+ gap = startOffset - mOrientationHelper.getStartAfterPadding();
if (gap > 0) {
mOrientationHelper.offsetChildren(-gap);
+ return fixOffset - gap;
}
}
+ return fixOffset;
}
private void updateRenderStateToFillEnd(int itemPosition, int offset) {
@@ -912,18 +983,19 @@
int remainingSpace = renderState.mAvailable + renderState.mExtra;
while (remainingSpace > 0 && renderState.hasMore(state)) {
View view = renderState.next(recycler);
- if (mShouldReverseLayout) {
- if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
+ if (view == null) {
+ // if we are laying out views in scrap, this may return null which means there is
+ // no more items to layout.
+ break;
+ }
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+ if (!params.isItemRemoved() && mRenderState.mScrapList == null) {
+ if (mShouldReverseLayout == (renderState.mLayoutDirection
+ == RenderState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
- } else {
- if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
- addView(view, 0);
- } else {
- addView(view);
- }
}
measureChildWithMargins(view, 0, 0);
int consumed = mOrientationHelper.getDecoratedMeasurement(view);
@@ -957,7 +1029,6 @@
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
- RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
layoutDecorated(view, left + params.leftMargin, top + params.topMargin
, right - params.rightMargin, bottom - params.bottomMargin);
if (DEBUG) {
@@ -966,9 +1037,12 @@
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
renderState.mOffset += consumed * renderState.mLayoutDirection;
- renderState.mAvailable -= consumed;
- // we keep a separate remaining space because mAvailable is important for recycling
- remainingSpace -= consumed;
+
+ if (!params.isItemRemoved()) {
+ renderState.mAvailable -= consumed;
+ // we keep a separate remaining space because mAvailable is important for recycling
+ remainingSpace -= consumed;
+ }
if (renderState.mScrollingOffset != RenderState.SCOLLING_OFFSET_NaN) {
renderState.mScrollingOffset += consumed;
@@ -1216,6 +1290,12 @@
int mExtra = 0;
/**
+ * When LLM needs to layout particular views, it sets this list in which case, RenderState
+ * will only return views from this list and return null if it cannot find an item.
+ */
+ List<RecyclerView.ViewHolder> mScrapList = null;
+
+ /**
* @return true if there are more items in the data adapter
*/
boolean hasMore(RecyclerView.State state) {
@@ -1229,11 +1309,49 @@
* @return The next element that we should render.
*/
View next(RecyclerView.Recycler recycler) {
+ if (mScrapList != null) {
+ return nextFromLimitedList();
+ }
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
+ /**
+ * Returns next item from limited list.
+ * <p>
+ * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection
+ *
+ * @return View if an item in the current position or direction exists if not null.
+ */
+ private View nextFromLimitedList() {
+ int size = mScrapList.size();
+ RecyclerView.ViewHolder closest = null;
+ int closestDistance = Integer.MAX_VALUE;
+ for (int i = 0; i < size; i++) {
+ RecyclerView.ViewHolder viewHolder = mScrapList.get(i);
+ final int distance = (viewHolder.getPosition() - mCurrentPosition) * mItemDirection;
+ if (distance < 0) {
+ continue; // item is not in current direction
+ }
+ if (distance < closestDistance) {
+ closest = viewHolder;
+ closestDistance = distance;
+ if (distance == 0) {
+ break;
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "layout from scrap. found view:?" + (closest != null));
+ }
+ if (closest != null) {
+ mCurrentPosition = closest.getPosition() + mItemDirection;
+ return closest.itemView;
+ }
+ return null;
+ }
+
void log() {
Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" +
mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 492be71..56b8b79 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -32,10 +32,60 @@
private static final String TAG = "RecyclerViewTest";
- private static final boolean DEBUG = true;
+ private boolean mDebug;
+
+ protected RecyclerView mRecyclerView;
public BaseRecyclerViewInstrumentationTest() {
+ this(false);
+ }
+
+ public BaseRecyclerViewInstrumentationTest(boolean debug) {
super("android.support.v7.widget", TestActivity.class);
+ mDebug = debug;
+ }
+
+ public void removeRecyclerView() throws Throwable {
+ mRecyclerView = null;
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ getActivity().mContainer.removeAllViews();
+ }
+ });
+ }
+
+ public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
+ mRecyclerView = recyclerView;
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ getActivity().mContainer.addView(recyclerView);
+ }
+ });
+ }
+
+ public void requestLayoutOnUIThread(final View view) throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ view.requestLayout();
+ }
+ });
+ }
+
+ public void scrollBy(final int dt) throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
+ mRecyclerView.scrollBy(dt, 0);
+ } else {
+ mRecyclerView.scrollBy(0, dt);
+ }
+
+ }
+ });
}
class TestViewHolder extends RecyclerView.ViewHolder {
@@ -55,12 +105,26 @@
layoutLatch = new CountDownLatch(count);
}
- public void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
- layoutLatch.await(timeout, timeUnit);
+ public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
+ layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit);
assertEquals("all expected layouts should be executed at the expected time",
0, layoutLatch.getCount());
}
+ public void assertLayoutCount(int count, String msg, long timeout) throws Throwable {
+ layoutLatch.await(timeout, TimeUnit.SECONDS);
+ assertEquals(msg, count, layoutLatch.getCount());
+ }
+
+ public void assertNoLayout(String msg, long timeout) throws Throwable {
+ layoutLatch.await(timeout, TimeUnit.SECONDS);
+ assertFalse(msg, layoutLatch.getCount() == 0);
+ }
+
+ public void waitForLayout(long timeout) throws Throwable {
+ waitForLayout(timeout, TimeUnit.SECONDS);
+ }
+
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
@@ -74,7 +138,7 @@
View view = getChildAt(i);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
Item item = ((TestViewHolder) lp.mViewHolder).mBindedItem;
- if (DEBUG) {
+ if (mDebug) {
Log.d(TAG, "testing item " + i);
}
assertSame("item position in LP should match adapter value",
@@ -82,18 +146,26 @@
}
}
+ RecyclerView.LayoutParams getLp(View v) {
+ return (RecyclerView.LayoutParams) v.getLayoutParams();
+ }
+
void layoutRange(RecyclerView.Recycler recycler, int start,
int end) {
- if (DEBUG) {
+ if (mDebug) {
Log.d(TAG, "will layout items from " + start + " to " + end);
}
for (int i = start; i < end; i++) {
- if (DEBUG) {
+ if (mDebug) {
Log.d(TAG, "laying out item " + i);
}
View view = recycler.getViewForPosition(i);
- assertNotNull("view should not be null for valid position", view);
- addView(view);
+ assertNotNull("view should not be null for valid position. "
+ + "got null view at position " + i, view);
+ if (!getLp(view).isItemRemoved()) {
+ addView(view);
+ }
+
measureChildWithMargins(view, 0, 0);
layoutDecorated(view, 0, (i - start) * 10, getDecoratedMeasuredWidth(view)
, getDecoratedMeasuredHeight(view));
@@ -137,34 +209,57 @@
holder.mBindedItem = item;
}
- public void deleteRangeAndNotify(final int start, final int end) throws Throwable {
+ public void deleteAndNotify(final int start, final int count) throws Throwable {
+ deleteAndNotify(new int[]{start, count});
+ }
+
+ /**
+ * Deletes items in the given ranges.
+ * <p>
+ * Note that each operation affects the one after so you should offset them properly.
+ * <p>
+ * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with
+ * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be
+ * A D E. Then it will delete 2,1 which means it will delete E.
+ */
+ public void deleteAndNotify(final int[]... startCountTuples) throws Throwable {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
- for (int i = start; i < end; i++) {
- mItems.remove(start);
+ for (int t = 0; t < startCountTuples.length; t++) {
+ int[] tuple = startCountTuples[t];
+ for (int i = 0; i < tuple[1]; i++) {
+ mItems.remove(tuple[0]);
+ }
+ notifyItemRangeRemoved(tuple[0], tuple[1]);
}
- notifyItemRangeRemoved(start, end - start);
+
}
});
}
- public void addRangeAndNotify(final int start, final int end) throws Throwable {
+ public void addAndNotify(final int start, final int count) throws Throwable {
+ addAndNotify(new int[]{start, count});
+ }
+
+ public void addAndNotify(final int[]... startCountTuples) throws Throwable {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
- final int count = end - start;
- for (int i = start; i < end; i++) {
- mItems.add(start, new Item(i, "new item " + i));
+ for (int t = 0; t < startCountTuples.length; t++) {
+ int[] tuple = startCountTuples[t];
+ for (int i = 0; i < tuple[1]; i++) {
+ mItems.add(tuple[0], new Item(i, "new item " + i));
+ }
+ // offset others
+ for (int i = tuple[0] + tuple[1]; i < mItems.size(); i++) {
+ mItems.get(i).originalIndex += tuple[1];
+ }
+ notifyItemRangeInserted(tuple[0], tuple[1]);
}
- // offset others
- for (int i = end; i < mItems.size(); i++) {
- mItems.get(i).originalIndex += count;
- }
- notifyItemRangeInserted(start, count);
+
}
});
-
}
@Override
@@ -172,5 +267,4 @@
return mItems.size();
}
}
-}
-
+}
\ No newline at end of file
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index b267487..7d43610 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -16,47 +16,326 @@
package android.support.v7.widget;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
import java.util.concurrent.TimeUnit;
public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
- public void testBasicLayout() throws Throwable {
- final RecyclerView recyclerView = new RecyclerView(getActivity());
- final int itemCount = 10;
- final TestAdapter testAdapter = new TestAdapter(itemCount);
- recyclerView.setAdapter(testAdapter);
- recyclerView.setItemAnimator(null);
- TestLayoutManager layoutManager = new TestLayoutManager() {
- @Override
- public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
- try {
- detachAndScrapAttachedViews(recycler);
+ private static final boolean DEBUG = false;
- layoutRange(recycler, 0, state.getItemCount());
- assertEquals("correct # of children should be rendered",
- state.getItemCount(), getChildCount());
- assertVisibleItemPositions();
- if (getRecyclerView().getItemAnimator() == null) {
- removeAndRecycleScrap(recycler);
- }
- } finally {
- layoutLatch.countDown();
- }
+ private static final String TAG = "RecyclerViewAnimationsTest";
- }
- };
- layoutManager.expectLayouts(1);
- recyclerView.setLayoutManager(layoutManager);
- runTestOnUiThread(new Runnable() {
- @Override
- public void run() {
- getActivity().mContainer.addView(recyclerView);
- }
- });
- layoutManager.waitForLayout(1, TimeUnit.SECONDS);
- layoutManager.expectLayouts(1);
- testAdapter.deleteRangeAndNotify(0, 7);
- layoutManager.waitForLayout(1, TimeUnit.SECONDS);
+ Throwable mainThreadException;
+
+ AnimationLayoutManager mLayoutManager;
+
+ TestAdapter mTestAdapter;
+
+ public RecyclerViewAnimationsTest() {
+ super(DEBUG);
}
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ void checkForMainThreadException() throws Throwable {
+ if (mainThreadException != null) {
+ throw mainThreadException;
+ }
+ }
+
+ RecyclerView setupBasic(int itemCount) throws Throwable {
+ return setupBasic(itemCount, 0, itemCount);
+ }
+
+ RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
+ throws Throwable {
+ final RecyclerView recyclerView = new TestRecyclerView(getActivity());
+ recyclerView.setHasFixedSize(true);
+ mTestAdapter = new TestAdapter(itemCount);
+ recyclerView.setAdapter(mTestAdapter);
+ mLayoutManager = new AnimationLayoutManager();
+ recyclerView.setLayoutManager(mLayoutManager);
+ mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex;
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount;
+
+ mLayoutManager.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ mLayoutManager.waitForLayout(2);
+ return recyclerView;
+ }
+
+ public void testAddInvisibleAndVisible() throws Throwable {
+ setupBasic(10, 1, 7);
+ mLayoutManager.expectLayouts(2);
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
+ mTestAdapter.addAndNotify(0, 1);// add a new item 0 // invisible
+ mTestAdapter.addAndNotify(7, 1);// add a new item after 5th (old 5, new 6)
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testAddInvisible() throws Throwable {
+ setupBasic(10, 1, 7);
+ mLayoutManager.expectLayouts(1);
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
+ mTestAdapter.addAndNotify(0, 1);// add a new item 0
+ mTestAdapter.addAndNotify(8, 1);// add a new item after 6th (old 6, new 7)
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testBasicAdd() throws Throwable {
+ setupBasic(10);
+ mLayoutManager.expectLayouts(2);
+ setExpectedItemCounts(10, 13);
+ mTestAdapter.addAndNotify(2, 3);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testDeleteVisibleAndInvisible() throws Throwable {
+ setupBasic(11, 3, 5); //layout items 3 4 5 6 7
+ mLayoutManager.expectLayouts(2);
+ setLayoutRange(3, 6); //layout previously invisible child 10 from end of the list
+ setExpectedItemCounts(9, 8);
+ mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
+ mLayoutManager.waitForLayout(2);
+ }
+
+ private void setLayoutRange(int start, int count) {
+ mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
+ }
+
+ private void setExpectedItemCounts(int preLayout, int postLayout) {
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
+ }
+
+ public void testDeleteInvisible() throws Throwable {
+ setupBasic(10, 1, 7);
+ mLayoutManager.expectLayouts(1);
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
+ mTestAdapter.deleteAndNotify(0, 1);// delete item id 0
+ mTestAdapter.deleteAndNotify(7, 1);// delete item id 8
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testBasicDelete() throws Throwable {
+ setupBasic(10);
+ final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() {
+ @Override
+ public void postDispatchLayout() {
+ // verify this only in first layout
+ assertEquals("deleted views should still be children of RV",
+ mLayoutManager.getChildCount() + mDeletedViewCount
+ , mRecyclerView.getChildCount());
+ }
+
+ @Override
+ void afterPreLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ super.afterPreLayout(recycler, layoutManager, state);
+ mLayoutItemCount = 3;
+ mLayoutMin = 0;
+ }
+ };
+ callbacks.mLayoutItemCount = 10;
+ callbacks.setExpectedItemCounts(10, 3);
+ mLayoutManager.setOnLayoutCallbacks(callbacks);
+
+ mLayoutManager.expectLayouts(2);
+ mTestAdapter.deleteAndNotify(0, 7);
+ mLayoutManager.waitForLayout(2);
+ callbacks.reset();// when animations end another layout will happen
+ }
+
+
+ class AnimationLayoutManager extends TestLayoutManager {
+
+ OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() {
+ };
+
+ @Override
+ public boolean supportsItemAnimations() {
+ return true;
+ }
+
+ @Override
+ public void expectLayouts(int count) {
+ super.expectLayouts(count);
+ mOnLayoutCallbacks.mLayoutCount = 0;
+ }
+
+ public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) {
+ mOnLayoutCallbacks = onLayoutCallbacks;
+ }
+
+ @Override
+ public final void onLayoutChildren(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ try {
+ mOnLayoutCallbacks.onLayoutChildren(recycler, this, state);
+ } finally {
+ layoutLatch.countDown();
+ }
+ }
+
+
+ public void onPostDispatchLayout() {
+ mOnLayoutCallbacks.postDispatchLayout();
+ }
+
+ @Override
+ public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
+ super.waitForLayout(timeout, timeUnit);
+ checkForMainThreadException();
+ }
+ }
+
+ abstract class OnLayoutCallbacks {
+
+ int mLayoutMin = Integer.MIN_VALUE;
+
+ int mLayoutItemCount = Integer.MAX_VALUE;
+
+ int expectedPreLayoutItemCount = -1;
+
+ int expectedPostLayoutItemCount = -1;
+
+ private int mLayoutCount;
+
+ int mDeletedViewCount;
+
+ void setExpectedItemCounts(int preLayout, int postLayout) {
+ expectedPreLayoutItemCount = preLayout;
+ expectedPostLayoutItemCount = postLayout;
+ }
+
+ void reset() {
+ mLayoutCount = 0;
+ mLayoutMin = Integer.MIN_VALUE;
+ mLayoutItemCount = Integer.MAX_VALUE;
+ expectedPreLayoutItemCount = -1;
+ expectedPostLayoutItemCount = -1;
+ }
+
+ void beforePreLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager lm, RecyclerView.State state) {
+ mDeletedViewCount = 0;
+ for (int i = 0; i < lm.getChildCount(); i++) {
+ View v = lm.getChildAt(i);
+ if (lm.getLp(v).isItemRemoved()) {
+ mDeletedViewCount++;
+ }
+ }
+ }
+
+ void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
+ RecyclerView.State state) {
+ if (DEBUG) {
+ Log.d(TAG, "item count " + state.getItemCount());
+ }
+ lm.detachAndScrapAttachedViews(recycler);
+ final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin;
+ final int count = mLayoutItemCount
+ == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount;
+ lm.layoutRange(recycler, start, start + count);
+ assertEquals("correct # of children should be laid out",
+ count - (inPreLayout() ? mDeletedViewCount : 0), lm.getChildCount());
+ if (!inPreLayout()) { // may not be the correct check
+ lm.assertVisibleItemPositions();
+ }
+ }
+
+ void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
+ RecyclerView.State state) {
+
+ if (mLayoutCount == 0) {
+ if (expectedPreLayoutItemCount != -1) {
+ assertEquals("on pre layout, state should return abstracted adapter size",
+ expectedPreLayoutItemCount, state.getItemCount());
+ }
+ beforePreLayout(recycler, lm, state);
+ } else if (mLayoutCount == 1) {
+ if (expectedPostLayoutItemCount != -1) {
+ assertEquals("on post layout, state should return real adapter size",
+ expectedPostLayoutItemCount, state.getItemCount());
+ }
+ beforePostLayout(recycler, lm, state);
+ }
+ doLayout(recycler, lm, state);
+ if (mLayoutCount == 0) {
+ afterPreLayout(recycler, lm, state);
+ } else if (mLayoutCount == 1) {
+ afterPostLayout(recycler, lm, state);
+ }
+ mLayoutCount++;
+ }
+
+ void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ }
+
+ void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ }
+
+ void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ }
+
+ void postDispatchLayout() {
+ }
+
+ boolean inPreLayout() {
+ return mLayoutCount == 0;
+ }
+ }
+
+ class TestRecyclerView extends RecyclerView {
+
+ public TestRecyclerView(Context context) {
+ super(context);
+ }
+
+ public TestRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ void dispatchLayout() {
+ try {
+ super.dispatchLayout();
+ if (getLayoutManager() instanceof AnimationLayoutManager) {
+ ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout();
+ }
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ }
+
+ }
+
+ private void postExceptionToInstrumentation(Throwable t) {
+ if (DEBUG) {
+ Log.e(TAG, "captured exception on main thread", t);
+ }
+ mainThreadException = t;
+ if (mLayoutManager instanceof TestLayoutManager) {
+ TestLayoutManager lm = mLayoutManager;
+ // finish all layouts so that we get the correct exception
+ while (lm.layoutLatch.getCount() > 0) {
+ lm.layoutLatch.countDown();
+ }
+ }
+ }
+ }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index c3243a3..d218f45 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -315,6 +315,4 @@
super(itemView);
}
}
-
-}
-
+}
\ No newline at end of file
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 4be8bb4..2d076ce 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -66,7 +66,7 @@
assertEquals("in second layout,structure changed should be false", false,
structureChanged.get());
testLayoutManager.expectLayouts(recyclerView.getItemAnimator() == null ? 1 : 2); //
- adapter.deleteRangeAndNotify(3, 5);
+ adapter.deleteAndNotify(3, 2);
testLayoutManager.waitForLayout(3, TimeUnit.SECONDS);
assertEquals("when items are removed, item count in state should be updated",
adapter.getItemCount(),
@@ -74,7 +74,7 @@
assertEquals("structure changed should be true when items are removed", true,
structureChanged.get());
testLayoutManager.expectLayouts(recyclerView.getItemAnimator() == null ? 1 : 2);
- adapter.addRangeAndNotify(2, 7);
+ adapter.addAndNotify(2, 5);
testLayoutManager.waitForLayout(3, TimeUnit.SECONDS);
assertEquals("when items are added, item count in state should be updated",
@@ -85,4 +85,4 @@
}
-}
+}
\ No newline at end of file