| /* |
| * 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); |
| |
| } |
| } |