| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| package android.support.v7.widget; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import android.support.test.InstrumentationRegistry; |
| import android.graphics.Color; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| import android.os.SystemClock; |
| import android.support.v4.view.ViewCompat; |
| import android.test.TouchUtils; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver; |
| import android.widget.TextView; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import static android.support.v7.widget.RecyclerView.NO_POSITION; |
| import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; |
| import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING; |
| import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING; |
| import static android.support.v7.widget.RecyclerView.getChildViewHolderInt; |
| import android.support.test.runner.AndroidJUnit4; |
| |
| @RunWith(AndroidJUnit4.class) |
| public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest { |
| private static final int FLAG_HORIZONTAL = 1; |
| private static final int FLAG_VERTICAL = 1 << 1; |
| private static final int FLAG_FLING = 1 << 2; |
| |
| private static final boolean DEBUG = false; |
| |
| private static final String TAG = "RecyclerViewLayoutTest"; |
| |
| public RecyclerViewLayoutTest() { |
| super(DEBUG); |
| } |
| |
| @Before |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| injectInstrumentation(InstrumentationRegistry.getInstrumentation()); |
| } |
| |
| @After |
| @Override |
| public void tearDown() throws Exception { |
| super.tearDown(); |
| } |
| |
| @Test |
| public void testFlingFrozen() throws Throwable { |
| testScrollFrozen(true); |
| } |
| |
| @Test |
| public void testDragFrozen() throws Throwable { |
| testScrollFrozen(false); |
| } |
| |
| private void testScrollFrozen(boolean fling) throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| |
| final int horizontalScrollCount = 3; |
| final int verticalScrollCount = 3; |
| final int horizontalVelocity = 1000; |
| final int verticalVelocity = 1000; |
| final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); |
| final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public boolean canScrollHorizontally() { |
| return true; |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutRange(recycler, 0, 10); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| if (verticalCounter.get() > 0) { |
| verticalCounter.decrementAndGet(); |
| return dy; |
| } |
| return 0; |
| } |
| |
| @Override |
| public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| if (horizontalCounter.get() > 0) { |
| horizontalCounter.decrementAndGet(); |
| return dx; |
| } |
| return 0; |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(100); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(2); |
| |
| freezeLayout(true); |
| |
| if (fling) { |
| assertFalse("fling should be blocked", fling(horizontalVelocity, verticalVelocity)); |
| } else { // drag |
| TouchUtils.dragViewTo(this, recyclerView, |
| Gravity.LEFT | Gravity.TOP, |
| mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); |
| } |
| assertEquals("rv's horizontal scroll cb must not run", horizontalScrollCount, |
| horizontalCounter.get()); |
| assertEquals("rv's vertical scroll cb must not run", verticalScrollCount, |
| verticalCounter.get()); |
| |
| freezeLayout(false); |
| |
| if (fling) { |
| assertTrue("fling should be started", fling(horizontalVelocity, verticalVelocity)); |
| } else { // drag |
| TouchUtils.dragViewTo(this, recyclerView, |
| Gravity.LEFT | Gravity.TOP, |
| mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); |
| } |
| assertEquals("rv's horizontal scroll cb must finishes", 0, horizontalCounter.get()); |
| assertEquals("rv's vertical scroll cb must finishes", 0, verticalCounter.get()); |
| } |
| |
| @Test |
| public void testFocusSearchFailFrozen() throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final CountDownLatch focusLatch = new CountDownLatch(1); |
| final AtomicInteger focusSearchCalled = new AtomicInteger(0); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public boolean canScrollHorizontally() { |
| return true; |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutRange(recycler, 0, 10); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public View onFocusSearchFailed(View focused, int direction, |
| RecyclerView.Recycler recycler, RecyclerView.State state) { |
| focusSearchCalled.addAndGet(1); |
| focusLatch.countDown(); |
| return null; |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(100); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(2); |
| |
| final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| c.requestFocus(); |
| } |
| }); |
| assertTrue(c.hasFocus()); |
| freezeLayout(true); |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| assertEquals("onFocusSearchFailed should not be called when layout is frozen", |
| 0, focusSearchCalled.get()); |
| freezeLayout(false); |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| assertTrue(focusLatch.await(2, TimeUnit.SECONDS)); |
| assertEquals(1, focusSearchCalled.get()); |
| } |
| |
| @Test |
| public void testFrozenAndChangeAdapter() throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| |
| final AtomicInteger focusSearchCalled = new AtomicInteger(0); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public boolean canScrollHorizontally() { |
| return true; |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutRange(recycler, 0, 10); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| focusSearchCalled.addAndGet(1); |
| return null; |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(100); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(2); |
| |
| freezeLayout(true); |
| TestAdapter adapter2 = new TestAdapter(1000); |
| setAdapter(adapter2); |
| assertFalse(recyclerView.isLayoutFrozen()); |
| assertSame(adapter2, recyclerView.getAdapter()); |
| |
| freezeLayout(true); |
| TestAdapter adapter3 = new TestAdapter(1000); |
| swapAdapter(adapter3, true); |
| assertFalse(recyclerView.isLayoutFrozen()); |
| assertSame(adapter3, recyclerView.getAdapter()); |
| } |
| |
| @Test |
| public void testScrollToPositionCallback() throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| int scrollPos = RecyclerView.NO_POSITION; |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutLatch.countDown(); |
| if (scrollPos == RecyclerView.NO_POSITION) { |
| layoutRange(recycler, 0, 10); |
| } else { |
| layoutRange(recycler, scrollPos, scrollPos + 10); |
| } |
| } |
| |
| @Override |
| public void scrollToPosition(int position) { |
| scrollPos = position; |
| requestLayout(); |
| } |
| }; |
| recyclerView.setLayoutManager(tlm); |
| TestAdapter adapter = new TestAdapter(100); |
| recyclerView.setAdapter(adapter); |
| final AtomicInteger rvCounter = new AtomicInteger(0); |
| final AtomicInteger viewGroupCounter = new AtomicInteger(0); |
| recyclerView.getViewTreeObserver().addOnScrollChangedListener( |
| new ViewTreeObserver.OnScrollChangedListener() { |
| @Override |
| public void onScrollChanged() { |
| viewGroupCounter.incrementAndGet(); |
| } |
| }); |
| recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrolled(RecyclerView recyclerView, int dx, int dy) { |
| rvCounter.incrementAndGet(); |
| super.onScrolled(recyclerView, dx, dy); |
| } |
| }); |
| tlm.expectLayouts(1); |
| |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(2); |
| // wait for draw :/ |
| Thread.sleep(1000); |
| |
| assertEquals("RV on scroll should be called for initialization", 1, rvCounter.get()); |
| assertEquals("VTO on scroll should be called for initialization", 1, |
| viewGroupCounter.get()); |
| tlm.expectLayouts(1); |
| freezeLayout(true); |
| scrollToPosition(3); |
| tlm.assertNoLayout("scrollToPosition should be ignored", 2); |
| freezeLayout(false); |
| scrollToPosition(3); |
| tlm.waitForLayout(2); |
| assertEquals("RV on scroll should be called", 2, rvCounter.get()); |
| assertEquals("VTO on scroll should be called", 2, viewGroupCounter.get()); |
| tlm.expectLayouts(1); |
| requestLayoutOnUIThread(recyclerView); |
| tlm.waitForLayout(2); |
| // wait for draw :/ |
| Thread.sleep(1000); |
| assertEquals("on scroll should NOT be called", 2, rvCounter.get()); |
| assertEquals("on scroll should NOT be called", 2, viewGroupCounter.get()); |
| |
| } |
| |
| @Test |
| public void testScrollInBothDirectionEqual() throws Throwable { |
| scrollInBothDirection(3, 3, 1000, 1000); |
| } |
| |
| @Test |
| public void testScrollInBothDirectionMoreVertical() throws Throwable { |
| scrollInBothDirection(2, 3, 1000, 1000); |
| } |
| |
| @Test |
| public void testScrollInBothDirectionMoreHorizontal() throws Throwable { |
| scrollInBothDirection(3, 2, 1000, 1000); |
| } |
| |
| @Test |
| public void testScrollHorizontalOnly() throws Throwable { |
| scrollInBothDirection(3, 0, 1000, 0); |
| } |
| |
| @Test |
| public void testScrollVerticalOnly() throws Throwable { |
| scrollInBothDirection(0, 3, 0, 1000); |
| } |
| |
| @Test |
| public void testScrollInBothDirectionEqualReverse() throws Throwable { |
| scrollInBothDirection(3, 3, -1000, -1000); |
| } |
| |
| @Test |
| public void testScrollInBothDirectionMoreVerticalReverse() throws Throwable { |
| scrollInBothDirection(2, 3, -1000, -1000); |
| } |
| |
| @Test |
| public void testScrollInBothDirectionMoreHorizontalReverse() throws Throwable { |
| scrollInBothDirection(3, 2, -1000, -1000); |
| } |
| |
| @Test |
| public void testScrollHorizontalOnlyReverse() throws Throwable { |
| scrollInBothDirection(3, 0, -1000, 0); |
| } |
| |
| @Test |
| public void testScrollVerticalOnlyReverse() throws Throwable { |
| scrollInBothDirection(0, 3, 0, -1000); |
| } |
| |
| public void scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount, |
| int horizontalVelocity, int verticalVelocity) |
| throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); |
| final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public boolean canScrollHorizontally() { |
| return true; |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutRange(recycler, 0, 10); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| if (verticalCounter.get() > 0) { |
| verticalCounter.decrementAndGet(); |
| return dy; |
| } |
| return 0; |
| } |
| |
| @Override |
| public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| if (horizontalCounter.get() > 0) { |
| horizontalCounter.decrementAndGet(); |
| return dx; |
| } |
| return 0; |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(100); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(2); |
| assertTrue("test sanity, fling must run", fling(horizontalVelocity, verticalVelocity)); |
| assertEquals("rv's horizontal scroll cb must run " + horizontalScrollCount + " times'", 0, |
| horizontalCounter.get()); |
| assertEquals("rv's vertical scroll cb must run " + verticalScrollCount + " times'", 0, |
| verticalCounter.get()); |
| } |
| |
| @Test |
| public void testDragHorizontal() throws Throwable { |
| scrollInOtherOrientationTest(FLAG_HORIZONTAL); |
| } |
| |
| @Test |
| public void testDragVertical() throws Throwable { |
| scrollInOtherOrientationTest(FLAG_VERTICAL); |
| } |
| |
| @Test |
| public void testFlingHorizontal() throws Throwable { |
| scrollInOtherOrientationTest(FLAG_HORIZONTAL | FLAG_FLING); |
| } |
| |
| @Test |
| public void testFlingVertical() throws Throwable { |
| scrollInOtherOrientationTest(FLAG_VERTICAL | FLAG_FLING); |
| } |
| |
| @Test |
| public void testNestedDragVertical() throws Throwable { |
| TestedFrameLayout tfl = getActivity().mContainer; |
| tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| scrollInOtherOrientationTest(FLAG_VERTICAL, 0); |
| } |
| |
| @Test |
| public void testNestedDragHorizontal() throws Throwable { |
| TestedFrameLayout tfl = getActivity().mContainer; |
| tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); |
| } |
| |
| @Test |
| public void testNestedDragHorizontalCallsStopNestedScroll() throws Throwable { |
| TestedFrameLayout tfl = getActivity().mContainer; |
| tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); |
| assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); |
| } |
| |
| @Test |
| public void testNestedDragVerticalCallsStopNestedScroll() throws Throwable { |
| TestedFrameLayout tfl = getActivity().mContainer; |
| tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); |
| scrollInOtherOrientationTest(FLAG_VERTICAL, 0); |
| assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); |
| } |
| |
| private void scrollInOtherOrientationTest(int flags) |
| throws Throwable { |
| scrollInOtherOrientationTest(flags, flags); |
| } |
| |
| private void scrollInOtherOrientationTest(final int flags, int expectedFlags) throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final AtomicBoolean scrolledHorizontal = new AtomicBoolean(false); |
| final AtomicBoolean scrolledVertical = new AtomicBoolean(false); |
| |
| final TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public boolean canScrollHorizontally() { |
| return (flags & FLAG_HORIZONTAL) != 0; |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return (flags & FLAG_VERTICAL) != 0; |
| } |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutRange(recycler, 0, 10); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| scrolledVertical.set(true); |
| return super.scrollVerticallyBy(dy, recycler, state); |
| } |
| |
| @Override |
| public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| scrolledHorizontal.set(true); |
| return super.scrollHorizontallyBy(dx, recycler, state); |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(100); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(2); |
| if ( (flags & FLAG_FLING) != 0 ) { |
| int flingVelocity = (mRecyclerView.getMaxFlingVelocity() + |
| mRecyclerView.getMinFlingVelocity()) / 2; |
| assertEquals("fling started", (expectedFlags & FLAG_FLING) != 0, |
| fling(flingVelocity, flingVelocity)); |
| } else { // drag |
| TouchUtils.dragViewTo(this, recyclerView, Gravity.LEFT | Gravity.TOP, |
| mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); |
| } |
| assertEquals("horizontally scrolled: " + tlm.mScrollHorizontallyAmount, |
| (expectedFlags & FLAG_HORIZONTAL) != 0, scrolledHorizontal.get()); |
| assertEquals("vertically scrolled: " + tlm.mScrollVerticallyAmount, |
| (expectedFlags & FLAG_VERTICAL) != 0, scrolledVertical.get()); |
| } |
| |
| private boolean fling(final int velocityX, final int velocityY) throws Throwable { |
| final AtomicBoolean didStart = new AtomicBoolean(false); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| boolean result = mRecyclerView.fling(velocityX, velocityY); |
| didStart.set(result); |
| } |
| }); |
| if (!didStart.get()) { |
| return false; |
| } |
| // cannot set scroll listener in case it is subject to some test so instead doing a busy |
| // loop until state goes idle |
| while (mRecyclerView.getScrollState() != SCROLL_STATE_IDLE) { |
| getInstrumentation().waitForIdleSync(); |
| } |
| return true; |
| } |
| |
| private void assertPendingUpdatesAndLayout(TestLayoutManager testLayoutManager, |
| final Runnable runnable) throws Throwable { |
| testLayoutManager.expectLayouts(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| runnable.run(); |
| assertTrue(mRecyclerView.hasPendingAdapterUpdates()); |
| } |
| }); |
| testLayoutManager.waitForLayout(1); |
| assertFalse(mRecyclerView.hasPendingAdapterUpdates()); |
| } |
| |
| private void setupBasic(RecyclerView recyclerView, TestLayoutManager tlm, |
| TestAdapter adapter, boolean waitForFirstLayout) throws Throwable { |
| recyclerView.setLayoutManager(tlm); |
| recyclerView.setAdapter(adapter); |
| if (waitForFirstLayout) { |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(1); |
| } else { |
| setRecyclerView(recyclerView); |
| } |
| } |
| |
| @Test |
| public void testHasPendingUpdatesBeforeFirstLayout() throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| TestLayoutManager layoutManager = new DumbLayoutManager(); |
| TestAdapter testAdapter = new TestAdapter(10); |
| setupBasic(recyclerView, layoutManager, testAdapter, false); |
| assertTrue(mRecyclerView.hasPendingAdapterUpdates()); |
| } |
| |
| @Test |
| public void testNoPendingUpdatesAfterLayout() throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| TestLayoutManager layoutManager = new DumbLayoutManager(); |
| TestAdapter testAdapter = new TestAdapter(10); |
| setupBasic(recyclerView, layoutManager, testAdapter, true); |
| assertFalse(mRecyclerView.hasPendingAdapterUpdates()); |
| } |
| |
| @Test |
| public void testHasPendingUpdatesWhenAdapterIsChanged() throws Throwable { |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| TestLayoutManager layoutManager = new DumbLayoutManager(); |
| final TestAdapter testAdapter = new TestAdapter(10); |
| setupBasic(recyclerView, layoutManager, testAdapter, false); |
| assertPendingUpdatesAndLayout(layoutManager, new Runnable() { |
| @Override |
| public void run() { |
| testAdapter.notifyItemRemoved(1); |
| } |
| }); |
| assertPendingUpdatesAndLayout(layoutManager, new Runnable() { |
| @Override |
| public void run() { |
| testAdapter.notifyItemInserted(2); |
| } |
| }); |
| |
| assertPendingUpdatesAndLayout(layoutManager, new Runnable() { |
| @Override |
| public void run() { |
| testAdapter.notifyItemMoved(2, 3); |
| } |
| }); |
| |
| assertPendingUpdatesAndLayout(layoutManager, new Runnable() { |
| @Override |
| public void run() { |
| testAdapter.notifyItemChanged(2); |
| } |
| }); |
| |
| assertPendingUpdatesAndLayout(layoutManager, new Runnable() { |
| @Override |
| public void run() { |
| testAdapter.notifyDataSetChanged(); |
| } |
| }); |
| } |
| |
| @Test |
| public void testTransientStateRecycleViaAdapter() throws Throwable { |
| transientStateRecycleTest(true, false); |
| } |
| |
| @Test |
| public void testTransientStateRecycleViaTransientStateCleanup() throws Throwable { |
| transientStateRecycleTest(false, true); |
| } |
| |
| @Test |
| public void testTransientStateDontRecycle() throws Throwable { |
| transientStateRecycleTest(false, false); |
| } |
| |
| public void transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState) |
| throws Throwable { |
| final List<View> failedToRecycle = new ArrayList<View>(); |
| final List<View> recycled = new ArrayList<View>(); |
| TestAdapter testAdapter = new TestAdapter(10) { |
| @Override |
| public boolean onFailedToRecycleView( |
| TestViewHolder holder) { |
| failedToRecycle.add(holder.itemView); |
| if (unsetTransientState) { |
| setHasTransientState(holder.itemView, false); |
| } |
| return succeed; |
| } |
| |
| @Override |
| public void onViewRecycled(TestViewHolder holder) { |
| recycled.add(holder.itemView); |
| super.onViewRecycled(holder); |
| } |
| }; |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| if (getChildCount() == 0) { |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, 5); |
| } else { |
| removeAndRecycleAllViews(recycler); |
| } |
| if (layoutLatch != null) { |
| layoutLatch.countDown(); |
| } |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(tlm); |
| recyclerView.setItemAnimator(null); |
| setRecyclerView(recyclerView); |
| getInstrumentation().waitForIdleSync(); |
| // make sure we have enough views after this position so that we'll receive the on recycled |
| // callback |
| View view = recyclerView.getChildAt(3);//this has to be greater than def cache size. |
| setHasTransientState(view, true); |
| tlm.expectLayouts(1); |
| requestLayoutOnUIThread(recyclerView); |
| tlm.waitForLayout(2); |
| |
| assertTrue(failedToRecycle.contains(view)); |
| assertEquals(succeed || unsetTransientState, recycled.contains(view)); |
| } |
| |
| @Test |
| public void testAdapterPositionInvalidation() throws Throwable { |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final TestAdapter adapter = new TestAdapter(10); |
| final TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutRange(recycler, 0, state.getItemCount()); |
| layoutLatch.countDown(); |
| } |
| }; |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| for (int i = 0; i < tlm.getChildCount(); i++) { |
| assertNotSame("adapter positions should not be undefined", |
| recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), |
| RecyclerView.NO_POSITION); |
| } |
| adapter.notifyDataSetChanged(); |
| for (int i = 0; i < tlm.getChildCount(); i++) { |
| assertSame("adapter positions should be undefined", |
| recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), |
| RecyclerView.NO_POSITION); |
| } |
| } |
| }); |
| } |
| |
| @Test |
| public void testAdapterPositionsBasic() throws Throwable { |
| adapterPositionsTest(null); |
| } |
| |
| @Test |
| public void testAdapterPositionsRemoveItems() throws Throwable { |
| adapterPositionsTest(new AdapterRunnable() { |
| @Override |
| public void run(TestAdapter adapter) throws Throwable { |
| adapter.deleteAndNotify(3, 4); |
| } |
| }); |
| } |
| |
| @Test |
| public void testAdapterPositionsRemoveItemsBefore() throws Throwable { |
| adapterPositionsTest(new AdapterRunnable() { |
| @Override |
| public void run(TestAdapter adapter) throws Throwable { |
| adapter.deleteAndNotify(0, 1); |
| } |
| }); |
| } |
| |
| @Test |
| public void testAdapterPositionsAddItemsBefore() throws Throwable { |
| adapterPositionsTest(new AdapterRunnable() { |
| @Override |
| public void run(TestAdapter adapter) throws Throwable { |
| adapter.addAndNotify(0, 5); |
| } |
| }); |
| } |
| |
| @Test |
| public void testAdapterPositionsAddItemsInside() throws Throwable { |
| adapterPositionsTest(new AdapterRunnable() { |
| @Override |
| public void run(TestAdapter adapter) throws Throwable { |
| adapter.addAndNotify(3, 2); |
| } |
| }); |
| } |
| |
| @Test |
| public void testAdapterPositionsMoveItems() throws Throwable { |
| adapterPositionsTest(new AdapterRunnable() { |
| @Override |
| public void run(TestAdapter adapter) throws Throwable { |
| adapter.moveAndNotify(3, 5); |
| } |
| }); |
| } |
| |
| @Test |
| public void testAdapterPositionsNotifyDataSetChanged() throws Throwable { |
| adapterPositionsTest(new AdapterRunnable() { |
| @Override |
| public void run(TestAdapter adapter) throws Throwable { |
| adapter.mItems.clear(); |
| for (int i = 0; i < 20; i++) { |
| adapter.mItems.add(new Item(i, "added item")); |
| } |
| adapter.notifyDataSetChanged(); |
| } |
| }); |
| } |
| |
| @Test |
| public void testAvoidLeakingRecyclerViewIfViewIsNotRecycled() throws Throwable { |
| final AtomicBoolean failedToRecycle = new AtomicBoolean(false); |
| RecyclerView rv = new RecyclerView(getActivity()); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, state.getItemCount()); |
| layoutLatch.countDown(); |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(10) { |
| @Override |
| public boolean onFailedToRecycleView( |
| TestViewHolder holder) { |
| failedToRecycle.set(true); |
| return false; |
| } |
| }; |
| rv.setAdapter(adapter); |
| rv.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(rv); |
| tlm.waitForLayout(1); |
| final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| ViewCompat.setHasTransientState(vh.itemView, true); |
| } |
| }); |
| tlm.expectLayouts(1); |
| adapter.deleteAndNotify(0, 10); |
| tlm.waitForLayout(2); |
| final CountDownLatch animationsLatch = new CountDownLatch(1); |
| rv.getItemAnimator().isRunning( |
| new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { |
| @Override |
| public void onAnimationsFinished() { |
| animationsLatch.countDown(); |
| } |
| }); |
| assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); |
| assertTrue(failedToRecycle.get()); |
| assertNull(vh.mOwnerRecyclerView); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testAvoidLeakingRecyclerViewViaViewHolder() throws Throwable { |
| RecyclerView rv = new RecyclerView(getActivity()); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, state.getItemCount()); |
| layoutLatch.countDown(); |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(10); |
| rv.setAdapter(adapter); |
| rv.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(rv); |
| tlm.waitForLayout(1); |
| final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); |
| tlm.expectLayouts(1); |
| adapter.deleteAndNotify(0, 10); |
| tlm.waitForLayout(2); |
| final CountDownLatch animationsLatch = new CountDownLatch(1); |
| rv.getItemAnimator().isRunning( |
| new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { |
| @Override |
| public void onAnimationsFinished() { |
| animationsLatch.countDown(); |
| } |
| }); |
| assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); |
| assertNull(vh.mOwnerRecyclerView); |
| checkForMainThreadException(); |
| } |
| |
| public void adapterPositionsTest(final AdapterRunnable adapterChanges) throws Throwable { |
| final TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| try { |
| layoutRange(recycler, Math.min(state.getItemCount(), 2) |
| , Math.min(state.getItemCount(), 7)); |
| layoutLatch.countDown(); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| }; |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setLayoutManager(tlm); |
| recyclerView.setAdapter(testAdapter); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| tlm.waitForLayout(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| final int count = recyclerView.getChildCount(); |
| Map<View, Integer> layoutPositions = new HashMap<View, Integer>(); |
| assertTrue("test sanity", count > 0); |
| for (int i = 0; i < count; i++) { |
| View view = recyclerView.getChildAt(i); |
| TestViewHolder vh = (TestViewHolder) recyclerView.getChildViewHolder(view); |
| int index = testAdapter.mItems.indexOf(vh.mBoundItem); |
| assertEquals("should be able to find VH with adapter position " + index, vh, |
| recyclerView.findViewHolderForAdapterPosition(index)); |
| assertEquals("get adapter position should return correct index", index, |
| vh.getAdapterPosition()); |
| layoutPositions.put(view, vh.mPosition); |
| } |
| if (adapterChanges != null) { |
| adapterChanges.run(testAdapter); |
| for (int i = 0; i < count; i++) { |
| View view = recyclerView.getChildAt(i); |
| TestViewHolder vh = (TestViewHolder) recyclerView |
| .getChildViewHolder(view); |
| int index = testAdapter.mItems.indexOf(vh.mBoundItem); |
| if (index >= 0) { |
| assertEquals("should be able to find VH with adapter position " |
| + index, vh, |
| recyclerView.findViewHolderForAdapterPosition(index)); |
| } |
| assertSame("get adapter position should return correct index", index, |
| vh.getAdapterPosition()); |
| assertSame("should be able to find view with layout position", |
| vh, mRecyclerView.findViewHolderForLayoutPosition( |
| layoutPositions.get(view))); |
| } |
| |
| } |
| |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| }); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testScrollStateForSmoothScroll() throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager tlm = new TestLayoutManager(); |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(tlm); |
| setRecyclerView(recyclerView); |
| getInstrumentation().waitForIdleSync(); |
| assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); |
| final int[] stateCnts = new int[10]; |
| final CountDownLatch latch = new CountDownLatch(2); |
| recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
| stateCnts[newState] = stateCnts[newState] + 1; |
| latch.countDown(); |
| } |
| }); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.smoothScrollBy(0, 500); |
| } |
| }); |
| latch.await(5, TimeUnit.SECONDS); |
| assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); |
| assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); |
| assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); |
| } |
| |
| @Test |
| public void testScrollStateForSmoothScrollWithStop() throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager tlm = new TestLayoutManager(); |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(tlm); |
| setRecyclerView(recyclerView); |
| getInstrumentation().waitForIdleSync(); |
| assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); |
| final int[] stateCnts = new int[10]; |
| final CountDownLatch latch = new CountDownLatch(1); |
| recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
| stateCnts[newState] = stateCnts[newState] + 1; |
| latch.countDown(); |
| } |
| }); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.smoothScrollBy(0, 500); |
| } |
| }); |
| latch.await(5, TimeUnit.SECONDS); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.stopScroll(); |
| } |
| }); |
| assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); |
| assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); |
| assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); |
| assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); |
| } |
| |
| @Test |
| public void testScrollStateForFling() throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager tlm = new TestLayoutManager(); |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(tlm); |
| setRecyclerView(recyclerView); |
| getInstrumentation().waitForIdleSync(); |
| assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); |
| final int[] stateCnts = new int[10]; |
| final CountDownLatch latch = new CountDownLatch(2); |
| recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
| stateCnts[newState] = stateCnts[newState] + 1; |
| latch.countDown(); |
| } |
| }); |
| final ViewConfiguration vc = ViewConfiguration.get(getActivity()); |
| final float fling = vc.getScaledMinimumFlingVelocity() |
| + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .1f; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.fling(0, Math.round(fling)); |
| } |
| }); |
| latch.await(5, TimeUnit.SECONDS); |
| assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); |
| assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); |
| assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); |
| } |
| |
| @Test |
| public void testScrollStateForFlingWithStop() throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager tlm = new TestLayoutManager(); |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(tlm); |
| setRecyclerView(recyclerView); |
| getInstrumentation().waitForIdleSync(); |
| assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); |
| final int[] stateCnts = new int[10]; |
| final CountDownLatch latch = new CountDownLatch(1); |
| recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
| stateCnts[newState] = stateCnts[newState] + 1; |
| latch.countDown(); |
| } |
| }); |
| final ViewConfiguration vc = ViewConfiguration.get(getActivity()); |
| final float fling = vc.getScaledMinimumFlingVelocity() |
| + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .8f; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.fling(0, Math.round(fling)); |
| } |
| }); |
| latch.await(5, TimeUnit.SECONDS); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.stopScroll(); |
| } |
| }); |
| assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); |
| assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); |
| assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); |
| assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); |
| } |
| |
| @Test |
| public void testScrollStateDrag() throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager tlm = new TestLayoutManager(); |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(tlm); |
| setRecyclerView(recyclerView); |
| getInstrumentation().waitForIdleSync(); |
| assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); |
| final int[] stateCnts = new int[10]; |
| recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
| stateCnts[newState] = stateCnts[newState] + 1; |
| } |
| }); |
| drag(mRecyclerView, 0, 0, 0, 500, 5); |
| assertEquals(0, stateCnts[SCROLL_STATE_SETTLING]); |
| assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); |
| assertEquals(1, stateCnts[SCROLL_STATE_DRAGGING]); |
| } |
| |
| public void drag(ViewGroup view, float fromX, float toX, float fromY, float toY, |
| int stepCount) throws Throwable { |
| long downTime = SystemClock.uptimeMillis(); |
| long eventTime = SystemClock.uptimeMillis(); |
| |
| float y = fromY; |
| float x = fromX; |
| |
| float yStep = (toY - fromY) / stepCount; |
| float xStep = (toX - fromX) / stepCount; |
| |
| MotionEvent event = MotionEvent.obtain(downTime, eventTime, |
| MotionEvent.ACTION_DOWN, x, y, 0); |
| sendTouch(view, event); |
| for (int i = 0; i < stepCount; ++i) { |
| y += yStep; |
| x += xStep; |
| eventTime = SystemClock.uptimeMillis(); |
| event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); |
| sendTouch(view, event); |
| } |
| |
| eventTime = SystemClock.uptimeMillis(); |
| event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); |
| sendTouch(view, event); |
| getInstrumentation().waitForIdleSync(); |
| } |
| |
| private void sendTouch(final ViewGroup view, final MotionEvent event) throws Throwable { |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| if (view.onInterceptTouchEvent(event)) { |
| view.onTouchEvent(event); |
| } |
| } |
| }); |
| } |
| |
| @Test |
| public void testRecycleScrap() throws Throwable { |
| recycleScrapTest(false); |
| removeRecyclerView(); |
| recycleScrapTest(true); |
| } |
| |
| public void recycleScrapTest(final boolean useRecycler) throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| final AtomicBoolean test = new AtomicBoolean(false); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| ViewInfoStore infoStore = mRecyclerView.mViewInfoStore; |
| if (test.get()) { |
| try { |
| detachAndScrapAttachedViews(recycler); |
| for (int i = recycler.getScrapList().size() - 1; i >= 0; i--) { |
| if (useRecycler) { |
| recycler.recycleView(recycler.getScrapList().get(i).itemView); |
| } else { |
| removeAndRecycleView(recycler.getScrapList().get(i).itemView, |
| recycler); |
| } |
| } |
| if (infoStore.mOldChangedHolders != null) { |
| for (int i = infoStore.mOldChangedHolders.size() - 1; i >= 0; i--) { |
| if (useRecycler) { |
| recycler.recycleView( |
| infoStore.mOldChangedHolders.valueAt(i).itemView); |
| } else { |
| removeAndRecycleView( |
| infoStore.mOldChangedHolders.valueAt(i).itemView, |
| recycler); |
| } |
| } |
| } |
| assertEquals("no scrap should be left over", 0, recycler.getScrapCount()); |
| assertEquals("pre layout map should be empty", 0, |
| InfoStoreTrojan.sizeOfPreLayout(infoStore)); |
| assertEquals("post layout map should be empty", 0, |
| InfoStoreTrojan.sizeOfPostLayout(infoStore)); |
| if (infoStore.mOldChangedHolders != null) { |
| assertEquals("post old change map should be empty", 0, |
| infoStore.mOldChangedHolders.size()); |
| } |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| |
| } |
| layoutRange(recycler, 0, 5); |
| layoutLatch.countDown(); |
| super.onLayoutChildren(recycler, state); |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(lm); |
| ((SimpleItemAnimator)recyclerView.getItemAnimator()).setSupportsChangeAnimations(true); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| test.set(true); |
| lm.expectLayouts(1); |
| testAdapter.changeAndNotify(3, 1); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testAccessRecyclerOnOnMeasure() throws Throwable { |
| accessRecyclerOnOnMeasureTest(false); |
| removeRecyclerView(); |
| accessRecyclerOnOnMeasureTest(true); |
| } |
| |
| @Test |
| public void testSmoothScrollWithRemovedItemsAndRemoveItem() throws Throwable { |
| smoothScrollTest(true); |
| } |
| |
| @Test |
| public void testSmoothScrollWithRemovedItems() throws Throwable { |
| smoothScrollTest(false); |
| } |
| |
| public void smoothScrollTest(final boolean removeItem) throws Throwable { |
| final LinearSmoothScroller[] lss = new LinearSmoothScroller[1]; |
| final CountDownLatch calledOnStart = new CountDownLatch(1); |
| final CountDownLatch calledOnStop = new CountDownLatch(1); |
| final int visibleChildCount = 10; |
| TestLayoutManager lm = new TestLayoutManager() { |
| int start = 0; |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| layoutRange(recycler, start, visibleChildCount); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| start++; |
| if (DEBUG) { |
| Log.d(TAG, "on scroll, remove and recycling. start:" + start + ", cnt:" |
| + visibleChildCount); |
| } |
| removeAndRecycleAllViews(recycler); |
| layoutRange(recycler, start, |
| Math.max(state.getItemCount(), start + visibleChildCount)); |
| return dy; |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| |
| @Override |
| public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, |
| int position) { |
| LinearSmoothScroller linearSmoothScroller = |
| new LinearSmoothScroller(recyclerView.getContext()) { |
| @Override |
| public PointF computeScrollVectorForPosition(int targetPosition) { |
| return new PointF(0, 1); |
| } |
| |
| @Override |
| protected void onStart() { |
| super.onStart(); |
| calledOnStart.countDown(); |
| } |
| |
| @Override |
| protected void onStop() { |
| super.onStop(); |
| calledOnStop.countDown(); |
| } |
| }; |
| linearSmoothScroller.setTargetPosition(position); |
| lss[0] = linearSmoothScroller; |
| startSmoothScroll(linearSmoothScroller); |
| } |
| }; |
| final RecyclerView rv = new RecyclerView(getActivity()); |
| TestAdapter testAdapter = new TestAdapter(500); |
| rv.setLayoutManager(lm); |
| rv.setAdapter(testAdapter); |
| lm.expectLayouts(1); |
| setRecyclerView(rv); |
| lm.waitForLayout(1); |
| // regular scroll |
| final int targetPosition = visibleChildCount * (removeItem ? 30 : 4); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| rv.smoothScrollToPosition(targetPosition); |
| } |
| }); |
| if (DEBUG) { |
| Log.d(TAG, "scrolling to target position " + targetPosition); |
| } |
| assertTrue("on start should be called very soon", calledOnStart.await(2, TimeUnit.SECONDS)); |
| if (removeItem) { |
| final int newTarget = targetPosition - 10; |
| testAdapter.deleteAndNotify(newTarget + 1, testAdapter.getItemCount() - newTarget - 1); |
| final CountDownLatch targetCheck = new CountDownLatch(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| ViewCompat.postOnAnimationDelayed(rv, new Runnable() { |
| @Override |
| public void run() { |
| try { |
| assertEquals("scroll position should be updated to next available", |
| newTarget, lss[0].getTargetPosition()); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| targetCheck.countDown(); |
| } |
| }, 50); |
| } |
| }); |
| assertTrue("target position should be checked on time ", |
| targetCheck.await(10, TimeUnit.SECONDS)); |
| checkForMainThreadException(); |
| assertTrue("on stop should be called", calledOnStop.await(30, TimeUnit.SECONDS)); |
| checkForMainThreadException(); |
| assertNotNull("should scroll to new target " + newTarget |
| , rv.findViewHolderForLayoutPosition(newTarget)); |
| if (DEBUG) { |
| Log.d(TAG, "on stop has been called on time"); |
| } |
| } else { |
| assertTrue("on stop should be called eventually", |
| calledOnStop.await(30, TimeUnit.SECONDS)); |
| assertNotNull("scroll to position should succeed", |
| rv.findViewHolderForLayoutPosition(targetPosition)); |
| } |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testConsecutiveSmoothScroll() throws Throwable { |
| final AtomicInteger visibleChildCount = new AtomicInteger(10); |
| final AtomicInteger totalScrolled = new AtomicInteger(0); |
| final TestLayoutManager lm = new TestLayoutManager() { |
| int start = 0; |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| layoutRange(recycler, start, visibleChildCount.get()); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| totalScrolled.set(totalScrolled.get() + dy); |
| return dy; |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| }; |
| final RecyclerView rv = new RecyclerView(getActivity()); |
| TestAdapter testAdapter = new TestAdapter(500); |
| rv.setLayoutManager(lm); |
| rv.setAdapter(testAdapter); |
| lm.expectLayouts(1); |
| setRecyclerView(rv); |
| lm.waitForLayout(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| rv.smoothScrollBy(0, 2000); |
| } |
| }); |
| Thread.sleep(250); |
| final AtomicInteger scrollAmt = new AtomicInteger(); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| final int soFar = totalScrolled.get(); |
| scrollAmt.set(soFar); |
| rv.smoothScrollBy(0, 5000 - soFar); |
| } |
| }); |
| while (rv.getScrollState() != SCROLL_STATE_IDLE) { |
| Thread.sleep(100); |
| } |
| final int soFar = totalScrolled.get(); |
| assertEquals("second scroll should be competed properly", 5000, soFar); |
| } |
| |
| public void accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations) |
| throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| final AtomicInteger expectedOnMeasureStateCount = new AtomicInteger(10); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| try { |
| layoutRange(recycler, 0, state.getItemCount()); |
| layoutLatch.countDown(); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| } |
| |
| @Override |
| public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, |
| int widthSpec, int heightSpec) { |
| try { |
| // make sure we access all views |
| for (int i = 0; i < state.getItemCount(); i++) { |
| View view = recycler.getViewForPosition(i); |
| assertNotNull(view); |
| assertEquals(i, getPosition(view)); |
| } |
| assertEquals(state.toString(), |
| expectedOnMeasureStateCount.get(), state.getItemCount()); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| super.onMeasure(recycler, state, widthSpec, heightSpec); |
| } |
| |
| @Override |
| public boolean supportsPredictiveItemAnimations() { |
| return enablePredictiveAnimations; |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| lm.expectLayouts(1); |
| if (!enablePredictiveAnimations) { |
| expectedOnMeasureStateCount.set(15); |
| } |
| testAdapter.addAndNotify(4, 5); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testSetCompatibleAdapter() throws Throwable { |
| compatibleAdapterTest(true, true); |
| removeRecyclerView(); |
| compatibleAdapterTest(false, true); |
| removeRecyclerView(); |
| compatibleAdapterTest(true, false); |
| removeRecyclerView(); |
| compatibleAdapterTest(false, false); |
| removeRecyclerView(); |
| } |
| |
| private void compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews) |
| throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| final AtomicInteger recycledViewCount = new AtomicInteger(); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| try { |
| layoutRange(recycler, 0, state.getItemCount()); |
| layoutLatch.countDown(); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() { |
| @Override |
| public void onViewRecycled(RecyclerView.ViewHolder holder) { |
| recycledViewCount.incrementAndGet(); |
| } |
| }); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView, !useCustomPool); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| lm.expectLayouts(1); |
| swapAdapter(new TestAdapter(10), removeAndRecycleExistingViews); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| if (removeAndRecycleExistingViews) { |
| assertTrue("Previous views should be recycled", recycledViewCount.get() > 0); |
| } else { |
| assertEquals("No views should be recycled if adapters are compatible and developer " |
| + "did not request a recycle", 0, recycledViewCount.get()); |
| } |
| } |
| |
| @Test |
| public void testSetIncompatibleAdapter() throws Throwable { |
| incompatibleAdapterTest(true); |
| incompatibleAdapterTest(false); |
| } |
| |
| public void incompatibleAdapterTest(boolean useCustomPool) throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| try { |
| layoutRange(recycler, 0, state.getItemCount()); |
| layoutLatch.countDown(); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView, !useCustomPool); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| lm.expectLayouts(1); |
| setAdapter(new TestAdapter2(10)); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testRecycleIgnored() throws Throwable { |
| final TestAdapter adapter = new TestAdapter(10); |
| final TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| layoutRange(recycler, 0, 5); |
| layoutLatch.countDown(); |
| } |
| }; |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| View child1 = lm.findViewByPosition(0); |
| View child2 = lm.findViewByPosition(1); |
| lm.ignoreView(child1); |
| lm.ignoreView(child2); |
| |
| lm.removeAndRecycleAllViews(recyclerView.mRecycler); |
| assertEquals("ignored child should not be recycled or removed", 2, |
| lm.getChildCount()); |
| |
| Throwable[] throwables = new Throwable[1]; |
| try { |
| lm.removeAndRecycleView(child1, mRecyclerView.mRecycler); |
| } catch (Throwable t) { |
| throwables[0] = t; |
| } |
| assertTrue("Trying to recycle an ignored view should throw IllegalArgException " |
| , throwables[0] instanceof IllegalArgumentException); |
| lm.removeAllViews(); |
| assertEquals("ignored child should be removed as well ", 0, lm.getChildCount()); |
| } |
| }); |
| } |
| |
| @Test |
| public void testFindIgnoredByPosition() throws Throwable { |
| final TestAdapter adapter = new TestAdapter(10); |
| final TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, 5); |
| layoutLatch.countDown(); |
| } |
| }; |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| Thread.sleep(5000); |
| final int pos = 1; |
| final View[] ignored = new View[1]; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| View child = lm.findViewByPosition(pos); |
| lm.ignoreView(child); |
| ignored[0] = child; |
| } |
| }); |
| assertNotNull("ignored child should not be null", ignored[0]); |
| assertNull("find view by position should not return ignored child", |
| lm.findViewByPosition(pos)); |
| lm.expectLayouts(1); |
| requestLayoutOnUIThread(mRecyclerView); |
| lm.waitForLayout(1); |
| assertEquals("child count should be ", 6, lm.getChildCount()); |
| View replacement = lm.findViewByPosition(pos); |
| assertNotNull("re-layout should replace ignored child w/ another one", replacement); |
| assertNotSame("replacement should be a different view", replacement, ignored[0]); |
| } |
| |
| @Test |
| public void testInvalidateAllDecorOffsets() throws Throwable { |
| final TestAdapter adapter = new TestAdapter(10); |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final AtomicBoolean invalidatedOffsets = new AtomicBoolean(true); |
| recyclerView.setAdapter(adapter); |
| final AtomicInteger layoutCount = new AtomicInteger(4); |
| final RecyclerView.ItemDecoration dummyItemDecoration = new RecyclerView.ItemDecoration() { |
| }; |
| TestLayoutManager testLayoutManager = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| try { |
| // test |
| for (int i = 0; i < getChildCount(); i++) { |
| View child = getChildAt(i); |
| RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) |
| child.getLayoutParams(); |
| assertEquals( |
| "Decor insets validation for VH should have expected value.", |
| invalidatedOffsets.get(), lp.mInsetsDirty); |
| } |
| for (RecyclerView.ViewHolder vh : mRecyclerView.mRecycler.mCachedViews) { |
| RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) |
| vh.itemView.getLayoutParams(); |
| assertEquals( |
| "Decor insets invalidation in cache for VH should have expected " |
| + "value.", |
| invalidatedOffsets.get(), lp.mInsetsDirty); |
| } |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, layoutCount.get()); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| } |
| |
| @Override |
| public boolean supportsPredictiveItemAnimations() { |
| return false; |
| } |
| }; |
| // first layout |
| recyclerView.setItemViewCacheSize(5); |
| recyclerView.setLayoutManager(testLayoutManager); |
| testLayoutManager.expectLayouts(1); |
| setRecyclerView(recyclerView, true, false); |
| testLayoutManager.waitForLayout(2); |
| checkForMainThreadException(); |
| |
| // re-layout w/o any change |
| invalidatedOffsets.set(false); |
| testLayoutManager.expectLayouts(1); |
| requestLayoutOnUIThread(recyclerView); |
| testLayoutManager.waitForLayout(1); |
| checkForMainThreadException(); |
| |
| // invalidate w/o an item decorator |
| |
| invalidateDecorOffsets(recyclerView); |
| testLayoutManager.expectLayouts(1); |
| invalidateDecorOffsets(recyclerView); |
| testLayoutManager.assertNoLayout("layout should not happen", 2); |
| checkForMainThreadException(); |
| |
| // set item decorator, should invalidate |
| invalidatedOffsets.set(true); |
| testLayoutManager.expectLayouts(1); |
| addItemDecoration(mRecyclerView, dummyItemDecoration); |
| testLayoutManager.waitForLayout(1); |
| checkForMainThreadException(); |
| |
| // re-layout w/o any change |
| invalidatedOffsets.set(false); |
| testLayoutManager.expectLayouts(1); |
| requestLayoutOnUIThread(recyclerView); |
| testLayoutManager.waitForLayout(1); |
| checkForMainThreadException(); |
| |
| // invalidate w/ item decorator |
| invalidatedOffsets.set(true); |
| invalidateDecorOffsets(recyclerView); |
| testLayoutManager.expectLayouts(1); |
| invalidateDecorOffsets(recyclerView); |
| testLayoutManager.waitForLayout(2); |
| checkForMainThreadException(); |
| |
| // trigger cache. |
| layoutCount.set(3); |
| invalidatedOffsets.set(false); |
| testLayoutManager.expectLayouts(1); |
| requestLayoutOnUIThread(mRecyclerView); |
| testLayoutManager.waitForLayout(1); |
| checkForMainThreadException(); |
| assertEquals("a view should be cached", 1, mRecyclerView.mRecycler.mCachedViews.size()); |
| |
| layoutCount.set(5); |
| invalidatedOffsets.set(true); |
| testLayoutManager.expectLayouts(1); |
| invalidateDecorOffsets(recyclerView); |
| testLayoutManager.waitForLayout(1); |
| checkForMainThreadException(); |
| |
| // remove item decorator |
| invalidatedOffsets.set(true); |
| testLayoutManager.expectLayouts(1); |
| removeItemDecoration(mRecyclerView, dummyItemDecoration); |
| testLayoutManager.waitForLayout(1); |
| checkForMainThreadException(); |
| } |
| |
| public void addItemDecoration(final RecyclerView recyclerView, final |
| RecyclerView.ItemDecoration itemDecoration) throws Throwable { |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| recyclerView.addItemDecoration(itemDecoration); |
| } |
| }); |
| } |
| |
| public void removeItemDecoration(final RecyclerView recyclerView, final |
| RecyclerView.ItemDecoration itemDecoration) throws Throwable { |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| recyclerView.removeItemDecoration(itemDecoration); |
| } |
| }); |
| } |
| |
| public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable { |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| recyclerView.invalidateItemDecorations(); |
| } |
| }); |
| } |
| |
| @Test |
| public void testInvalidateDecorOffsets() throws Throwable { |
| final TestAdapter adapter = new TestAdapter(10); |
| adapter.setHasStableIds(true); |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(adapter); |
| |
| final Map<Long, Boolean> changes = new HashMap<Long, Boolean>(); |
| |
| TestLayoutManager testLayoutManager = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| try { |
| if (changes.size() > 0) { |
| // test |
| for (int i = 0; i < getChildCount(); i++) { |
| View child = getChildAt(i); |
| RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) |
| child.getLayoutParams(); |
| RecyclerView.ViewHolder vh = lp.mViewHolder; |
| if (!changes.containsKey(vh.getItemId())) { |
| continue; //nothing to test |
| } |
| assertEquals( |
| "Decord insets validation for VH should have expected value.", |
| changes.get(vh.getItemId()).booleanValue(), |
| lp.mInsetsDirty); |
| } |
| } |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, state.getItemCount()); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| } |
| |
| @Override |
| public boolean supportsPredictiveItemAnimations() { |
| return false; |
| } |
| }; |
| recyclerView.setLayoutManager(testLayoutManager); |
| testLayoutManager.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| testLayoutManager.waitForLayout(2); |
| int itemAddedTo = 5; |
| for (int i = 0; i < itemAddedTo; i++) { |
| changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); |
| } |
| for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) { |
| changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); |
| } |
| testLayoutManager.expectLayouts(1); |
| adapter.addAndNotify(5, 1); |
| testLayoutManager.waitForLayout(2); |
| checkForMainThreadException(); |
| |
| changes.clear(); |
| int[] changedItems = new int[]{3, 5, 6}; |
| for (int i = 0; i < adapter.getItemCount(); i++) { |
| changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); |
| } |
| for (int i = 0; i < changedItems.length; i++) { |
| changes.put(mRecyclerView.findViewHolderForLayoutPosition(changedItems[i]).getItemId(), |
| true); |
| } |
| testLayoutManager.expectLayouts(1); |
| adapter.changePositionsAndNotify(changedItems); |
| testLayoutManager.waitForLayout(2); |
| checkForMainThreadException(); |
| |
| for (int i = 0; i < adapter.getItemCount(); i++) { |
| changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); |
| } |
| testLayoutManager.expectLayouts(1); |
| adapter.dispatchDataSetChanged(); |
| testLayoutManager.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testMovingViaStableIds() throws Throwable { |
| stableIdsMoveTest(true); |
| removeRecyclerView(); |
| stableIdsMoveTest(false); |
| removeRecyclerView(); |
| } |
| |
| public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable { |
| final TestAdapter testAdapter = new TestAdapter(10); |
| testAdapter.setHasStableIds(true); |
| final AtomicBoolean test = new AtomicBoolean(false); |
| final int movedViewFromIndex = 3; |
| final int movedViewToIndex = 6; |
| final View[] movedView = new View[1]; |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| try { |
| if (test.get()) { |
| if (state.isPreLayout()) { |
| View view = recycler.getViewForPosition(movedViewFromIndex, true); |
| assertSame("In pre layout, should be able to get moved view w/ old " |
| + "position", movedView[0], view); |
| RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); |
| assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); |
| // clear scrap flag |
| holder.clearReturnedFromScrapFlag(); |
| } else { |
| View view = recycler.getViewForPosition(movedViewToIndex, true); |
| assertSame("In post layout, should be able to get moved view w/ new " |
| + "position", movedView[0], view); |
| RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); |
| assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); |
| // clear scrap flag |
| holder.clearReturnedFromScrapFlag(); |
| } |
| } |
| layoutRange(recycler, 0, state.getItemCount()); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| |
| |
| } |
| |
| @Override |
| public boolean supportsPredictiveItemAnimations() { |
| return supportsPredictive; |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(this.getActivity()); |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(1); |
| |
| movedView[0] = recyclerView.getChildAt(movedViewFromIndex); |
| test.set(true); |
| lm.expectLayouts(supportsPredictive ? 2 : 1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| Item item = testAdapter.mItems.remove(movedViewFromIndex); |
| testAdapter.mItems.add(movedViewToIndex, item); |
| testAdapter.notifyItemRemoved(movedViewFromIndex); |
| testAdapter.notifyItemInserted(movedViewToIndex); |
| } |
| }); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testAdapterChangeDuringLayout() throws Throwable { |
| adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyDataSetChanged(); |
| } |
| }); |
| |
| adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyItemChanged(2); |
| } |
| }); |
| |
| adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyItemInserted(2); |
| } |
| }); |
| adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyItemRemoved(2); |
| } |
| }); |
| } |
| |
| public void adapterChangeInMainThreadTest(String msg, |
| final Runnable onLayoutRunnable) throws Throwable { |
| final AtomicBoolean doneFirstLayout = new AtomicBoolean(false); |
| TestAdapter testAdapter = new TestAdapter(10); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| try { |
| layoutRange(recycler, 0, state.getItemCount()); |
| if (doneFirstLayout.get()) { |
| onLayoutRunnable.run(); |
| } |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setAdapter(testAdapter); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| doneFirstLayout.set(true); |
| lm.expectLayouts(1); |
| requestLayoutOnUIThread(recyclerView); |
| lm.waitForLayout(2); |
| removeRecyclerView(); |
| assertTrue("Invalid data updates should be caught:" + msg, |
| mainThreadException instanceof IllegalStateException); |
| mainThreadException = null; |
| } |
| |
| @Test |
| public void testAdapterChangeDuringScroll() throws Throwable { |
| for (int orientation : new int[]{OrientationHelper.HORIZONTAL, |
| OrientationHelper.VERTICAL}) { |
| adapterChangeDuringScrollTest("notifyDataSetChanged", orientation, |
| new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyDataSetChanged(); |
| } |
| }); |
| adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyItemChanged(2); |
| } |
| }); |
| |
| adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyItemInserted(2); |
| } |
| }); |
| adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.getAdapter().notifyItemRemoved(2); |
| } |
| }); |
| } |
| } |
| |
| public void adapterChangeDuringScrollTest(String msg, final int orientation, |
| final Runnable onScrollRunnable) throws Throwable { |
| TestAdapter testAdapter = new TestAdapter(100); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| try { |
| layoutRange(recycler, 0, 10); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return orientation == OrientationHelper.VERTICAL; |
| } |
| |
| @Override |
| public boolean canScrollHorizontally() { |
| return orientation == OrientationHelper.HORIZONTAL; |
| } |
| |
| public int mockScroll() { |
| try { |
| onScrollRunnable.run(); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| return 0; |
| } |
| |
| @Override |
| public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| return mockScroll(); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| return mockScroll(); |
| } |
| }; |
| RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setAdapter(testAdapter); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| lm.expectLayouts(1); |
| scrollBy(200); |
| lm.waitForLayout(2); |
| removeRecyclerView(); |
| assertTrue("Invalid data updates should be caught:" + msg, |
| mainThreadException instanceof IllegalStateException); |
| mainThreadException = null; |
| } |
| |
| @Test |
| public void testRecycleOnDetach() throws Throwable { |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final TestAdapter testAdapter = new TestAdapter(10); |
| final AtomicBoolean didRunOnDetach = new AtomicBoolean(false); |
| final TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| layoutRange(recycler, 0, state.getItemCount() - 1); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { |
| super.onDetachedFromWindow(view, recycler); |
| didRunOnDetach.set(true); |
| removeAndRecycleAllViews(recycler); |
| } |
| }; |
| recyclerView.setAdapter(testAdapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| removeRecyclerView(); |
| assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get()); |
| assertEquals("All children should be recycled", recyclerView.getChildCount(), 0); |
| } |
| |
| @Test |
| public void testUpdatesWhileDetached() throws Throwable { |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final int initialAdapterSize = 20; |
| final TestAdapter adapter = new TestAdapter(initialAdapterSize); |
| final AtomicInteger layoutCount = new AtomicInteger(0); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| layoutRange(recycler, 0, 5); |
| layoutCount.incrementAndGet(); |
| layoutLatch.countDown(); |
| } |
| }; |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setHasFixedSize(true); |
| lm.expectLayouts(1); |
| adapter.addAndNotify(4, 5); |
| lm.assertNoLayout("When RV is not attached, layout should not happen", 1); |
| } |
| |
| @Test |
| public void testUpdatesAfterDetach() throws Throwable { |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final int initialAdapterSize = 20; |
| final TestAdapter adapter = new TestAdapter(initialAdapterSize); |
| final AtomicInteger layoutCount = new AtomicInteger(0); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| layoutRange(recycler, 0, 5); |
| layoutCount.incrementAndGet(); |
| layoutLatch.countDown(); |
| } |
| }; |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| recyclerView.setHasFixedSize(true); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| lm.expectLayouts(1); |
| final int prevLayoutCount = layoutCount.get(); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| adapter.addAndNotify(4, 5); |
| removeRecyclerView(); |
| } catch (Throwable throwable) { |
| postExceptionToInstrumentation(throwable); |
| } |
| } |
| }); |
| checkForMainThreadException(); |
| |
| lm.assertNoLayout("When RV is not attached, layout should not happen", 1); |
| assertEquals("No extra layout should happen when detached", prevLayoutCount, |
| layoutCount.get()); |
| } |
| |
| @Test |
| public void testNotifyDataSetChangedWithStableIds() throws Throwable { |
| final int defaultViewType = 1; |
| final Map<Item, Integer> viewTypeMap = new HashMap<Item, Integer>(); |
| final Map<Integer, Integer> oldPositionToNewPositionMapping = |
| new HashMap<Integer, Integer>(); |
| final TestAdapter adapter = new TestAdapter(100) { |
| @Override |
| public int getItemViewType(int position) { |
| Integer type = viewTypeMap.get(mItems.get(position)); |
| return type == null ? defaultViewType : type; |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return mItems.get(position).mId; |
| } |
| }; |
| adapter.setHasStableIds(true); |
| final ArrayList<Item> previousItems = new ArrayList<Item>(); |
| previousItems.addAll(adapter.mItems); |
| |
| final AtomicInteger layoutStart = new AtomicInteger(50); |
| final AtomicBoolean validate = new AtomicBoolean(false); |
| final int childCount = 10; |
| final TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| try { |
| super.onLayoutChildren(recycler, state); |
| if (validate.get()) { |
| assertEquals("Cached views should be kept", 5, recycler |
| .mCachedViews.size()); |
| for (RecyclerView.ViewHolder vh : recycler.mCachedViews) { |
| TestViewHolder tvh = (TestViewHolder) vh; |
| assertTrue("view holder should be marked for update", |
| tvh.needsUpdate()); |
| assertTrue("view holder should be marked as invalid", tvh.isInvalid()); |
| } |
| } |
| detachAndScrapAttachedViews(recycler); |
| if (validate.get()) { |
| assertEquals("cache size should stay the same", 5, |
| recycler.mCachedViews.size()); |
| assertEquals("all views should be scrapped", childCount, |
| recycler.getScrapList().size()); |
| for (RecyclerView.ViewHolder vh : recycler.getScrapList()) { |
| // TODO create test case for type change |
| TestViewHolder tvh = (TestViewHolder) vh; |
| assertTrue("view holder should be marked for update", |
| tvh.needsUpdate()); |
| assertTrue("view holder should be marked as invalid", tvh.isInvalid()); |
| } |
| } |
| layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); |
| if (validate.get()) { |
| for (int i = 0; i < getChildCount(); i++) { |
| View view = getChildAt(i); |
| TestViewHolder tvh = (TestViewHolder) mRecyclerView |
| .getChildViewHolder(view); |
| final int oldPos = previousItems.indexOf(tvh.mBoundItem); |
| assertEquals("view holder's position should be correct", |
| oldPositionToNewPositionMapping.get(oldPos).intValue(), |
| tvh.getLayoutPosition()); |
| ; |
| } |
| } |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } finally { |
| layoutLatch.countDown(); |
| } |
| } |
| }; |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setItemAnimator(null); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setItemViewCacheSize(10); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| getInstrumentation().waitForIdleSync(); |
| layoutStart.set(layoutStart.get() + 5);//55 |
| lm.expectLayouts(1); |
| requestLayoutOnUIThread(recyclerView); |
| lm.waitForLayout(2); |
| validate.set(true); |
| lm.expectLayouts(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| adapter.moveItems(false, |
| new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2}, |
| new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64}, |
| new int[]{75, 58}); |
| for (int i = 0; i < previousItems.size(); i++) { |
| Item item = previousItems.get(i); |
| oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item)); |
| } |
| adapter.dispatchDataSetChanged(); |
| } catch (Throwable throwable) { |
| postExceptionToInstrumentation(throwable); |
| } |
| } |
| }); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testCallbacksDuringAdapterSwap() throws Throwable { |
| callbacksDuringAdapterChange(true); |
| } |
| |
| @Test |
| public void testCallbacksDuringAdapterSet() throws Throwable { |
| callbacksDuringAdapterChange(false); |
| } |
| |
| public void callbacksDuringAdapterChange(boolean swap) throws Throwable { |
| final TestAdapter2 adapter1 = swap ? createBinderCheckingAdapter() |
| : createOwnerCheckingAdapter(); |
| final TestAdapter2 adapter2 = swap ? createBinderCheckingAdapter() |
| : createOwnerCheckingAdapter(); |
| |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| try { |
| layoutRange(recycler, 0, state.getItemCount()); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| layoutLatch.countDown(); |
| } |
| }; |
| RecyclerView rv = new RecyclerView(getActivity()); |
| rv.setAdapter(adapter1); |
| rv.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(rv); |
| tlm.waitForLayout(1); |
| checkForMainThreadException(); |
| tlm.expectLayouts(1); |
| if (swap) { |
| swapAdapter(adapter2, true); |
| } else { |
| setAdapter(adapter2); |
| } |
| checkForMainThreadException(); |
| tlm.waitForLayout(1); |
| checkForMainThreadException(); |
| } |
| |
| private TestAdapter2 createOwnerCheckingAdapter() { |
| return new TestAdapter2(10) { |
| @Override |
| public void onViewRecycled(TestViewHolder2 holder) { |
| assertSame("on recycled should be called w/ the creator adapter", this, |
| holder.mData); |
| super.onViewRecycled(holder); |
| } |
| |
| @Override |
| public void onBindViewHolder(TestViewHolder2 holder, int position) { |
| super.onBindViewHolder(holder, position); |
| assertSame("on bind should be called w/ the creator adapter", this, holder.mData); |
| } |
| |
| @Override |
| public TestViewHolder2 onCreateViewHolder(ViewGroup parent, |
| int viewType) { |
| final TestViewHolder2 vh = super.onCreateViewHolder(parent, viewType); |
| vh.mData = this; |
| return vh; |
| } |
| }; |
| } |
| |
| private TestAdapter2 createBinderCheckingAdapter() { |
| return new TestAdapter2(10) { |
| @Override |
| public void onViewRecycled(TestViewHolder2 holder) { |
| assertSame("on recycled should be called w/ the creator adapter", this, |
| holder.mData); |
| holder.mData = null; |
| super.onViewRecycled(holder); |
| } |
| |
| @Override |
| public void onBindViewHolder(TestViewHolder2 holder, int position) { |
| super.onBindViewHolder(holder, position); |
| holder.mData = this; |
| } |
| }; |
| } |
| |
| @Test |
| public void testFindViewById() throws Throwable { |
| findViewByIdTest(false); |
| removeRecyclerView(); |
| findViewByIdTest(true); |
| } |
| |
| public void findViewByIdTest(final boolean supportPredictive) throws Throwable { |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final int initialAdapterSize = 20; |
| final TestAdapter adapter = new TestAdapter(initialAdapterSize); |
| final int deleteStart = 6; |
| final int deleteCount = 5; |
| recyclerView.setAdapter(adapter); |
| final AtomicBoolean assertPositions = new AtomicBoolean(false); |
| TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| if (assertPositions.get()) { |
| if (state.isPreLayout()) { |
| for (int i = 0; i < deleteStart; i++) { |
| View view = findViewByPosition(i); |
| assertNotNull("find view by position for existing items should work " |
| + "fine", view); |
| assertFalse("view should not be marked as removed", |
| ((RecyclerView.LayoutParams) view.getLayoutParams()) |
| .isItemRemoved()); |
| } |
| for (int i = 0; i < deleteCount; i++) { |
| View view = findViewByPosition(i + deleteStart); |
| assertNotNull("find view by position should work fine for removed " |
| + "views in pre-layout", view); |
| assertTrue("view should be marked as removed", |
| ((RecyclerView.LayoutParams) view.getLayoutParams()) |
| .isItemRemoved()); |
| } |
| for (int i = deleteStart + deleteCount; i < 20; i++) { |
| View view = findViewByPosition(i); |
| assertNotNull(view); |
| assertFalse("view should not be marked as removed", |
| ((RecyclerView.LayoutParams) view.getLayoutParams()) |
| .isItemRemoved()); |
| } |
| } else { |
| for (int i = 0; i < initialAdapterSize - deleteCount; i++) { |
| View view = findViewByPosition(i); |
| assertNotNull("find view by position for existing item " + i + |
| " should work fine. child count:" + getChildCount(), view); |
| TestViewHolder viewHolder = |
| (TestViewHolder) mRecyclerView.getChildViewHolder(view); |
| assertSame("should be the correct item " + viewHolder |
| , viewHolder.mBoundItem, |
| adapter.mItems.get(viewHolder.mPosition)); |
| assertFalse("view should not be marked as removed", |
| ((RecyclerView.LayoutParams) view.getLayoutParams()) |
| .isItemRemoved()); |
| } |
| } |
| } |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, state.getItemCount() - 1, -1); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public boolean supportsPredictiveItemAnimations() { |
| return supportPredictive; |
| } |
| }; |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| getInstrumentation().waitForIdleSync(); |
| |
| assertPositions.set(true); |
| lm.expectLayouts(supportPredictive ? 2 : 1); |
| adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1}); |
| lm.waitForLayout(2); |
| } |
| |
| @Test |
| public void testTypeForCache() throws Throwable { |
| final AtomicInteger viewType = new AtomicInteger(1); |
| final TestAdapter adapter = new TestAdapter(100) { |
| @Override |
| public int getItemViewType(int position) { |
| return viewType.get(); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return mItems.get(position).mId; |
| } |
| }; |
| adapter.setHasStableIds(true); |
| final AtomicInteger layoutStart = new AtomicInteger(2); |
| final int childCount = 10; |
| final TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); |
| layoutLatch.countDown(); |
| } |
| }; |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setItemAnimator(null); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(lm); |
| recyclerView.setItemViewCacheSize(10); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| getInstrumentation().waitForIdleSync(); |
| layoutStart.set(4); // trigger a cache for 3,4 |
| lm.expectLayouts(1); |
| requestLayoutOnUIThread(recyclerView); |
| lm.waitForLayout(2); |
| // |
| viewType.incrementAndGet(); |
| layoutStart.set(2); // go back to bring views from cache |
| lm.expectLayouts(1); |
| adapter.mItems.remove(1); |
| adapter.dispatchDataSetChanged(); |
| lm.waitForLayout(2); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| for (int i = 2; i < 4; i++) { |
| RecyclerView.ViewHolder vh = recyclerView.findViewHolderForLayoutPosition(i); |
| assertEquals("View holder's type should match latest type", viewType.get(), |
| vh.getItemViewType()); |
| } |
| } |
| }); |
| } |
| |
| @Test |
| public void testTypeForExistingViews() throws Throwable { |
| final AtomicInteger viewType = new AtomicInteger(1); |
| final int invalidatedCount = 2; |
| final int layoutStart = 2; |
| final TestAdapter adapter = new TestAdapter(100) { |
| @Override |
| public int getItemViewType(int position) { |
| return viewType.get(); |
| } |
| |
| @Override |
| public void onBindViewHolder(TestViewHolder holder, |
| int position) { |
| super.onBindViewHolder(holder, position); |
| if (position >= layoutStart && position < invalidatedCount + layoutStart) { |
| try { |
| assertEquals("holder type should match current view type at position " + |
| position, viewType.get(), holder.getItemViewType()); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return mItems.get(position).mId; |
| } |
| }; |
| adapter.setHasStableIds(true); |
| |
| final int childCount = 10; |
| final TestLayoutManager lm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, layoutStart, layoutStart + childCount); |
| layoutLatch.countDown(); |
| } |
| }; |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(lm); |
| lm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| lm.waitForLayout(2); |
| getInstrumentation().waitForIdleSync(); |
| viewType.incrementAndGet(); |
| lm.expectLayouts(1); |
| adapter.changeAndNotify(layoutStart, invalidatedCount); |
| lm.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| |
| @Test |
| public void testState() throws Throwable { |
| final TestAdapter adapter = new TestAdapter(10); |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setItemAnimator(null); |
| final AtomicInteger itemCount = new AtomicInteger(); |
| final AtomicBoolean structureChanged = new AtomicBoolean(); |
| TestLayoutManager testLayoutManager = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, state.getItemCount()); |
| itemCount.set(state.getItemCount()); |
| structureChanged.set(state.didStructureChange()); |
| layoutLatch.countDown(); |
| } |
| }; |
| recyclerView.setLayoutManager(testLayoutManager); |
| testLayoutManager.expectLayouts(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| getActivity().mContainer.addView(recyclerView); |
| } |
| }); |
| testLayoutManager.waitForLayout(2, TimeUnit.SECONDS); |
| |
| assertEquals("item count in state should be correct", adapter.getItemCount() |
| , itemCount.get()); |
| assertEquals("structure changed should be true for first layout", true, |
| structureChanged.get()); |
| Thread.sleep(1000); //wait for other layouts. |
| testLayoutManager.expectLayouts(1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| recyclerView.requestLayout(); |
| } |
| }); |
| testLayoutManager.waitForLayout(2); |
| assertEquals("in second layout,structure changed should be false", false, |
| structureChanged.get()); |
| testLayoutManager.expectLayouts(1); // |
| adapter.deleteAndNotify(3, 2); |
| testLayoutManager.waitForLayout(2); |
| assertEquals("when items are removed, item count in state should be updated", |
| adapter.getItemCount(), |
| itemCount.get()); |
| assertEquals("structure changed should be true when items are removed", true, |
| structureChanged.get()); |
| testLayoutManager.expectLayouts(1); |
| adapter.addAndNotify(2, 5); |
| testLayoutManager.waitForLayout(2); |
| |
| assertEquals("when items are added, item count in state should be updated", |
| adapter.getItemCount(), |
| itemCount.get()); |
| assertEquals("structure changed should be true when items are removed", true, |
| structureChanged.get()); |
| } |
| |
| @Test |
| public void testDetachWithoutLayoutManager() throws Throwable { |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| setRecyclerView(recyclerView); |
| removeRecyclerView(); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| }); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testUpdateHiddenView() throws Throwable { |
| final RecyclerView.ViewHolder[] mTargetVH = new RecyclerView.ViewHolder[1]; |
| final RecyclerView recyclerView = new RecyclerView(getActivity()); |
| final int[] preLayoutRange = new int[]{0, 10}; |
| final int[] postLayoutRange = new int[]{0, 10}; |
| final AtomicBoolean enableGetViewTest = new AtomicBoolean(false); |
| final List<Integer> disappearingPositions = new ArrayList<Integer>(); |
| final TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public boolean supportsPredictiveItemAnimations() { |
| return true; |
| } |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| try { |
| final int[] layoutRange = state.isPreLayout() ? preLayoutRange |
| : postLayoutRange; |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, layoutRange[0], layoutRange[1]); |
| if (!state.isPreLayout()) { |
| for (Integer position : disappearingPositions) { |
| // test sanity. |
| assertNull(findViewByPosition(position)); |
| final View view = recycler.getViewForPosition(position); |
| addDisappearingView(view); |
| measureChildWithMargins(view, 0, 0); |
| // position item out of bounds. |
| view.layout(0, -500, view.getMeasuredWidth(), |
| -500 + view.getMeasuredHeight()); |
| } |
| } |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| layoutLatch.countDown(); |
| } |
| }; |
| |
| recyclerView.getItemAnimator().setMoveDuration(2000); |
| recyclerView.getItemAnimator().setRemoveDuration(2000); |
| final TestAdapter adapter = new TestAdapter(100); |
| recyclerView.setAdapter(adapter); |
| recyclerView.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(recyclerView); |
| |
| tlm.waitForLayout(1); |
| checkForMainThreadException(); |
| mTargetVH[0] = recyclerView.findViewHolderForAdapterPosition(0); |
| // now, a child disappears |
| disappearingPositions.add(0); |
| // layout one shifted |
| postLayoutRange[0] = 1; |
| postLayoutRange[1] = 11; |
| tlm.expectLayouts(2); |
| adapter.addAndNotify(8, 1); |
| tlm.waitForLayout(2); |
| checkForMainThreadException(); |
| |
| tlm.expectLayouts(2); |
| disappearingPositions.clear(); |
| // now that item should be moving, invalidate it and delete it. |
| enableGetViewTest.set(true); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| adapter.changeAndNotify(0, 1); |
| adapter.deleteAndNotify(0, 1); |
| } catch (Throwable throwable) { |
| throwable.printStackTrace(); |
| } |
| } |
| }); |
| tlm.waitForLayout(2); |
| checkForMainThreadException(); |
| } |
| |
| @Test |
| public void testFocusBigViewOnTop() throws Throwable { |
| focusTooBigViewTest(Gravity.TOP); |
| } |
| |
| @Test |
| public void testFocusBigViewOnLeft() throws Throwable { |
| focusTooBigViewTest(Gravity.LEFT); |
| } |
| |
| @Test |
| public void testFocusBigViewOnRight() throws Throwable { |
| focusTooBigViewTest(Gravity.RIGHT); |
| } |
| |
| @Test |
| public void testFocusBigViewOnBottom() throws Throwable { |
| focusTooBigViewTest(Gravity.BOTTOM); |
| } |
| |
| @Test |
| public void testFocusBigViewOnLeftRTL() throws Throwable { |
| focusTooBigViewTest(Gravity.LEFT, true); |
| assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, |
| mRecyclerView.getLayoutManager().getLayoutDirection()); |
| } |
| |
| @Test |
| public void testFocusBigViewOnRightRTL() throws Throwable { |
| focusTooBigViewTest(Gravity.RIGHT, true); |
| assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, |
| mRecyclerView.getLayoutManager().getLayoutDirection()); |
| } |
| |
| public void focusTooBigViewTest(final int gravity) throws Throwable { |
| focusTooBigViewTest(gravity, false); |
| } |
| |
| public void focusTooBigViewTest(final int gravity, final boolean rtl) throws Throwable { |
| RecyclerView rv = new RecyclerView(getActivity()); |
| if (rtl) { |
| ViewCompat.setLayoutDirection(rv, ViewCompat.LAYOUT_DIRECTION_RTL); |
| } |
| final AtomicInteger vScrollDist = new AtomicInteger(0); |
| final AtomicInteger hScrollDist = new AtomicInteger(0); |
| final AtomicInteger vDesiredDist = new AtomicInteger(0); |
| final AtomicInteger hDesiredDist = new AtomicInteger(0); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| |
| @Override |
| public int getLayoutDirection() { |
| return rtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR; |
| } |
| |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| final View view = recycler.getViewForPosition(0); |
| addView(view); |
| int left = 0, top = 0; |
| view.setBackgroundColor(Color.rgb(0, 0, 255)); |
| switch (gravity) { |
| case Gravity.LEFT: |
| case Gravity.RIGHT: |
| view.measure( |
| View.MeasureSpec.makeMeasureSpec((int) (getWidth() * 1.5), |
| View.MeasureSpec.EXACTLY), |
| View.MeasureSpec.makeMeasureSpec((int) (getHeight() * .9), |
| View.MeasureSpec.AT_MOST)); |
| left = gravity == Gravity.LEFT ? getWidth() - view.getMeasuredWidth() - 80 |
| : 90; |
| top = 0; |
| if (ViewCompat.LAYOUT_DIRECTION_RTL == getLayoutDirection()) { |
| hDesiredDist.set((left + view.getMeasuredWidth()) - getWidth()); |
| } else { |
| hDesiredDist.set(left); |
| } |
| break; |
| case Gravity.TOP: |
| case Gravity.BOTTOM: |
| view.measure( |
| View.MeasureSpec.makeMeasureSpec((int) (getWidth() * .9), |
| View.MeasureSpec.AT_MOST), |
| View.MeasureSpec.makeMeasureSpec((int) (getHeight() * 1.5), |
| View.MeasureSpec.EXACTLY)); |
| top = gravity == Gravity.TOP ? getHeight() - view.getMeasuredHeight() - |
| 80 : 90; |
| left = 0; |
| vDesiredDist.set(top); |
| break; |
| } |
| |
| view.layout(left, top, left + view.getMeasuredWidth(), |
| top + view.getMeasuredHeight()); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| |
| @Override |
| public boolean canScrollHorizontally() { |
| return super.canScrollHorizontally(); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| vScrollDist.addAndGet(dy); |
| getChildAt(0).offsetTopAndBottom(-dy); |
| return dy; |
| } |
| |
| @Override |
| public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| hScrollDist.addAndGet(dx); |
| getChildAt(0).offsetLeftAndRight(-dx); |
| return dx; |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(10); |
| rv.setAdapter(adapter); |
| rv.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(rv); |
| tlm.waitForLayout(2); |
| View view = rv.getChildAt(0); |
| requestFocus(view); |
| Thread.sleep(1000); |
| assertEquals(vDesiredDist.get(), vScrollDist.get()); |
| assertEquals(hDesiredDist.get(), hScrollDist.get()); |
| assertEquals(mRecyclerView.getPaddingTop(), view.getTop()); |
| if (rtl) { |
| assertEquals(mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(), |
| view.getRight()); |
| } else { |
| assertEquals(mRecyclerView.getPaddingLeft(), view.getLeft()); |
| } |
| } |
| |
| @Test |
| public void testFocusRectOnScreenWithDecorOffsets() throws Throwable { |
| focusRectOnScreenTest(true); |
| } |
| |
| @Test |
| public void testFocusRectOnScreenWithout() throws Throwable { |
| focusRectOnScreenTest(false); |
| } |
| |
| |
| public void focusRectOnScreenTest(boolean addItemDecors) throws Throwable { |
| RecyclerView rv = new RecyclerView(getActivity()); |
| final AtomicInteger scrollDist = new AtomicInteger(0); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| final View view = recycler.getViewForPosition(0); |
| addView(view); |
| measureChildWithMargins(view, 0, 0); |
| view.layout(0, -20, view.getWidth(), |
| -20 + view.getHeight());// ignore decors on purpose |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public boolean canScrollVertically() { |
| return true; |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| scrollDist.addAndGet(dy); |
| return dy; |
| } |
| }; |
| TestAdapter adapter = new TestAdapter(10); |
| if (addItemDecors) { |
| rv.addItemDecoration(new RecyclerView.ItemDecoration() { |
| @Override |
| public void getItemOffsets(Rect outRect, View view, RecyclerView parent, |
| RecyclerView.State state) { |
| outRect.set(0, 10, 0, 10); |
| } |
| }); |
| } |
| rv.setAdapter(adapter); |
| rv.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(rv); |
| tlm.waitForLayout(2); |
| |
| View view = rv.getChildAt(0); |
| requestFocus(view); |
| Thread.sleep(1000); |
| assertEquals(addItemDecors ? -30 : -20, scrollDist.get()); |
| } |
| |
| @Test |
| public void testUnimplementedSmoothScroll() throws Throwable { |
| final AtomicInteger receivedScrollToPosition = new AtomicInteger(-1); |
| final AtomicInteger receivedSmoothScrollToPosition = new AtomicInteger(-1); |
| final CountDownLatch cbLatch = new CountDownLatch(2); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| layoutRange(recycler, 0, 10); |
| layoutLatch.countDown(); |
| } |
| |
| @Override |
| public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, |
| int position) { |
| assertEquals(-1, receivedSmoothScrollToPosition.get()); |
| receivedSmoothScrollToPosition.set(position); |
| RecyclerView.SmoothScroller ss = |
| new LinearSmoothScroller(recyclerView.getContext()) { |
| @Override |
| public PointF computeScrollVectorForPosition(int targetPosition) { |
| return null; |
| } |
| }; |
| ss.setTargetPosition(position); |
| startSmoothScroll(ss); |
| cbLatch.countDown(); |
| } |
| |
| @Override |
| public void scrollToPosition(int position) { |
| assertEquals(-1, receivedScrollToPosition.get()); |
| receivedScrollToPosition.set(position); |
| cbLatch.countDown(); |
| } |
| }; |
| RecyclerView rv = new RecyclerView(getActivity()); |
| rv.setAdapter(new TestAdapter(100)); |
| rv.setLayoutManager(tlm); |
| tlm.expectLayouts(1); |
| setRecyclerView(rv); |
| tlm.waitForLayout(2); |
| freezeLayout(true); |
| smoothScrollToPosition(35); |
| assertEquals("smoothScrollToPosition should be ignored when frozen", |
| -1, receivedSmoothScrollToPosition.get()); |
| freezeLayout(false); |
| smoothScrollToPosition(35); |
| assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS)); |
| checkForMainThreadException(); |
| assertEquals(35, receivedSmoothScrollToPosition.get()); |
| assertEquals(35, receivedScrollToPosition.get()); |
| } |
| |
| @Test |
| public void testJumpingJackSmoothScroller() throws Throwable { |
| jumpingJackSmoothScrollerTest(true); |
| } |
| |
| @Test |
| public void testJumpingJackSmoothScrollerGoesIdle() throws Throwable { |
| jumpingJackSmoothScrollerTest(false); |
| } |
| |
| private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable { |
| final List<Integer> receivedScrollToPositions = new ArrayList<>(); |
| final TestAdapter testAdapter = new TestAdapter(200); |
| final AtomicBoolean mTargetFound = new AtomicBoolean(false); |
| TestLayoutManager tlm = new TestLayoutManager() { |
| int pendingScrollPosition = -1; |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| detachAndScrapAttachedViews(recycler); |
| final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition; |
| layoutRange(recycler, pos, pos + 10); |
| if (layoutLatch != null) { |
| layoutLatch.countDown(); |
| } |
| } |
| |
| @Override |
| public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, |
| final int position) { |
| RecyclerView.SmoothScroller ss = |
| new LinearSmoothScroller(recyclerView.getContext()) { |
| @Override |
| public PointF computeScrollVectorForPosition(int targetPosition) { |
| return new PointF(0, 1); |
| } |
| |
| @Override |
| protected void onTargetFound(View targetView, RecyclerView.State state, |
| Action action) { |
| super.onTargetFound(targetView, state, action); |
| mTargetFound.set(true); |
| } |
| |
| @Override |
| protected void updateActionForInterimTarget(Action action) { |
| int limit = succeed ? getTargetPosition() : 100; |
| if (pendingScrollPosition + 2 < limit) { |
| if (pendingScrollPosition != NO_POSITION) { |
| assertEquals(pendingScrollPosition, |
| getChildViewHolderInt(getChildAt(0)) |
| .getAdapterPosition()); |
| } |
| action.jumpTo(pendingScrollPosition + 2); |
| } |
| } |
| }; |
| ss.setTargetPosition(position); |
| startSmoothScroll(ss); |
| } |
| |
| @Override |
| public void scrollToPosition(int position) { |
| receivedScrollToPositions.add(position); |
| pendingScrollPosition = position; |
| requestLayout(); |
| } |
| }; |
| final RecyclerView rv = new RecyclerView(getActivity()); |
| rv.setAdapter(testAdapter); |
| rv.setLayoutManager(tlm); |
| |
| tlm.expectLayouts(1); |
| setRecyclerView(rv); |
| tlm.waitForLayout(2); |
| |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| rv.smoothScrollToPosition(150); |
| } |
| }); |
| int limit = 100; |
| while (rv.getLayoutManager().isSmoothScrolling() && --limit > 0) { |
| Thread.sleep(200); |
| checkForMainThreadException(); |
| } |
| checkForMainThreadException(); |
| assertTrue(limit > 0); |
| for (int i = 1; i < 100; i+=2) { |
| assertTrue("scroll positions must include " + i, receivedScrollToPositions.contains(i)); |
| } |
| |
| assertEquals(succeed, mTargetFound.get()); |
| |
| } |
| |
| private static class TestViewHolder2 extends RecyclerView.ViewHolder { |
| |
| Object mData; |
| |
| public TestViewHolder2(View itemView) { |
| super(itemView); |
| } |
| } |
| |
| private static class TestAdapter2 extends RecyclerView.Adapter<TestViewHolder2> { |
| |
| List<Item> mItems; |
| |
| private TestAdapter2(int count) { |
| mItems = new ArrayList<Item>(count); |
| for (int i = 0; i < count; i++) { |
| mItems.add(new Item(i, "Item " + i)); |
| } |
| } |
| |
| @Override |
| public TestViewHolder2 onCreateViewHolder(ViewGroup parent, |
| int viewType) { |
| return new TestViewHolder2(new TextView(parent.getContext())); |
| } |
| |
| @Override |
| public void onBindViewHolder(TestViewHolder2 holder, int position) { |
| final Item item = mItems.get(position); |
| ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")"); |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mItems.size(); |
| } |
| } |
| |
| private static interface AdapterRunnable { |
| |
| public void run(TestAdapter adapter) throws Throwable; |
| } |
| |
| } |