Skip to content

Commit

Permalink
Invalidate item decorations automatically when adapter contents change
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensousa committed Aug 29, 2024
1 parent 17c184f commit 985c749
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 6 deletions.
1 change: 1 addition & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle
public final fun removeView (Landroid/view/View;)V
public final fun removeViewAt (I)V
public final fun requestLayout ()V
public fun setAdapter (Landroidx/recyclerview/widget/RecyclerView$Adapter;)V
public final fun setAlignmentLookup (Lcom/rubensousa/dpadrecyclerview/AlignmentLookup;Z)V
public static synthetic fun setAlignmentLookup$default (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Lcom/rubensousa/dpadrecyclerview/AlignmentLookup;ZILjava/lang/Object;)V
public final fun setAlignments (Lcom/rubensousa/dpadrecyclerview/ParentAlignment;Lcom/rubensousa/dpadrecyclerview/ChildAlignment;Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import java.util.Collections
import java.util.concurrent.Executors

abstract class AbstractTestAdapter<VH : RecyclerView.ViewHolder>(
adapterConfiguration: TestAdapterConfiguration
adapterConfiguration: TestAdapterConfiguration,
) : RecyclerView.Adapter<VH>(), DpadDragHelper.DragAdapter<Int> {

companion object {
Expand Down Expand Up @@ -84,7 +84,7 @@ abstract class AbstractTestAdapter<VH : RecyclerView.ViewHolder>(

private fun calculateDiff(
oldList: List<Int>,
newList: List<Int>
newList: List<Int>,
): DiffResult {
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
Expand All @@ -98,7 +98,7 @@ abstract class AbstractTestAdapter<VH : RecyclerView.ViewHolder>(

override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
newItemPosition: Int,
): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
Expand All @@ -110,7 +110,7 @@ abstract class AbstractTestAdapter<VH : RecyclerView.ViewHolder>(
private fun latchList(
newList: MutableList<Int>,
result: DiffResult,
commitCallback: Runnable?
commitCallback: Runnable?,
) {
items = newList
result.dispatchUpdatesTo(this)
Expand All @@ -123,13 +123,22 @@ abstract class AbstractTestAdapter<VH : RecyclerView.ViewHolder>(
notifyItemRemoved(index)
}

fun removeFrom(index: Int, count: Int) {
currentVersion++
repeat(count) {
items.removeAt(index)
}
notifyItemRangeRemoved(index, count)
}


fun move(from: Int, to: Int) {
currentVersion++
Collections.swap(items, from, to)
notifyItemMoved(from, to)
}

fun addAt(item: Int, index: Int) {
fun addAt(index: Int, item: Int) {
currentVersion++
items.add(index, item)
notifyItemInserted(index)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class VerticalFocusTest : DpadRecyclerViewTest() {
recyclerView.itemAnimator?.moveDuration = 2500L
}
mutateAdapter { adapter ->
adapter.addAt(1000, index = 3)
adapter.addAt(item = 1000, index = 3)
}
waitForCondition("Waiting for animation start") { recyclerView ->
recyclerView.isAnimating
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import com.rubensousa.dpadrecyclerview.ChildAlignment
import com.rubensousa.dpadrecyclerview.ParentAlignment
import com.rubensousa.dpadrecyclerview.layoutmanager.layout.ViewBounds
import com.rubensousa.dpadrecyclerview.spacing.DpadLinearSpacingDecoration
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.helpers.selectLastPosition
import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition
import com.rubensousa.dpadrecyclerview.test.helpers.waitForAdapterUpdate
Expand Down Expand Up @@ -286,4 +289,171 @@ class AdapterMutationTest : DpadRecyclerViewTest() {
// then
assertFocusAndSelection(0)
}

@Test
fun testDecorationsAreCorrectAfterRemovingItem() {
// given
val verticalItemSpacing = 24
val verticalEdgeSpacing = 200
val perpendicularSpacing = 64
val decoration = DpadLinearSpacingDecoration.create(
itemSpacing = verticalItemSpacing,
edgeSpacing = verticalEdgeSpacing,
perpendicularEdgeSpacing = perpendicularSpacing
)
onRecyclerView("Setting decoration") { recyclerView ->
recyclerView.addItemDecoration(decoration)
}

// when
mutateAdapter { adapter ->
adapter.removeFrom(2, adapter.itemCount - 2)
}
waitForAdapterUpdate()

// then
assertChildDecorations(
childIndex = 1,
insets = ViewBounds(
left = perpendicularSpacing,
top = 0,
right = perpendicularSpacing,
bottom = verticalEdgeSpacing
),
)
}

@Test
fun testDecorationsAreCorrectAfterAddingItem() {
// given
val verticalItemSpacing = 24
val verticalEdgeSpacing = 200
val perpendicularSpacing = 64
val decoration = DpadLinearSpacingDecoration.create(
itemSpacing = verticalItemSpacing,
edgeSpacing = verticalEdgeSpacing,
perpendicularEdgeSpacing = perpendicularSpacing
)
onRecyclerView("Setting decoration") { recyclerView ->
recyclerView.addItemDecoration(decoration)
}
mutateAdapter { adapter ->
adapter.setList(mutableListOf(0))
adapter.notifyDataSetChanged()
}
waitForAdapterUpdate()

// when
mutateAdapter { adapter ->
adapter.add()
}
waitForAdapterUpdate()

// then
assertChildDecorations(
childIndex = 0,
insets = ViewBounds(
left = perpendicularSpacing,
top = verticalEdgeSpacing,
right = perpendicularSpacing,
bottom = verticalItemSpacing
),
)
}

@Test
fun testDecorationsAreCorrectAfterMovingItem() {
// given
val verticalItemSpacing = 24
val verticalEdgeSpacing = 200
val perpendicularSpacing = 64
val decoration = DpadLinearSpacingDecoration.create(
itemSpacing = verticalItemSpacing,
edgeSpacing = verticalEdgeSpacing,
perpendicularEdgeSpacing = perpendicularSpacing
)
onRecyclerView("Setting decoration") { recyclerView ->
recyclerView.addItemDecoration(decoration)
}
mutateAdapter { adapter ->
adapter.setList(mutableListOf(0, 1))
adapter.notifyDataSetChanged()
}
waitForAdapterUpdate()

// when
mutateAdapter { adapter ->
adapter.move(0, 1)
}
waitForAdapterUpdate()

// then
assertChildDecorations(
childIndex = 0,
insets = ViewBounds(
left = perpendicularSpacing,
top = verticalEdgeSpacing,
right = perpendicularSpacing,
bottom = verticalItemSpacing
),
)
assertChildDecorations(
childIndex = 1,
insets = ViewBounds(
left = perpendicularSpacing,
top = 0,
right = perpendicularSpacing,
bottom = verticalEdgeSpacing
),
)
}

@Test
fun testDecorationsAreCorrectAfterUpdatingItems() {
// given
val verticalItemSpacing = 24
val verticalEdgeSpacing = 200
val perpendicularSpacing = 64
val decoration = DpadLinearSpacingDecoration.create(
itemSpacing = verticalItemSpacing,
edgeSpacing = verticalEdgeSpacing,
perpendicularEdgeSpacing = perpendicularSpacing
)
onRecyclerView("Setting decoration") { recyclerView ->
recyclerView.addItemDecoration(decoration)
}
mutateAdapter { adapter ->
adapter.setList(mutableListOf(0, 1, 2))
adapter.notifyDataSetChanged()
}
waitForAdapterUpdate()

// when
mutateAdapter { adapter ->
adapter.setList(mutableListOf(0, 1))
adapter.notifyDataSetChanged()
}
waitForAdapterUpdate()

// then
assertChildDecorations(
childIndex = 0,
insets = ViewBounds(
left = perpendicularSpacing,
top = verticalEdgeSpacing,
right = perpendicularSpacing,
bottom = verticalItemSpacing
),
)
assertChildDecorations(
childIndex = 1,
insets = ViewBounds(
left = perpendicularSpacing,
top = 0,
right = perpendicularSpacing,
bottom = verticalEdgeSpacing
),
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ open class DpadRecyclerView @JvmOverloads constructor(
private val focusableChildDrawingCallback = FocusableChildDrawingCallback()
private val fadingEdge = FadingEdge()
private val focusLossListeners = mutableListOf<OnFocusLostListener>()
private val adapterObserver = object : AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
invalidateDecorationsSafely()
}

override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
invalidateDecorationsSafely()
}

override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
invalidateDecorationsSafely()
}
}
private val globalFocusChangeListener by lazy {
GlobalFocusChangeListener(this) {
focusLossListeners.forEach { listener ->
Expand Down Expand Up @@ -221,6 +234,22 @@ open class DpadRecyclerView @JvmOverloads constructor(
return layout
}

override fun setAdapter(adapter: Adapter<*>?) {
this.adapter?.unregisterAdapterDataObserver(adapterObserver)
super.setAdapter(adapter)
adapter?.registerAdapterDataObserver(adapterObserver)
}

private fun invalidateDecorationsSafely() {
if (isComputingLayout || scrollState != SCROLL_STATE_IDLE) {
post {
invalidateDecorationsSafely()
}
} else {
invalidateItemDecorations()
}
}

final override fun setLayoutManager(layout: LayoutManager?) {
super.setLayoutManager(layout)
viewHolderTaskExecutor?.let {
Expand Down

0 comments on commit 985c749

Please sign in to comment.