blob: 9ee7958cef2eab9545e576e814e0bfe89bd86f75 [file] [log] [blame]
/*
* 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);
}
}