Skip to content

Commit

Permalink
Merge pull request #213 from rubensousa/nested_states
Browse files Browse the repository at this point in the history
Add built-in support for saving and restoring scroll and view states
  • Loading branch information
rubensousa authored May 30, 2024
2 parents ae085c5 + 13f5532 commit 88ecc57
Show file tree
Hide file tree
Showing 16 changed files with 532 additions and 119 deletions.
23 changes: 23 additions & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,26 @@ public abstract interface class com/rubensousa/dpadrecyclerview/spacing/DpadSpac
public abstract fun shouldApplySpacing (Landroidx/recyclerview/widget/RecyclerView$ViewHolder;I)Z
}

public final class com/rubensousa/dpadrecyclerview/state/DpadScrollState {
public final fun clear ()V
public final fun clear (Ljava/lang/String;)V
public final fun restore (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Ljava/lang/String;Landroidx/recyclerview/widget/RecyclerView$Adapter;)V
public final fun save (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Ljava/lang/String;Z)V
public static synthetic fun save$default (Lcom/rubensousa/dpadrecyclerview/state/DpadScrollState;Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Ljava/lang/String;ZILjava/lang/Object;)V
}

public final class com/rubensousa/dpadrecyclerview/state/DpadStateRegistry {
public fun <init> (Landroidx/savedstate/SavedStateRegistryOwner;)V
public final fun getScrollState ()Lcom/rubensousa/dpadrecyclerview/state/DpadScrollState;
public final fun getViewHolderState ()Lcom/rubensousa/dpadrecyclerview/state/DpadViewHolderState;
public final fun setSaveInstanceStateEnabled (Z)V
}

public final class com/rubensousa/dpadrecyclerview/state/DpadViewHolderState {
public final fun clear ()V
public final fun clear (Ljava/lang/String;)V
public final fun restoreState (Landroidx/recyclerview/widget/RecyclerView$ViewHolder;Ljava/lang/String;Z)V
public static synthetic fun restoreState$default (Lcom/rubensousa/dpadrecyclerview/state/DpadViewHolderState;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;Ljava/lang/String;ZILjava/lang/Object;)V
public final fun saveState (Landroidx/recyclerview/widget/RecyclerView$ViewHolder;Ljava/lang/String;)V
}

1 change: 1 addition & 0 deletions dpadrecyclerview/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ android {
dependencies {
api libs.androidx.recyclerview
implementation libs.androidx.collection
implementation libs.androidx.fragment

// Required for dependency resolution
debugImplementation libs.guava
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
import com.rubensousa.dpadrecyclerview.OnViewFocusedListener
import com.rubensousa.dpadrecyclerview.state.DpadScrollState
import com.rubensousa.dpadrecyclerview.state.DpadStateRegistry
import com.rubensousa.dpadrecyclerview.test.tests.AbstractTestAdapter
import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent

Expand All @@ -34,6 +36,7 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container
itemLayoutId = R.layout.dpadrecyclerview_item_horizontal,
numberOfItems = 200
)
private val stateRegistry = DpadStateRegistry(this)
private val parentFocusEvents = arrayListOf<DpadFocusEvent>()
private val childFocusEvents = arrayListOf<DpadFocusEvent>()
private val parentFocusListener = object : OnViewFocusedListener {
Expand All @@ -52,7 +55,11 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container
childFocusEvents.add(DpadFocusEvent(parent, child, parent.layoutPosition))
}
}
private val nestedAdapter = NestedAdapter(configuration, childFocusListener)
private val nestedAdapter = NestedAdapter(
configuration = configuration,
onViewFocusedListener = childFocusListener,
scrollState = stateRegistry.getScrollState()
)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand All @@ -70,48 +77,63 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container

class NestedAdapter(
private val configuration: TestAdapterConfiguration,
private val onViewFocusedListener: OnViewFocusedListener
private val onViewFocusedListener: OnViewFocusedListener,
private val scrollState: DpadScrollState,
) : AbstractTestAdapter<ListViewHolder>(configuration) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.dpadrecyclerview_nested_list, parent, false),
configuration,
onViewFocusedListener
onViewFocusedListener,
)
}

override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
holder.bind(position)
scrollState.restore(
recyclerView = holder.recyclerView,
key = position.toString(),
adapter = holder.adapter
)
}

}
override fun onViewRecycled(holder: ListViewHolder) {
super.onViewRecycled(holder)
scrollState.save(
recyclerView = holder.recyclerView,
key = holder.absoluteAdapterPosition.toString(),
detachAdapter = true
)
}

}

class ListViewHolder(
val view: View,
val configuration: TestAdapterConfiguration,
onViewFocusedListener: OnViewFocusedListener,
) : RecyclerView.ViewHolder(view) {

private val adapter = TestAdapter(
val adapter = TestAdapter(
adapterConfiguration = configuration,
onViewHolderSelected = { position -> },
onViewHolderDeselected = { position -> }
)
private val textView = view.findViewById<TextView>(R.id.textView)
private val recyclerView = view.findViewById<DpadRecyclerView>(R.id.nestedRecyclerView)

val recyclerView = view.findViewById<DpadRecyclerView>(R.id.nestedRecyclerView)

init {
recyclerView.adapter = adapter
recyclerView.addOnViewFocusedListener(onViewFocusedListener)
}

fun bind(position: Int) {
recyclerView.tag = position
textView.text = "List $position"
textView.freezesText = true
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,48 @@

package com.rubensousa.dpadrecyclerview.test.tests.selection

import androidx.recyclerview.widget.RecyclerView
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import com.google.common.truth.Truth.assertThat
import com.rubensousa.dpadrecyclerview.ChildAlignment
import com.rubensousa.dpadrecyclerview.ParentAlignment
import com.rubensousa.dpadrecyclerview.ParentAlignment.Edge
import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration
import com.rubensousa.dpadrecyclerview.test.TestNestedListFragment
import com.rubensousa.dpadrecyclerview.test.helpers.assertFocusPosition
import com.rubensousa.dpadrecyclerview.test.helpers.assertOnRecyclerView
import com.rubensousa.dpadrecyclerview.test.helpers.getItemViewBounds
import com.rubensousa.dpadrecyclerview.test.helpers.getRecyclerViewBounds
import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest
import com.rubensousa.dpadrecyclerview.test.helpers.waitForCondition
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.R
import com.rubensousa.dpadrecyclerview.testing.assertions.DpadRecyclerViewAssertions
import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule
import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class SaveRestoreStateTest : DpadRecyclerViewTest() {
class SaveRestoreStateTest {

@get:Rule
val idleTimeoutRule = DisableIdleTimeoutRule()

override fun getDefaultLayoutConfiguration(): TestLayoutConfiguration {
return TestLayoutConfiguration(
spans = 1,
orientation = RecyclerView.VERTICAL,
parentAlignment = ParentAlignment(
edge = Edge.MIN_MAX
),
childAlignment = ChildAlignment(offset = 0)
)
}
private lateinit var fragmentScenario: FragmentScenario<TestNestedListFragment>

@Before
fun setup() {
launchFragment()
fragmentScenario = launchFragment()
}

@Test
fun testSelectionStateIsSavedAndRestored() {
KeyEvents.pressDown(times = 5)
assertFocusPosition(5)

recreateFragment()
fragmentScenario.recreate()

val recyclerViewBounds = getRecyclerViewBounds()
val viewBounds = getItemViewBounds(position = 5)
Expand All @@ -70,4 +66,52 @@ class SaveRestoreStateTest : DpadRecyclerViewTest() {
assertFocusPosition(5)
}

@Test
fun testSelectionStateAcrossNestedListsIsSaved() {
// given
KeyEvents.pressRight(times = 5)

// when
KeyEvents.pressDown(times = 25)
KeyEvents.pressUp(times = 25)

// then
Espresso.onView(
allOf(
withId(com.rubensousa.dpadrecyclerview.test.R.id.nestedRecyclerView),
withTagValue(Matchers.`is`(0))
)
).check(DpadRecyclerViewAssertions.isSelected(position = 5))
}

@Test
fun testSelectionStateAcrossNestedListsSurvivesConfigurationChanges() {
// given
KeyEvents.pressRight(times = 5)
KeyEvents.pressDown(times = 25)
KeyEvents.pressUp(times = 25)

// when
fragmentScenario.recreate()

// then
Espresso.onView(
allOf(
withId(com.rubensousa.dpadrecyclerview.test.R.id.nestedRecyclerView),
withTagValue(Matchers.`is`(0))
)
).check(DpadRecyclerViewAssertions.isSelected(position = 5))
}

private fun launchFragment(): FragmentScenario<TestNestedListFragment> {
return launchFragmentInContainer<TestNestedListFragment>(
themeResId = R.style.DpadRecyclerViewTestTheme
).also {
fragmentScenario = it
waitForCondition("Waiting for layout pass") { recyclerView ->
!recyclerView.isLayoutRequested
}
}
}

}
Loading

0 comments on commit 88ecc57

Please sign in to comment.