From dbe8ef58eccfe15491817bd0c8e65affec15b6a9 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Thu, 30 May 2024 13:29:43 +0200 Subject: [PATCH] Invalidate span cache to prevent infinite layout loops --- .../test/tests/AbstractTestAdapter.kt | 2 +- .../tests/mutation/GridAdapterMutationTest.kt | 96 +++++++++++++++++++ .../layoutmanager/PivotLayoutManager.kt | 8 +- .../layoutmanager/layout/LayoutInfo.kt | 4 + .../layout/grid/GridLayoutEngineer.kt | 2 + 5 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/GridAdapterMutationTest.kt diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/AbstractTestAdapter.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/AbstractTestAdapter.kt index ef517d59..3350ca8a 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/AbstractTestAdapter.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/AbstractTestAdapter.kt @@ -140,6 +140,6 @@ abstract class AbstractTestAdapter( override fun getItemCount(): Int = items.size - protected fun getItem(position: Int) = items[position] + fun getItem(position: Int) = items[position] } diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/GridAdapterMutationTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/GridAdapterMutationTest.kt new file mode 100644 index 00000000..474f06d2 --- /dev/null +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/GridAdapterMutationTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2024 RĂºben Sousa + * + * 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 com.rubensousa.dpadrecyclerview.test.tests.mutation + +import androidx.recyclerview.widget.RecyclerView +import com.google.common.truth.Truth.assertThat +import com.rubensousa.dpadrecyclerview.ChildAlignment +import com.rubensousa.dpadrecyclerview.DpadSpanSizeLookup +import com.rubensousa.dpadrecyclerview.ParentAlignment +import com.rubensousa.dpadrecyclerview.test.TestAdapterConfiguration +import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration +import com.rubensousa.dpadrecyclerview.test.helpers.assertFocusAndSelection +import com.rubensousa.dpadrecyclerview.test.helpers.assertItemAtPosition +import com.rubensousa.dpadrecyclerview.test.helpers.getRelativeItemViewBounds +import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView +import com.rubensousa.dpadrecyclerview.test.tests.AbstractTestAdapter +import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest +import com.rubensousa.dpadrecyclerview.testing.KeyEvents +import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class GridAdapterMutationTest : DpadRecyclerViewTest() { + + @get:Rule + val idleTimeoutRule = DisableIdleTimeoutRule() + + override fun getDefaultLayoutConfiguration(): TestLayoutConfiguration { + return TestLayoutConfiguration( + spans = 4, + orientation = RecyclerView.VERTICAL, + parentAlignment = ParentAlignment( + edge = ParentAlignment.Edge.MIN_MAX, + fraction = 0.5f + ), + childAlignment = ChildAlignment( + fraction = 0.5f + ) + ) + } + + override fun getDefaultAdapterConfiguration(): TestAdapterConfiguration { + return super.getDefaultAdapterConfiguration().copy( + numberOfItems = 40 + ) + } + + @Before + fun setup() { + launchFragment() + } + + @Test + fun testGridRemovalWithSpanLookupDoesNotCrash() { + onRecyclerView("Change span size") { recyclerView -> + recyclerView.setSpanSizeLookup(object : DpadSpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + val adapter = recyclerView.adapter as AbstractTestAdapter<*> + val item = adapter.getItem(position) + return if (item % 9 == 0) { + recyclerView.getSpanCount() + } else { + 1 + } + } + }) + } + KeyEvents.pressDown() + val oldViewBounds = getRelativeItemViewBounds(position = 1) + mutateAdapter { adapter -> + adapter.removeAt(3) + adapter.removeAt(13) + } + assertFocusAndSelection(1) + assertItemAtPosition(position = 1, item = 1) + + val newViewBounds = getRelativeItemViewBounds(position = 1) + assertThat(newViewBounds).isEqualTo(oldViewBounds) + } + +} diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt index 73286406..597ffeae 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt @@ -258,24 +258,24 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager() } override fun onItemsAdded(recyclerView: RecyclerView, positionStart: Int, itemCount: Int) { - configuration.spanSizeLookup.invalidateCache() + layoutInfo.invalidateSpanCache() pivotLayout.onItemsAdded(positionStart, itemCount) pivotSelector.onItemsAdded(positionStart, itemCount) } override fun onItemsChanged(recyclerView: RecyclerView) { - configuration.spanSizeLookup.invalidateCache() + layoutInfo.invalidateSpanCache() pivotSelector.onItemsChanged() } override fun onItemsRemoved(recyclerView: RecyclerView, positionStart: Int, itemCount: Int) { - configuration.spanSizeLookup.invalidateCache() + layoutInfo.invalidateSpanCache() pivotLayout.onItemsRemoved(positionStart, itemCount) pivotSelector.onItemsRemoved(positionStart, itemCount) } override fun onItemsMoved(recyclerView: RecyclerView, from: Int, to: Int, itemCount: Int) { - configuration.spanSizeLookup.invalidateCache() + layoutInfo.invalidateSpanCache() pivotLayout.onItemsMoved(from, to, itemCount) pivotSelector.onItemsMoved(from, to, itemCount) } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt index e4aa5c08..621d335b 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt @@ -134,6 +134,10 @@ internal class LayoutInfo( return getStartSpanIndex(position) + configuration.spanSizeLookup.getSpanSize(position) - 1 } + fun invalidateSpanCache() { + configuration.spanSizeLookup.invalidateCache() + } + fun getSpanGroupIndex(position: Int): Int { return configuration.spanSizeLookup.getCachedSpanGroupIndex( position, diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/grid/GridLayoutEngineer.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/grid/GridLayoutEngineer.kt index c15f7f48..f8f5b410 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/grid/GridLayoutEngineer.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/grid/GridLayoutEngineer.kt @@ -589,6 +589,7 @@ internal class GridLayoutEngineer( ) return layoutInfo.getStartSpanIndex(position) } + layoutInfo.invalidateSpanCache() return layoutInfo.getStartSpanIndex(adapterPosition) } @@ -608,6 +609,7 @@ internal class GridLayoutEngineer( ) return layoutInfo.getSpanSize(position) } + layoutInfo.invalidateSpanCache() return layoutInfo.getSpanSize(adapterPosition) }