diff --git a/dpadrecyclerview/api/dpadrecyclerview.api b/dpadrecyclerview/api/dpadrecyclerview.api index 9facf262..a358de3b 100644 --- a/dpadrecyclerview/api/dpadrecyclerview.api +++ b/dpadrecyclerview/api/dpadrecyclerview.api @@ -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 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 263dc3be..0dc63dc6 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 @@ -27,7 +27,7 @@ import java.util.Collections import java.util.concurrent.Executors abstract class AbstractTestAdapter( - adapterConfiguration: TestAdapterConfiguration + adapterConfiguration: TestAdapterConfiguration, ) : RecyclerView.Adapter(), DpadDragHelper.DragAdapter { companion object { @@ -84,7 +84,7 @@ abstract class AbstractTestAdapter( private fun calculateDiff( oldList: List, - newList: List + newList: List, ): DiffResult { return DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size @@ -98,7 +98,7 @@ abstract class AbstractTestAdapter( override fun areContentsTheSame( oldItemPosition: Int, - newItemPosition: Int + newItemPosition: Int, ): Boolean { val oldItem = oldList[oldItemPosition] val newItem = newList[newItemPosition] @@ -110,7 +110,7 @@ abstract class AbstractTestAdapter( private fun latchList( newList: MutableList, result: DiffResult, - commitCallback: Runnable? + commitCallback: Runnable?, ) { items = newList result.dispatchUpdatesTo(this) @@ -123,13 +123,22 @@ abstract class AbstractTestAdapter( 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) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/VerticalFocusTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/VerticalFocusTest.kt index 5c1a082d..33047d51 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/VerticalFocusTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/VerticalFocusTest.kt @@ -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 diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/AdapterMutationTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/AdapterMutationTest.kt index 82bd58e6..a522d6ad 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/AdapterMutationTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/mutation/AdapterMutationTest.kt @@ -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 @@ -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 + ), + ) + } + } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt index 891e9e5a..df9ed0de 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt @@ -70,6 +70,19 @@ open class DpadRecyclerView @JvmOverloads constructor( private val focusableChildDrawingCallback = FocusableChildDrawingCallback() private val fadingEdge = FadingEdge() private val focusLossListeners = mutableListOf() + 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 -> @@ -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 {