| /* |
| * Copyright (C) 2015 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.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| import android.graphics.Rect; |
| import android.support.v4.view.ViewCompat; |
| import android.test.suitebuilder.annotation.MediumTest; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import static android.support.v7.widget.LayoutState.LAYOUT_END; |
| import static android.support.v7.widget.LayoutState.LAYOUT_START; |
| import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; |
| import static android.support.v7.widget.LinearLayoutManager.VERTICAL; |
| |
| import static org.hamcrest.CoreMatchers.hasItem; |
| import static org.hamcrest.CoreMatchers.is; |
| import static org.hamcrest.CoreMatchers.not; |
| import static org.hamcrest.CoreMatchers.sameInstance; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertThat; |
| |
| import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; |
| import static android.view.ViewGroup.LayoutParams.FILL_PARENT; |
| |
| /** |
| * Tests that rely on the basic configuration and does not do any additions / removals |
| */ |
| @RunWith(Parameterized.class) |
| @MediumTest |
| public class LinearLayoutManagerBaseConfigSetTest extends BaseLinearLayoutManagerTest { |
| |
| private final Config mConfig; |
| |
| public LinearLayoutManagerBaseConfigSetTest(Config config) { |
| mConfig = config; |
| } |
| |
| |
| @Parameterized.Parameters(name = "{0}") |
| public static List<Config> configs() throws CloneNotSupportedException { |
| List<Config> result = new ArrayList<>(); |
| for (Config config : createBaseVariations()) { |
| result.add(config); |
| } |
| return result; |
| } |
| |
| @Test |
| public void scrollToPositionWithOffsetTest() throws Throwable { |
| Config config = ((Config) mConfig.clone()).itemCount(300); |
| setupByConfig(config, true); |
| OrientationHelper orientationHelper = OrientationHelper |
| .createOrientationHelper(mLayoutManager, config.mOrientation); |
| Rect layoutBounds = getDecoratedRecyclerViewBounds(); |
| // try scrolling towards head, should not affect anything |
| Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); |
| if (config.mStackFromEnd) { |
| scrollToPositionWithOffset(mTestAdapter.getItemCount() - 1, |
| mLayoutManager.mOrientationHelper.getEnd() - 500); |
| } else { |
| scrollToPositionWithOffset(0, 20); |
| } |
| assertRectSetsEqual(config + " trying to over scroll with offset should be no-op", |
| before, mLayoutManager.collectChildCoordinates()); |
| // try offsetting some visible children |
| int testCount = 10; |
| while (testCount-- > 0) { |
| // get middle child |
| final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2); |
| final int position = mRecyclerView.getChildLayoutPosition(child); |
| final int startOffset = config.mReverseLayout ? |
| orientationHelper.getEndAfterPadding() - orientationHelper |
| .getDecoratedEnd(child) |
| : orientationHelper.getDecoratedStart(child) - orientationHelper |
| .getStartAfterPadding(); |
| final int scrollOffset = config.mStackFromEnd ? startOffset + startOffset / 2 |
| : startOffset / 2; |
| mLayoutManager.expectLayouts(1); |
| scrollToPositionWithOffset(position, scrollOffset); |
| mLayoutManager.waitForLayout(2); |
| final int finalOffset = config.mReverseLayout ? |
| orientationHelper.getEndAfterPadding() - orientationHelper |
| .getDecoratedEnd(child) |
| : orientationHelper.getDecoratedStart(child) - orientationHelper |
| .getStartAfterPadding(); |
| assertEquals(config + " scroll with offset on a visible child should work fine " + |
| " offset:" + finalOffset + " , existing offset:" + startOffset + ", " |
| + "child " + position, |
| scrollOffset, finalOffset); |
| } |
| |
| // try scrolling to invisible children |
| testCount = 10; |
| // we test above and below, one by one |
| int offsetMultiplier = -1; |
| while (testCount-- > 0) { |
| final TargetTuple target = findInvisibleTarget(config); |
| final String logPrefix = config + " " + target; |
| mLayoutManager.expectLayouts(1); |
| final int offset = offsetMultiplier |
| * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3; |
| scrollToPositionWithOffset(target.mPosition, offset); |
| mLayoutManager.waitForLayout(2); |
| final View child = mLayoutManager.findViewByPosition(target.mPosition); |
| assertNotNull(logPrefix + " scrolling to a mPosition with offset " + offset |
| + " should layout it", child); |
| final Rect bounds = mLayoutManager.getViewBounds(child); |
| if (DEBUG) { |
| Log.d(TAG, logPrefix + " post scroll to invisible mPosition " + bounds + " in " |
| + layoutBounds + " with offset " + offset); |
| } |
| |
| if (config.mReverseLayout) { |
| assertEquals(logPrefix + " when scrolling with offset to an invisible in reverse " |
| + "layout, its end should align with recycler view's end - offset", |
| orientationHelper.getEndAfterPadding() - offset, |
| orientationHelper.getDecoratedEnd(child) |
| ); |
| } else { |
| assertEquals( |
| logPrefix + " when scrolling with offset to an invisible child in normal" |
| + " layout its start should align with recycler view's start + " |
| + "offset", |
| orientationHelper.getStartAfterPadding() + offset, |
| orientationHelper.getDecoratedStart(child) |
| ); |
| } |
| offsetMultiplier *= -1; |
| } |
| } |
| |
| @Test |
| public void getFirstLastChildrenTest() throws Throwable { |
| final Config config = ((Config) mConfig.clone()).itemCount(300); |
| setupByConfig(config, true); |
| Runnable viewInBoundsTest = new Runnable() { |
| @Override |
| public void run() { |
| VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren(); |
| final String boundsLog = mLayoutManager.getBoundsLog(); |
| assertEquals(config + ":\nfirst visible child should match traversal result\n" |
| + boundsLog, visibleChildren.firstVisiblePosition, |
| mLayoutManager.findFirstVisibleItemPosition() |
| ); |
| assertEquals( |
| config + ":\nfirst fully visible child should match traversal result\n" |
| + boundsLog, visibleChildren.firstFullyVisiblePosition, |
| mLayoutManager.findFirstCompletelyVisibleItemPosition() |
| ); |
| |
| assertEquals(config + ":\nlast visible child should match traversal result\n" |
| + boundsLog, visibleChildren.lastVisiblePosition, |
| mLayoutManager.findLastVisibleItemPosition() |
| ); |
| assertEquals( |
| config + ":\nlast fully visible child should match traversal result\n" |
| + boundsLog, visibleChildren.lastFullyVisiblePosition, |
| mLayoutManager.findLastCompletelyVisibleItemPosition() |
| ); |
| } |
| }; |
| runTestOnUiThread(viewInBoundsTest); |
| // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching |
| // case |
| final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount(); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.smoothScrollToPosition(scrollPosition); |
| } |
| }); |
| while (mLayoutManager.isSmoothScrolling() || |
| mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { |
| runTestOnUiThread(viewInBoundsTest); |
| Thread.sleep(400); |
| } |
| // delete all items |
| mLayoutManager.expectLayouts(2); |
| mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount()); |
| mLayoutManager.waitForLayout(2); |
| // test empty case |
| runTestOnUiThread(viewInBoundsTest); |
| // set a new adapter with huge items to test full bounds check |
| mLayoutManager.expectLayouts(1); |
| final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace(); |
| final TestAdapter newAdapter = new TestAdapter(100) { |
| @Override |
| public void onBindViewHolder(TestViewHolder holder, |
| int position) { |
| super.onBindViewHolder(holder, position); |
| if (config.mOrientation == HORIZONTAL) { |
| holder.itemView.setMinimumWidth(totalSpace + 5); |
| } else { |
| holder.itemView.setMinimumHeight(totalSpace + 5); |
| } |
| } |
| }; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mRecyclerView.setAdapter(newAdapter); |
| } |
| }); |
| mLayoutManager.waitForLayout(2); |
| runTestOnUiThread(viewInBoundsTest); |
| } |
| |
| @Test |
| public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable { |
| final Config config = ((Config) mConfig.clone()).itemCount(1000); |
| setupByConfig(config, true); |
| mLayoutManager.expectLayouts(1); |
| scrollToPosition(500); |
| mLayoutManager.waitForLayout(2); |
| final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(500); |
| OrientationHelper helper = mLayoutManager.mOrientationHelper; |
| int gap = helper.getDecoratedStart(vh.itemView); |
| scrollBy(gap); |
| gap = helper.getDecoratedStart(vh.itemView); |
| assertThat("test sanity", gap, is(0)); |
| |
| final int size = helper.getDecoratedMeasurement(vh.itemView); |
| AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| if (mConfig.mOrientation == HORIZONTAL) { |
| ViewCompat.setTranslationX(vh.itemView, size * 2); |
| } else { |
| ViewCompat.setTranslationY(vh.itemView, size * 2); |
| } |
| } |
| }); |
| scrollBy(size * 2); |
| assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); |
| assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); |
| assertThat(vh.getAdapterPosition(), is(500)); |
| scrollBy(size * 2); |
| assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); |
| } |
| |
| @Test |
| public void dontRecycleViewsTranslatedOutOfBoundsFromEnd() throws Throwable { |
| final Config config = ((Config) mConfig.clone()).itemCount(1000); |
| setupByConfig(config, true); |
| mLayoutManager.expectLayouts(1); |
| scrollToPosition(500); |
| mLayoutManager.waitForLayout(2); |
| final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(500); |
| OrientationHelper helper = mLayoutManager.mOrientationHelper; |
| int gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView); |
| scrollBy(-gap); |
| gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView); |
| assertThat("test sanity", gap, is(0)); |
| |
| final int size = helper.getDecoratedMeasurement(vh.itemView); |
| AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| if (mConfig.mOrientation == HORIZONTAL) { |
| ViewCompat.setTranslationX(vh.itemView, -size * 2); |
| } else { |
| ViewCompat.setTranslationY(vh.itemView, -size * 2); |
| } |
| } |
| }); |
| scrollBy(-size * 2); |
| assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); |
| assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); |
| assertThat(vh.getAdapterPosition(), is(500)); |
| scrollBy(-size * 2); |
| assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); |
| } |
| |
| private TargetTuple findInvisibleTarget(Config config) { |
| int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE; |
| for (int i = 0; i < mLayoutManager.getChildCount(); i++) { |
| View child = mLayoutManager.getChildAt(i); |
| int position = mRecyclerView.getChildLayoutPosition(child); |
| if (position < minPosition) { |
| minPosition = position; |
| } |
| if (position > maxPosition) { |
| maxPosition = position; |
| } |
| } |
| final int tailTarget = maxPosition + |
| (mRecyclerView.getAdapter().getItemCount() - maxPosition) / 2; |
| final int headTarget = minPosition / 2; |
| final int target; |
| // where will the child come from ? |
| final int itemLayoutDirection; |
| if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) { |
| target = tailTarget; |
| itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END; |
| } else { |
| target = headTarget; |
| itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START; |
| } |
| if (DEBUG) { |
| Log.d(TAG, |
| config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition); |
| } |
| return new TargetTuple(target, itemLayoutDirection); |
| } |
| } |