[go: nahoru, domu]

blob: 1200b29366653454c6b0e958ff611690c26cb52c [file] [log] [blame]
/*
* Copyright 2018 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 androidx.recyclerview.widget;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION;
import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseIntArray;
import android.util.StateSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.GridView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {
private static final int[] SPAN_SIZES = new int[]{1, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 2};
private final GridLayoutManager.SpanSizeLookup mSpanSizeLookupForSpanIndexTest =
new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return SPAN_SIZES[position];
}
};
@Test
public void focusSearchFailureUp() throws Throwable {
focusSearchFailure(false);
}
@Test
public void focusSearchFailureDown() throws Throwable {
focusSearchFailure(true);
}
@Test
public void scrollToBadOffset() throws Throwable {
scrollToBadOffset(false);
}
@Test
public void scrollToBadOffsetReverse() throws Throwable {
scrollToBadOffset(true);
}
private void scrollToBadOffset(boolean reverseLayout) throws Throwable {
final int w = 500;
final int h = 1000;
RecyclerView recyclerView = setupBasic(new Config(2, 100).reverseLayout(reverseLayout),
new GridTestAdapter(100) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp == null) {
lp = new ViewGroup.LayoutParams(w / 2, h / 2);
holder.itemView.setLayoutParams(lp);
} else {
lp.width = w / 2;
lp.height = h / 2;
holder.itemView.setLayoutParams(lp);
}
}
});
TestedFrameLayout.FullControlLayoutParams lp
= new TestedFrameLayout.FullControlLayoutParams(w, h);
recyclerView.setLayoutParams(lp);
waitForFirstLayout(recyclerView);
mGlm.expectLayout(1);
scrollToPosition(11);
mGlm.waitForLayout(2);
// assert spans and position etc
for (int i = 0; i < mGlm.getChildCount(); i++) {
View child = mGlm.getChildAt(i);
GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) child
.getLayoutParams();
assertThat("span index for child at " + i + " with position " + params
.getViewAdapterPosition(),
params.getSpanIndex(), CoreMatchers.is(params.getViewAdapterPosition() % 2));
}
// assert spans and positions etc.
int lastVisible = mGlm.findLastVisibleItemPosition();
// this should be the scrolled child
assertThat(lastVisible, CoreMatchers.is(11));
}
private void focusSearchFailure(boolean scrollDown) throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown)
, new GridTestAdapter(31, 1) {
RecyclerView mAttachedRv;
@Override
@SuppressWarnings("deprecation") // used for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
testViewHolder.itemView.setFocusable(true);
testViewHolder.itemView.setFocusableInTouchMode(true);
// Good to have colors for debugging
StateListDrawable stl = new StateListDrawable();
stl.addState(new int[]{android.R.attr.state_focused},
new ColorDrawable(Color.RED));
stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
testViewHolder.itemView.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mAttachedRv = recyclerView;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
}
});
waitForFirstLayout(recyclerView);
View viewToFocus = recyclerView.findViewHolderForAdapterPosition(1).itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, recyclerView.getFocusedChild());
int pos = 1;
View focusedView = viewToFocus;
while (pos < 31) {
focusSearch(focusedView, scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP);
waitForIdleScroll(recyclerView);
focusedView = recyclerView.getFocusedChild();
assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1),
recyclerView
.getChildViewHolder(focusedView).getAbsoluteAdapterPosition());
pos += 3;
}
}
/**
* Tests that the GridLayoutManager retains the focused element after multiple measure
* calls to the RecyclerView. There was a bug where the focused view was lost when the soft
* keyboard opened. This test simulates the measure/layout events triggered by the opening
* of the soft keyboard by making two calls to measure. A simulation was done because using
* the soft keyboard in the test caused many issues on API levels 15, 17 and 19.
*/
@Test
public void focusedChildStaysInViewWhenRecyclerViewShrinks() throws Throwable {
// Arrange.
final int spanCount = 3;
// For simplicity and test stability, item count should be a multiple of spanCount.
final int itemCount = 33 * spanCount;
final RecyclerView recyclerView = inflateWrappedRV();
ViewGroup.LayoutParams lp = recyclerView.getLayoutParams();
lp.height = WRAP_CONTENT;
lp.width = MATCH_PARENT;
Config config = new Config(spanCount, itemCount);
mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
config.mReverseLayout);
recyclerView.setLayoutManager(mGlm);
GridFocusableAdapter gridFocusableAdapter = new GridFocusableAdapter(itemCount);
gridFocusableAdapter.assignSpanSizeLookup(mGlm);
recyclerView.setAdapter(gridFocusableAdapter);
mGlm.expectLayout(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
getActivity().getContainer().addView(recyclerView);
}
});
mGlm.waitForLayout(3);
int width = recyclerView.getWidth();
int height = recyclerView.getHeight();
final int widthMeasureSpec =
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
final int fullHeightMeasureSpec =
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
// "MinusOne" so that a measure call will appropriately trigger onMeasure after RecyclerView
// was previously laid out with the full height version.
final int fullHeightMinusOneMeasureSpec =
View.MeasureSpec.makeMeasureSpec(height - 1, View.MeasureSpec.AT_MOST);
final int halfHeightMeasureSpec =
View.MeasureSpec.makeMeasureSpec(height / 2, View.MeasureSpec.AT_MOST);
// Act 1.
// First focus on the last fully visible child located at span index #1.
View toFocus = findLastFullyVisibleChild(recyclerView);
int focusIndex = recyclerView.getChildAdapterPosition(toFocus);
focusIndex = (focusIndex / spanCount) * spanCount + 1;
toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;
assertTrue(focusIndex >= 1 && focusIndex < itemCount);
requestFocus(toFocus, false);
mGlm.expectLayout(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
recyclerView.layout(
0,
0,
recyclerView.getMeasuredWidth(),
recyclerView.getMeasuredHeight());
}
});
mGlm.waitForLayout(3);
// Assert 1.
assertThat("Child at position " + focusIndex + " should be focused",
toFocus.hasFocus(), is(true));
assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
isViewPartiallyInBound(recyclerView, toFocus));
// Act 2.
mGlm.expectLayout(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
recyclerView.measure(widthMeasureSpec, fullHeightMeasureSpec);
recyclerView.layout(
0,
0,
recyclerView.getMeasuredWidth(),
recyclerView.getMeasuredHeight());
}
});
mGlm.waitForLayout(3);
// Assert 2.
assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
isViewPartiallyInBound(recyclerView, toFocus));
// Act 3.
// Now focus on the first fully visible EditText located at the last span index.
toFocus = findFirstFullyVisibleChild(recyclerView);
focusIndex = recyclerView.getChildAdapterPosition(toFocus);
focusIndex = (focusIndex / spanCount) * spanCount + (spanCount - 1);
toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;
requestFocus(toFocus, false);
mGlm.expectLayout(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
recyclerView.layout(
0,
0,
recyclerView.getMeasuredWidth(),
recyclerView.getMeasuredHeight());
}
});
mGlm.waitForLayout(3);
// Assert 3.
assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
isViewPartiallyInBound(recyclerView, toFocus));
}
@Test
public void topUnfocusableViewsVisibility() throws Throwable {
// The maximum number of rows that can be fully in-bounds of RV.
final int visibleRowCount = 5;
final int spanCount = 3;
final int consecutiveFocusableRowsCount = 4;
final int consecutiveUnFocusableRowsCount = 8;
final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
* spanCount;
final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
.reverseLayout(true),
new GridTestAdapter(itemCount, 1) {
RecyclerView mAttachedRv;
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
// Good to have colors for debugging
StateListDrawable stl = new StateListDrawable();
stl.addState(new int[]{android.R.attr.state_focused},
new ColorDrawable(Color.RED));
stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
testViewHolder.itemView.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mAttachedRv = recyclerView;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (position < spanCount * consecutiveFocusableRowsCount) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
}
});
waitForFirstLayout(recyclerView);
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, recyclerView.getFocusedChild());
// adapter position of the item (whether focusable or not) that just becomes fully
// visible after focusSearch.
int visibleIndex = focusIndex;
// The VH of the above adapter position
RecyclerView.ViewHolder toVisible = null;
int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
* spanCount + visibleIndex;
// Navigate up through the focusable and unfocusable rows. The focusable rows should
// become focused one by one until hitting the last focusable row, at which point,
// unfocusable rows should become visible on the screen until the currently focused row
// stays on the screen.
int pos = focusIndex + spanCount;
while (pos < itemCount) {
focusSearch(recyclerView.getFocusedChild(), View.FOCUS_UP, true);
waitForIdleScroll(recyclerView);
focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
assertThat("Child at position " + focusIndex + " should be focused",
toFocus.itemView.hasFocus(), is(true));
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(recyclerView, toFocus.itemView));
assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
isViewFullyInBound(recyclerView, toVisible.itemView));
pos += spanCount;
}
}
@Test
public void bottomUnfocusableViewsVisibility() throws Throwable {
// The maximum number of rows that can be fully in-bounds of RV.
final int visibleRowCount = 5;
final int spanCount = 3;
final int consecutiveFocusableRowsCount = 4;
final int consecutiveUnFocusableRowsCount = 8;
final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
* spanCount;
final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
.reverseLayout(false),
new GridTestAdapter(itemCount, 1) {
RecyclerView mAttachedRv;
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
// Good to have colors for debugging
StateListDrawable stl = new StateListDrawable();
stl.addState(new int[]{android.R.attr.state_focused},
new ColorDrawable(Color.RED));
stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
testViewHolder.itemView.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mAttachedRv = recyclerView;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (position < spanCount * consecutiveFocusableRowsCount) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
}
});
waitForFirstLayout(recyclerView);
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, recyclerView.getFocusedChild());
// adapter position of the item (whether focusable or not) that just becomes fully
// visible after focusSearch.
int visibleIndex = focusIndex;
// The VH of the above adapter position
RecyclerView.ViewHolder toVisible = null;
int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
* spanCount + visibleIndex;
// Navigate down through the focusable and unfocusable rows. The focusable rows should
// become focused one by one until hitting the last focusable row, at which point,
// unfocusable rows should become visible on the screen until the currently focused row
// stays on the screen.
int pos = focusIndex + spanCount;
while (pos < itemCount) {
focusSearch(recyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
waitForIdleScroll(recyclerView);
focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
assertThat("Child at position " + focusIndex + " should be focused",
toFocus.itemView.hasFocus(), is(true));
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(recyclerView, toFocus.itemView));
assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
isViewFullyInBound(recyclerView, toVisible.itemView));
pos += spanCount;
}
}
@Test
public void leftUnfocusableViewsVisibility() throws Throwable {
// The maximum number of columns that can be fully in-bounds of RV.
final int visibleColCount = 5;
final int spanCount = 3;
final int consecutiveFocusableColsCount = 4;
final int consecutiveUnFocusableColsCount = 8;
final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
* spanCount;
final int childWidth = 200;
final int childHeight = WRAP_CONTENT;
// Parent width is 1 more than 4 times child width, so when focusable child is 1 pixel on
// screen 4 non-focusable children can fit on screen.
final int parentWidth = childWidth * 4 + 1;
final int parentHeight = 1000;
final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
.orientation(HORIZONTAL).reverseLayout(true),
new GridTestAdapter(itemCount, 1) {
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
// Good to have colors for debugging
StateListDrawable stl = new StateListDrawable();
stl.addState(new int[]{android.R.attr.state_focused},
new ColorDrawable(Color.RED));
stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
testViewHolder.itemView.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (position < spanCount * consecutiveFocusableColsCount) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
holder.itemView.setLayoutParams(
new RecyclerView.LayoutParams(childWidth, childHeight));
}
});
recyclerView.setLayoutParams(new ViewGroup.LayoutParams(parentWidth, parentHeight));
waitForFirstLayout(recyclerView);
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, recyclerView.getFocusedChild());
// adapter position of the item (whether focusable or not) that just becomes fully
// visible after focusSearch.
int visibleIndex = focusIndex;
// The VH of the above adapter position
RecyclerView.ViewHolder toVisible = null;
int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
* spanCount + visibleIndex;
// Navigate left through the focusable and unfocusable columns. The focusable columns should
// become focused one by one until hitting the last focusable column, at which point,
// unfocusable columns should become visible on the screen until the currently focused
// column stays on the screen.
int pos = focusIndex + spanCount;
while (pos < itemCount) {
focusSearch(recyclerView.getFocusedChild(), View.FOCUS_LEFT, true);
waitForIdleScroll(recyclerView);
focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
assertThat("Child at position " + focusIndex + " should be focused",
toFocus.itemView.hasFocus(), is(true));
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(recyclerView, toFocus.itemView));
assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
isViewFullyInBound(recyclerView, toVisible.itemView));
pos += spanCount;
}
}
@Test
public void rightUnfocusableViewsVisibility() throws Throwable {
// The maximum number of columns that can be fully in-bounds of RV.
final int visibleColCount = 5;
final int spanCount = 3;
final int consecutiveFocusableColsCount = 4;
final int consecutiveUnFocusableColsCount = 8;
final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
* spanCount;
final int childWidth = 200;
final int childHeight = WRAP_CONTENT;
// Parent width is 1 more than 4 times child width, so when focusable child is 1 pixel on
// screen 4 non-focusable children can fit on screen.
final int parentWidth = childWidth * 4 + 1;
final int parentHeight = 1000;
final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
.orientation(HORIZONTAL).reverseLayout(false),
new GridTestAdapter(itemCount, 1) {
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
// Good to have colors for debugging
StateListDrawable stl = new StateListDrawable();
stl.addState(new int[]{android.R.attr.state_focused},
new ColorDrawable(Color.RED));
stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
testViewHolder.itemView.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (position < spanCount * consecutiveFocusableColsCount) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
holder.itemView.setLayoutParams(
new RecyclerView.LayoutParams(childWidth, childHeight));
}
});
recyclerView.setLayoutParams(new ViewGroup.LayoutParams(parentWidth, parentHeight));
waitForFirstLayout(recyclerView);
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, recyclerView.getFocusedChild());
// adapter position of the item (whether focusable or not) that just becomes fully
// visible after focusSearch.
int visibleIndex = focusIndex;
// The VH of the above adapter position
RecyclerView.ViewHolder toVisible = null;
int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
* spanCount + visibleIndex;
// Navigate right through the focusable and unfocusable columns. The focusable columns
// should become focused one by one until hitting the last focusable column, at which point,
// unfocusable columns should become visible on the screen until the currently focused
// column stays on the screen.
int pos = focusIndex + spanCount;
while (pos < itemCount) {
focusSearch(recyclerView.getFocusedChild(), View.FOCUS_RIGHT, true);
waitForIdleScroll(recyclerView);
focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
assertThat("Child at position " + focusIndex + " should be focused",
toFocus.itemView.hasFocus(), is(true));
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(recyclerView, toFocus.itemView));
assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
isViewFullyInBound(recyclerView, toVisible.itemView));
pos += spanCount;
}
}
@UiThreadTest
@Test
public void scrollWithoutLayout() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
mGlm.expectLayout(1);
setRecyclerView(recyclerView);
mGlm.setSpanCount(5);
recyclerView.scrollBy(0, 10);
}
@Test
public void scrollWithoutLayoutAfterInvalidate() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
waitForFirstLayout(recyclerView);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mGlm.setSpanCount(5);
recyclerView.scrollBy(0, 10);
}
});
}
@Test
public void predictiveSpanLookup1() throws Throwable {
predictiveSpanLookupTest(0, false);
}
@Test
public void predictiveSpanLookup2() throws Throwable {
predictiveSpanLookupTest(0, true);
}
@Test
public void predictiveSpanLookup3() throws Throwable {
predictiveSpanLookupTest(1, false);
}
@Test
public void predictiveSpanLookup4() throws Throwable {
predictiveSpanLookupTest(1, true);
}
public void predictiveSpanLookupTest(int remaining, boolean removeFromStart) throws Throwable {
RecyclerView recyclerView = setupBasic(new Config(3, 10));
mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position < 0 || position >= mAdapter.getItemCount()) {
postExceptionToInstrumentation(new AssertionError("position is not within " +
"adapter range. pos:" + position + ", adapter size:" +
mAdapter.getItemCount()));
}
return 1;
}
@Override
public int getSpanIndex(int position, int spanCount) {
if (position < 0 || position >= mAdapter.getItemCount()) {
postExceptionToInstrumentation(new AssertionError("position is not within " +
"adapter range. pos:" + position + ", adapter size:" +
mAdapter.getItemCount()));
}
return super.getSpanIndex(position, spanCount);
}
});
waitForFirstLayout(recyclerView);
checkForMainThreadException();
assertTrue("Assumption check", mGlm.supportsPredictiveItemAnimations());
mGlm.expectLayout(2);
int deleteCnt = 10 - remaining;
int deleteStart = removeFromStart ? 0 : remaining;
mAdapter.deleteAndNotify(deleteStart, deleteCnt);
mGlm.waitForLayout(2);
checkForMainThreadException();
}
@Test
public void movingAGroupOffScreenForAddedItems() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
final int[] maxId = new int[1];
maxId[0] = -1;
final SparseIntArray spanLookups = new SparseIntArray();
final AtomicBoolean enableSpanLookupLogging = new AtomicBoolean(false);
mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (maxId[0] > 0 && mAdapter.getItemAt(position).mId > maxId[0]) {
return 1;
} else if (enableSpanLookupLogging.get() && !rv.mState.isPreLayout()) {
spanLookups.put(position, spanLookups.get(position, 0) + 1);
}
return 3;
}
});
((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(true);
waitForFirstLayout(rv);
View lastView = rv.getChildAt(rv.getChildCount() - 1);
final int lastPos = rv.getChildAdapterPosition(lastView);
maxId[0] = mAdapter.getItemAt(mAdapter.getItemCount() - 1).mId;
// now add a lot of items below this and those new views should have span size 3
enableSpanLookupLogging.set(true);
mGlm.expectLayout(2);
mAdapter.addAndNotify(lastPos - 2, 30);
mGlm.waitForLayout(2);
checkForMainThreadException();
assertEquals("last items span count should be queried twice", 2,
spanLookups.get(lastPos + 30));
}
@Test
public void layoutParams() throws Throwable {
layoutParamsTest(GridLayoutManager.HORIZONTAL);
removeRecyclerView();
layoutParamsTest(GridLayoutManager.VERTICAL);
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void horizontalAccessibilitySpanIndices() throws Throwable {
accessibilitySpanIndicesTest(HORIZONTAL);
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void verticalAccessibilitySpanIndices() throws Throwable {
accessibilitySpanIndicesTest(VERTICAL);
}
public void accessibilitySpanIndicesTest(int orientation) throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false));
waitForFirstLayout(recyclerView);
final AccessibilityDelegateCompat delegateCompat = mRecyclerView
.getCompatAccessibilityDelegate().getItemDelegate();
final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2);
final int position = recyclerView.getChildLayoutPosition(chosen);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info);
}
});
GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info
.getCollectionItemInfo();
assertNotNull(itemInfo);
assertEquals("result should have span group position",
ssl.getSpanGroupIndex(position, mGlm.getSpanCount()),
orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex());
assertEquals("result should have span index",
ssl.getSpanIndex(position, mGlm.getSpanCount()),
orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex());
assertEquals("result should have span size",
ssl.getSpanSize(position),
orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan());
}
@Test
public void rowCountForAccessibility_verticalOrientation() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
waitForFirstLayout(recyclerView);
int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(34, count);
}
@Test
public void rowCountForAccessibility_horizontalOrientation() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
mGlm.setOrientation(RecyclerView.HORIZONTAL);
waitForFirstLayout(recyclerView);
int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(3, count);
}
@Test
public void rowCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 2));
waitForFirstLayout(recyclerView);
int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(1, count);
}
@Test
public void rowCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 2));
mGlm.setOrientation(RecyclerView.HORIZONTAL);
waitForFirstLayout(recyclerView);
int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(2, count);
}
@Test
public void columnCountForAccessibility_verticalOrientation() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
waitForFirstLayout(recyclerView);
int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(3, count);
}
@Test
public void columnCountForAccessibility_horizontalOrientation() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
mGlm.setOrientation(RecyclerView.HORIZONTAL);
waitForFirstLayout(recyclerView);
int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(34, count);
}
@Test
public void columnCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 2));
waitForFirstLayout(recyclerView);
int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(2, count);
}
@Test
public void columnCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 2));
mGlm.setOrientation(RecyclerView.HORIZONTAL);
waitForFirstLayout(recyclerView);
int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
recyclerView.mState);
assertEquals(1, count);
}
@Test
public void accessibilityClassName() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
waitForFirstLayout(recyclerView);
final AccessibilityDelegateCompat delegateCompat = mRecyclerView
.getCompatAccessibilityDelegate();
final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
mActivityRule.runOnUiThread(
() -> delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info));
assertEquals(GridView.class.getName(), info.getClassName());
}
@Test
public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 0));
waitForFirstLayout(recyclerView);
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
assertFalse(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mGlm.onInitializeAccessibilityNodeInfo(nodeInfo);
}
});
assertFalse(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_addedWithNonEmptyList()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 1));
waitForFirstLayout(recyclerView);
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
assertFalse(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mGlm.onInitializeAccessibilityNodeInfo(nodeInfo);
}
});
assertTrue(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
}
@Test
public void performAccessibilityAction_actionScrollToPosition_withNullArgs_returnsFalse()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
waitForFirstLayout(recyclerView);
final boolean[] returnValue = {false};
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
returnValue[0] = mGlm.performAccessibilityAction(
android.R.id.accessibilityActionScrollToPosition, null);
}
});
assertFalse(returnValue[0]);
}
@Test
public void performAccessibilityAction_actionScrollToPosition_noRow_returnsFalse()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
waitForFirstLayout(recyclerView);
Bundle bundle = new Bundle();
bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 10);
final boolean[] returnValue = {false};
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
returnValue[0] = mGlm.performAccessibilityAction(
android.R.id.accessibilityActionScrollToPosition, bundle);
}
});
assertFalse(returnValue[0]);
}
@Test
public void performAccessibilityAction_actionScrollToPosition_noColumn_returnsFalse()
throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
waitForFirstLayout(recyclerView);
Bundle bundle = new Bundle();
bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
final boolean[] returnValue = {false};
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
returnValue[0] = mGlm.performAccessibilityAction(
android.R.id.accessibilityActionScrollToPosition, bundle);
}
});
assertFalse(returnValue[0]);
}
@Test
public void performAccessibilityAction_withValidRowAndColumn_performsScroll() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
final GridLayoutManager.SpanSizeLookup spanSizeLookup =
new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position % 5 == 0) {
return 2;
}
return 1;
}
};
mGlm.setOrientation(RecyclerView.HORIZONTAL);
mGlm.setSpanSizeLookup(spanSizeLookup);
/*
This generates the following grid, with items 1, 6, 11, etc. (at indices 0, 5, 10, etc.)
spanning two rows.
1 3 6 8 11 13 16 etc.
4 9 14 etc.
2 5 7 10 12 15 17 etc.
*/
waitForFirstLayout(recyclerView);
mGlm.expectLayout(1);
Bundle bundle = new Bundle();
bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 0);
bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 2);
final boolean[] returnValue = {false};
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
returnValue[0] = recyclerView.getLayoutManager().performAccessibilityAction(
android.R.id.accessibilityActionScrollToPosition, bundle);
}
});
mGlm.waitForLayout(2);
assertTrue(returnValue[0]);
assertEquals(((TextView) mGlm.getChildAt(0)).getText(), "Item (6)");
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
public void onInitializeAccessibilityNodeInfo_addActionScrollInDirection_notAddedWithEmptyList()
throws Throwable {
mRecyclerView = setupBasic(new Config(2, 0));
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
assertThat(nodeInfo.getActionList()).doesNotContain(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION);
mActivityRule.runOnUiThread(
() -> mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo));
assertThat(nodeInfo.getActionList()).doesNotContain(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION);
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
public void onInitializeAccessibilityNodeInfo_addActionScrollInDirection_withOneItemList()
throws Throwable {
mRecyclerView = setupBasic(new Config(2, 1));
final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
assertThat(node.getActionList()).doesNotContain(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION);
mActivityRule.runOnUiThread(
() -> mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(node));
assertThat(node.getActionList()).doesNotContain(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION);
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
public void onInitializeAccessibilityNodeInfo_addActionScrollInDirection_withMoreThanOneItem()
throws Throwable {
mRecyclerView = setupBasic(new Config(2, 2));
final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
assertThat(node.getActionList()).doesNotContain(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION);
mActivityRule.runOnUiThread(
() -> mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(node));
assertThat(node.getActionList()).contains(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION);
}
public GridLayoutManager.LayoutParams ensureGridLp(View view) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
GridLayoutManager.LayoutParams glp;
if (lp instanceof GridLayoutManager.LayoutParams) {
glp = (GridLayoutManager.LayoutParams) lp;
} else if (lp == null) {
glp = (GridLayoutManager.LayoutParams) mGlm
.generateDefaultLayoutParams();
view.setLayoutParams(glp);
} else {
glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp);
view.setLayoutParams(glp);
}
return glp;
}
public void layoutParamsTest(final int orientation) throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation),
new GridTestAdapter(100) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
int val = 0;
switch (position % 5) {
case 0:
val = 10;
break;
case 1:
val = 30;
break;
case 2:
val = GridLayoutManager.LayoutParams.WRAP_CONTENT;
break;
case 3:
val = GridLayoutManager.LayoutParams.MATCH_PARENT;
break;
case 4:
val = 200;
break;
}
if (orientation == GridLayoutManager.VERTICAL) {
glp.height = val;
} else {
glp.width = val;
}
holder.itemView.setLayoutParams(glp);
}
});
waitForFirstLayout(rv);
final OrientationHelper helper = mGlm.mOrientationHelper;
final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2)));
assertEquals(firstRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(0)));
assertEquals(firstRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(1)));
assertEquals(firstRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(2)));
assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0)));
assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1)));
assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2)));
final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3)));
assertEquals(secondRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(3)));
assertEquals(secondRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(4)));
assertEquals(secondRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(5)));
assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3)));
assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4)));
assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
}
@Test
public void anchorUpdate() throws InterruptedException {
GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
final GridLayoutManager.SpanSizeLookup spanSizeLookup
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 200) {
return 100;
}
if (position > 20) {
return 2;
}
return 1;
}
};
glm.setSpanSizeLookup(spanSizeLookup);
glm.mAnchorInfo.mPosition = 11;
RecyclerView.State state = new RecyclerView.State();
mRecyclerView = new RecyclerView(getActivity());
state.mItemCount = 1000;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition);
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
assertEquals("gm should keep anchor in last span in the row", 20,
glm.mAnchorInfo.mPosition);
glm.mAnchorInfo.mPosition = 5;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
assertEquals("gm should keep anchor in last span in the row", 10,
glm.mAnchorInfo.mPosition);
glm.mAnchorInfo.mPosition = 13;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition);
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
assertEquals("gm should keep anchor in last span in the row", 20,
glm.mAnchorInfo.mPosition);
glm.mAnchorInfo.mPosition = 23;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition);
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
assertEquals("gm should keep anchor in last span in the row", 25,
glm.mAnchorInfo.mPosition);
glm.mAnchorInfo.mPosition = 35;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition);
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
assertEquals("gm should keep anchor in last span in the row", 35,
glm.mAnchorInfo.mPosition);
}
@Test
public void spanLookup() {
spanLookupTest(false);
}
@Test
public void spanLookupWithCache() {
spanLookupTest(true);
}
@Test
public void spanLookupCache() {
final GridLayoutManager.SpanSizeLookup ssl
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 6) {
return 2;
}
return 1;
}
};
ssl.setSpanIndexCacheEnabled(true);
assertEquals("reference child non existent", -1,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
2));
ssl.getCachedSpanIndex(4, 5);
assertEquals("reference child non existent", -1,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
3));
// this should not happen and if happens, it is better to return -1
assertEquals("reference child itself", -1,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
4));
assertEquals("reference child before", 4,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
5));
assertEquals("reference child before", 4,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
100));
ssl.getCachedSpanIndex(6, 5);
assertEquals("reference child before", 6,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
7));
assertEquals("reference child before", 4,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
6));
assertEquals("reference child itself", -1,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
4));
ssl.getCachedSpanIndex(12, 5);
assertEquals("reference child before", 12,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
13));
assertEquals("reference child before", 6,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
12));
assertEquals("reference child before", 6,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
7));
for (int i = 0; i < 6; i++) {
ssl.getCachedSpanIndex(i, 5);
}
for (int i = 1; i < 7; i++) {
assertEquals("reference child right before " + i, i - 1,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
i));
}
assertEquals("reference child before 0 ", -1,
GridLayoutManager.SpanSizeLookup.findFirstKeyLessThan(ssl.mSpanIndexCache,
0));
}
public void spanLookupTest(boolean enableCache) {
final GridLayoutManager.SpanSizeLookup ssl
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 200) {
return 100;
}
if (position > 6) {
return 2;
}
return 1;
}
};
ssl.setSpanIndexCacheEnabled(enableCache);
assertEquals(0, ssl.getCachedSpanIndex(0, 5));
assertEquals(4, ssl.getCachedSpanIndex(4, 5));
assertEquals(0, ssl.getCachedSpanIndex(5, 5));
assertEquals(1, ssl.getCachedSpanIndex(6, 5));
assertEquals(2, ssl.getCachedSpanIndex(7, 5));
assertEquals(2, ssl.getCachedSpanIndex(9, 5));
assertEquals(0, ssl.getCachedSpanIndex(8, 5));
}
@Test
public void removeAnchorItem() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0);
}
@Test
public void removeAnchorItemReverse() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100,
0);
}
@Test
public void removeAnchorItemHorizontal() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(HORIZONTAL).reverseLayout(
false), 100, 0);
}
@Test
public void removeAnchorItemReverseHorizontal() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(HORIZONTAL).reverseLayout(true),
100, 0);
}
/**
* This tests a regression where predictive animations were not working as expected when the
* first item is removed and there aren't any more items to add from that direction.
* First item refers to the default anchor item.
*/
public void removeAnchorItemTest(final Config config, int adapterSize,
final int removePos) throws Throwable {
GridTestAdapter adapter = new GridTestAdapter(adapterSize) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
lp = new ViewGroup.MarginLayoutParams(0, 0);
holder.itemView.setLayoutParams(lp);
}
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
final int maxSize;
if (config.mOrientation == HORIZONTAL) {
maxSize = mRecyclerView.getWidth();
mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
} else {
maxSize = mRecyclerView.getHeight();
mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
}
final int desiredSize;
if (position == removePos) {
// make it large
desiredSize = maxSize / 4;
} else {
// make it small
desiredSize = maxSize / 8;
}
if (config.mOrientation == HORIZONTAL) {
mlp.width = desiredSize;
} else {
mlp.height = desiredSize;
}
}
};
RecyclerView recyclerView = setupBasic(config, adapter);
waitForFirstLayout(recyclerView);
final int childCount = mGlm.getChildCount();
RecyclerView.ViewHolder toBeRemoved = null;
List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
for (int i = 0; i < childCount; i++) {
View child = mGlm.getChildAt(i);
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
if (holder.getAbsoluteAdapterPosition() == removePos) {
toBeRemoved = holder;
} else {
toBeMoved.add(holder);
}
}
assertNotNull("Assumption check", toBeRemoved);
assertEquals("Assumption check", childCount - 1, toBeMoved.size());
LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
mRecyclerView.setItemAnimator(loggingItemAnimator);
loggingItemAnimator.reset();
loggingItemAnimator.expectRunPendingAnimationsCall(1);
mGlm.expectLayout(2);
adapter.deleteAndNotify(removePos, 1);
mGlm.waitForLayout(1);
loggingItemAnimator.waitForPendingAnimationsCall(2);
assertTrue("removed child should receive remove animation",
loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
for (RecyclerView.ViewHolder vh : toBeMoved) {
assertTrue("view holder should be in moved list",
loggingItemAnimator.mMoveVHs.contains(vh));
}
List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
for (int i = 0; i < mGlm.getChildCount(); i++) {
View child = mGlm.getChildAt(i);
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
newHolders.add(holder);
}
}
assertTrue("some new children should show up for the new space", newHolders.size() > 0);
assertEquals("no items should receive animate add since they are not new", 0,
loggingItemAnimator.mAddVHs.size());
for (RecyclerView.ViewHolder holder : newHolders) {
assertTrue("new holder should receive a move animation",
loggingItemAnimator.mMoveVHs.contains(holder));
}
// for removed view, 3 for new row
assertTrue("control against adding too many children due to bad layout state preparation."
+ " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
mRecyclerView.getChildCount() <= childCount + 1 + 3);
}
@Test
public void spanGroupIndex() {
final GridLayoutManager.SpanSizeLookup ssl
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 200) {
return 100;
}
if (position > 6) {
return 2;
}
return 1;
}
};
assertEquals(0, ssl.getSpanGroupIndex(0, 5));
assertEquals(0, ssl.getSpanGroupIndex(4, 5));
assertEquals(1, ssl.getSpanGroupIndex(5, 5));
assertEquals(1, ssl.getSpanGroupIndex(6, 5));
assertEquals(1, ssl.getSpanGroupIndex(7, 5));
assertEquals(2, ssl.getSpanGroupIndex(9, 5));
assertEquals(2, ssl.getSpanGroupIndex(8, 5));
}
@Test
public void notifyDataSetChange() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup();
ssl.setSpanIndexCacheEnabled(true);
waitForFirstLayout(recyclerView);
assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0);
final Callback callback = new Callback() {
@Override
public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (!state.isPreLayout()) {
assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size());
}
}
@Override
public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (!state.isPreLayout()) {
assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0);
}
}
};
mGlm.mCallbacks.add(callback);
mGlm.expectLayout(2);
mAdapter.deleteAndNotify(2, 3);
mGlm.waitForLayout(2);
checkForMainThreadException();
}
@Test
public void unevenHeights() throws Throwable {
final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
new HashMap<Integer, RecyclerView.ViewHolder>();
RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
glp.height = 50 + position * 50;
viewHolderMap.put(position, holder);
}
});
waitForFirstLayout(recyclerView);
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should get max height", 150,
vh.itemView.getHeight());
}
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should have measured the max height", 150,
vh.itemView.getMeasuredHeight());
}
}
@Test
public void unevenWidths() throws Throwable {
final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
new HashMap<Integer, RecyclerView.ViewHolder>();
RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false),
new GridTestAdapter(3) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
glp.width = 50 + position * 50;
viewHolderMap.put(position, holder);
}
});
waitForFirstLayout(recyclerView);
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should get max width", 150,
vh.itemView.getWidth());
}
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should have measured the max width", 150,
vh.itemView.getMeasuredWidth());
}
}
@Test
public void spanSizeChange() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
waitForFirstLayout(rv);
assertTrue(mGlm.supportsPredictiveItemAnimations());
mGlm.expectLayout(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mGlm.setSpanCount(5);
assertFalse(mGlm.supportsPredictiveItemAnimations());
}
});
mGlm.waitForLayout(2);
mGlm.expectLayout(2);
mAdapter.deleteAndNotify(3, 2);
mGlm.waitForLayout(2);
assertTrue(mGlm.supportsPredictiveItemAnimations());
}
@Test
public void cacheSpanIndices() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
waitForFirstLayout(rv);
GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0);
assertEquals("item index 5 should be in span 2", 2,
getLp(mGlm.findViewByPosition(5)).getSpanIndex());
mGlm.expectLayout(2);
mAdapter.mFullSpanItems.add(4);
mAdapter.changeAndNotify(4, 1);
mGlm.waitForLayout(2);
assertEquals("item index 5 should be in span 2", 0,
getLp(mGlm.findViewByPosition(5)).getSpanIndex());
}
@Test
public void computeVerticalScrollRange_spansUsedAndGroupIndexesCached_rangeIsConstant()
throws Throwable {
int nItems = 100;
final RecyclerView rv = setupBasic(new Config(2, nItems));
mGlm.setUsingSpansToEstimateScrollbarDimensions(true);
mGlm.mSpanSizeLookup.setSpanGroupIndexCacheEnabled(true);
int[] fullSpanItems = new int[nItems / 2];
for (int i = 0; i < fullSpanItems.length; i++) {
fullSpanItems[i] = i;
}
mAdapter.setFullSpan(fullSpanItems);
waitForFirstLayout(rv);
int constantRange = mGlm.computeVerticalScrollRange(rv.mState);
assertEquals(0, mGlm.computeVerticalScrollOffset(rv.mState));
scrollToPosition(nItems - 1);
mGlm.waitForLayout(2);
int maxOffset = mGlm.computeVerticalScrollOffset(rv.mState);
assertEquals(mGlm.computeVerticalScrollRange(rv.mState), constantRange);
assertEquals(maxOffset + mGlm.computeVerticalScrollExtent(rv.mState), constantRange);
}
@Test // reproduces b/179181037
public void spanCacheWithAnimations() throws Throwable {
GridTestAdapter adapter = new GridTestAdapter(8, 1);
adapter.setItemLayoutParams(
new RecyclerView.LayoutParams(250, 200)
);
final RecyclerView rv = setupBasic(new Config(2, 8), adapter);
rv.setLayoutParams(new ViewGroup.LayoutParams(500, 500));
mAdapter.setFullSpan(0);
waitForFirstLayout(rv);
assertThat(getPositionToSpanIndexMapping()).containsExactly(
0, 0,
1, 0,
2, 1,
3, 0,
4, 1
);
// trigger laying out other items and scroll back to 0 to move them to the recycler cache
smoothScrollToPosition(7);
smoothScrollToPosition(0);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.notifyItemRemoved(0);
mAdapter.notifyItemInserted(0);
}
});
waitForAnimations(10);
assertThat(getPositionToSpanIndexMapping()).containsExactly(
0, 0,
1, 0,
2, 1,
3, 0,
4, 1
);
smoothScrollToPosition(7);
// expected layout
// ---- visible below
// 3 4
// 5 6
// 7
assertThat(getPositionToSpanIndexMapping()).containsExactly(
3, 0,
4, 1,
5, 0,
6, 1,
7, 0
);
}
@SuppressWarnings("ConstantConditions")
@NonNull
/**
* Returns a map of adapter position -> span index from GLM children
*/
private Map<Integer, Integer> getPositionToSpanIndexMapping() {
Map<Integer, Integer> result = new HashMap<>();
for (int i = 0; i < mGlm.getChildCount(); i++) {
TestViewHolder viewHolder = (TestViewHolder) mRecyclerView.getChildViewHolder(
mGlm.getChildAt(i)
);
int adapterPos = viewHolder.getAbsoluteAdapterPosition();
GridLayoutManager.LayoutParams layoutParams =
(GridLayoutManager.LayoutParams) viewHolder.itemView.getLayoutParams();
result.put(adapterPos, layoutParams.getSpanIndex());
}
return result;
}
@Test
public void getSpanGroupIndex_noCaching() {
assertGetSpanGroupIndex();
}
@Test
public void getSpanGroupIndex_cacheSpanIndex() {
mSpanSizeLookupForSpanIndexTest.setSpanIndexCacheEnabled(true);
assertGetSpanGroupIndex();
}
@Test
public void getSpanGroupIndex_cacheSpanGroupIndex() {
mSpanSizeLookupForSpanIndexTest.setSpanGroupIndexCacheEnabled(true);
assertGetSpanGroupIndex();
}
@Test
public void getSpanGroupIndex_cacheAll() {
mSpanSizeLookupForSpanIndexTest.setSpanGroupIndexCacheEnabled(true);
mSpanSizeLookupForSpanIndexTest.setSpanIndexCacheEnabled(true);
assertGetSpanGroupIndex();
}
private void assertGetSpanGroupIndex() {
assertEquals(0, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(0, 3));
assertEquals(0, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(1, 3));
assertEquals(0, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(2, 3));
assertEquals(1, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(3, 3));
assertEquals(2, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(4, 3));
assertEquals(3, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(5, 3));
assertEquals(4, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(6, 3));
assertEquals(5, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(7, 3));
assertEquals(6, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(8, 3));
assertEquals(7, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(9, 3));
assertEquals(8, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(10, 3));
assertEquals(9, mSpanSizeLookupForSpanIndexTest.getSpanGroupIndex(11, 3));
}
@Test
public void getSpanGroupIndex_calledTwiceForSameItemAndCachingOn_internalCalledOnce() {
final int[] callCount = new int[] {0};
GridLayoutManager.SpanSizeLookup spanSizeLookup = new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return SPAN_SIZES[position];
}
@Override
public int getSpanGroupIndex(int adapterPosition, int spanCount) {
callCount[0]++;
return super.getSpanGroupIndex(adapterPosition, spanCount);
}
};
spanSizeLookup.setSpanGroupIndexCacheEnabled(true);
spanSizeLookup.getCachedSpanGroupIndex(0, 3);
spanSizeLookup.getCachedSpanGroupIndex(0, 3);
assertEquals(1, callCount[0]);
}
@Test
public void computeVerticalScrollValues_isCorrect() throws Throwable {
assertThatComputeScrollValuesIsCorrect(VERTICAL);
}
@Test
public void computeHorizontalScrollValues_isCorrect() throws Throwable {
assertThatComputeScrollValuesIsCorrect(HORIZONTAL);
}
private void assertThatComputeScrollValuesIsCorrect(@RecyclerView.Orientation int orientation)
throws Throwable {
final int spanCount = 2;
final int itemCount = 100;
final int childWidth = orientation == VERTICAL ? MATCH_PARENT : 100;
final int childHeight = orientation == VERTICAL ? 100 : MATCH_PARENT;
final int rvHeight = orientation == VERTICAL ? childHeight * 4 : MATCH_PARENT;
final int rvWidth = orientation == VERTICAL ? MATCH_PARENT : childWidth * 4;
final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
.orientation(orientation),
new GridTestAdapter(itemCount, 1) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
holder.itemView.setLayoutParams(
new RecyclerView.LayoutParams(childWidth, childHeight));
}
});
recyclerView.setLayoutParams(new ViewGroup.LayoutParams(rvWidth, rvHeight));
mGlm.setUsingSpansToEstimateScrollbarDimensions(true);
int[] fullSpanItems = new int[itemCount / 2];
for (int i = 0; i < fullSpanItems.length; i++) {
fullSpanItems[i] = i;
}
int expectedNumberOfRows = itemCount / 2 /* half the rows contain one item */
+ itemCount / 2 / 2; /* the other half of the rows contain two items */
mAdapter.setFullSpan(fullSpanItems);
waitForFirstLayout(recyclerView);
int expectedExtent = orientation == VERTICAL ? rvHeight : rvWidth;
int childSize = orientation == VERTICAL ? childHeight : childWidth;
assertEquals(0, getScrollOffset(recyclerView, orientation));
assertEquals(expectedExtent, getScrollExtent(recyclerView, orientation));
assertEquals(childSize * expectedNumberOfRows, getScrollRange(recyclerView, orientation));
scrollToPosition(10);
mGlm.waitForLayout(2);
// We scroll to position 10 that means that the first item on the screen is item 7, because
// there are four items on the screen, so 7,8,9,10.
assertEquals(childSize * 7, getScrollOffset(recyclerView, orientation));
assertEquals(expectedExtent, getScrollExtent(recyclerView, orientation));
assertEquals(childSize * expectedNumberOfRows, getScrollRange(recyclerView, orientation));
scrollToPosition(itemCount - 1);
mGlm.waitForLayout(2);
assertEquals(childSize * (expectedNumberOfRows - 4),
getScrollOffset(recyclerView, orientation));
assertEquals(expectedExtent, getScrollExtent(recyclerView, orientation));
assertEquals(childSize * expectedNumberOfRows,
mGlm.computeVerticalScrollRange(recyclerView.mState));
}
private int getScrollOffset(
RecyclerView recyclerView,
@RecyclerView.Orientation int orientation) {
return orientation == VERTICAL
? mGlm.computeVerticalScrollOffset(recyclerView.mState)
: mGlm.computeHorizontalScrollOffset(recyclerView.mState);
}
private int getScrollExtent(
RecyclerView recyclerView,
@RecyclerView.Orientation int orientation) {
return orientation == VERTICAL
? mGlm.computeVerticalScrollExtent(recyclerView.mState)
: mGlm.computeHorizontalScrollExtent(recyclerView.mState);
}
private int getScrollRange(
RecyclerView recyclerView,
@RecyclerView.Orientation int orientation) {
return orientation == VERTICAL
? mGlm.computeVerticalScrollRange(recyclerView.mState)
: mGlm.computeHorizontalScrollRange(recyclerView.mState);
}
}