Skip to content

Commit

Permalink
Invalidate span cache to prevent infinite layout loops
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensousa committed May 30, 2024
1 parent 64e8148 commit dbe8ef5
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,6 @@ abstract class AbstractTestAdapter<VH : RecyclerView.ViewHolder>(

override fun getItemCount(): Int = items.size

protected fun getItem(position: Int) = items[position]
fun getItem(position: Int) = items[position]

}
Original file line number Diff line number Diff line change
@@ -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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ internal class GridLayoutEngineer(
)
return layoutInfo.getStartSpanIndex(position)
}
layoutInfo.invalidateSpanCache()
return layoutInfo.getStartSpanIndex(adapterPosition)
}

Expand All @@ -608,6 +609,7 @@ internal class GridLayoutEngineer(
)
return layoutInfo.getSpanSize(position)
}
layoutInfo.invalidateSpanCache()
return layoutInfo.getSpanSize(adapterPosition)
}

Expand Down

0 comments on commit dbe8ef5

Please sign in to comment.