Skip to content

Commit

Permalink
Add support for selecting a sub position with a ViewHolderTask
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensousa committed Jan 17, 2024
1 parent 6633054 commit 8abe96f
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 6 deletions.
2 changes: 2 additions & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,10 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle
public final fun setSelectedPositionSmooth (ILcom/rubensousa/dpadrecyclerview/ViewHolderTask;)V
public final fun setSelectedSubPosition (I)V
public final fun setSelectedSubPosition (II)V
public final fun setSelectedSubPosition (IILcom/rubensousa/dpadrecyclerview/ViewHolderTask;)V
public final fun setSelectedSubPositionSmooth (I)V
public final fun setSelectedSubPositionSmooth (II)V
public final fun setSelectedSubPositionSmooth (IILcom/rubensousa/dpadrecyclerview/ViewHolderTask;)V
public final fun setSmoothFocusChangesEnabled (Z)V
public final fun setSmoothScrollBehavior (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$SmoothScrollByBehavior;)V
public final fun setSmoothScrollMaxPendingAlignments (I)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
recyclerView: DpadRecyclerView,
adapterConfig: TestAdapterConfiguration
): RecyclerView.Adapter<*> {
return TestAdapter(adapterConfig,
return TestAdapter(
adapterConfig,
onViewHolderSelected = ::addViewHolderSelected,
onViewHolderDeselected = ::addViewHolderDeselected
)
Expand Down Expand Up @@ -145,6 +146,30 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
}
}

fun selectWithTask(
position: Int,
subPosition: Int,
smooth: Boolean,
executeWhenAligned: Boolean = false
) {
val recyclerView = requireView().findViewById<DpadRecyclerView>(R.id.recyclerView)
val task = object : ViewHolderTask(executeWhenAligned) {
override fun execute(viewHolder: RecyclerView.ViewHolder) {
tasks.add(
DpadSelectionEvent(
position = position,
subPosition = subPosition
)
)
}
}
if (smooth) {
recyclerView.setSelectedSubPositionSmooth(position, subPosition, task)
} else {
recyclerView.setSelectedSubPosition(position, subPosition, task)
}
}

fun clearEvents() {
selectionEvents.clear()
alignedEvents.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
import com.google.common.truth.Truth.assertThat
import com.rubensousa.dpadrecyclerview.ChildAlignment
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
import com.rubensousa.dpadrecyclerview.DpadViewHolder
Expand All @@ -40,7 +41,9 @@ import com.rubensousa.dpadrecyclerview.test.assertions.ViewHolderSelectionCountA
import com.rubensousa.dpadrecyclerview.test.helpers.assertFocusAndSelection
import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition
import com.rubensousa.dpadrecyclerview.test.helpers.selectSubPosition
import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState
import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest
import com.rubensousa.dpadrecyclerview.testfixtures.DpadSelectionEvent
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.R
import org.junit.Test
Expand Down Expand Up @@ -136,6 +139,62 @@ class SubSelectionTest : DpadRecyclerViewTest() {
)
}

@Test
fun testTaskIsExecutedAfterViewHolderIsSelectedAndAligned() {
launchSubPositionFragment()

selectWithTask(
position = 0,
subPosition = 1,
smooth = true,
executeWhenAligned = true
)

waitForIdleScrollState()

selectWithTask(
position = 0,
subPosition = 2,
smooth = false,
executeWhenAligned = false
)

waitForIdleScrollState()

assertThat(getSelectionsFromTasks()).isEqualTo(
listOf(
DpadSelectionEvent(
position = 0,
subPosition = 1
),
DpadSelectionEvent(
position = 0,
subPosition = 2
)
)
)
}


private fun getSelectionsFromTasks(): List<DpadSelectionEvent> {
var events = listOf<DpadSelectionEvent>()
fragmentScenario.onFragment { fragment ->
events = fragment.getTasksExecuted()
}
return events
}

private fun selectWithTask(
position: Int,
subPosition: Int,
smooth: Boolean,
executeWhenAligned: Boolean
) {
fragmentScenario.onFragment { fragment ->
fragment.selectWithTask(position, subPosition, smooth, executeWhenAligned)
}
}

private fun launchSubPositionFragment() {
launchSubPositionFragment(
getDefaultLayoutConfiguration(),
Expand All @@ -147,15 +206,14 @@ class SubSelectionTest : DpadRecyclerViewTest() {
layoutConfiguration: TestLayoutConfiguration,
adapterConfiguration: TestAdapterConfiguration
): FragmentScenario<TestSubPositionFragment> {
return launchFragmentInContainer<TestSubPositionFragment>(
fragmentScenario = launchFragmentInContainer<TestSubPositionFragment>(
fragmentArgs = TestGridFragment.getArgs(
layoutConfiguration,
adapterConfiguration
),
themeResId = R.style.DpadRecyclerViewTestTheme
).also {
fragmentScenario = it
}
)
return fragmentScenario
}

class TestSubPositionFragment : TestGridFragment() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,18 @@ open class DpadRecyclerView @JvmOverloads constructor(
requireLayout().selectPosition(position, subPosition, smooth = false)
}

/**
* Performs a task on a ViewHolder at a given position and sub position after scrolling to it.
*
* @param position Adapter position of the item to select
* @param subPosition index of the alignment from [DpadViewHolder.getSubPositionAlignments]
* @param task Task to executed on the ViewHolder at the given position
*/
fun setSelectedSubPosition(position: Int, subPosition: Int, task: ViewHolderTask) {
viewHolderTaskExecutor.schedule(position, subPosition, task)
requireLayout().selectPosition(position, subPosition, smooth = false)
}

/**
* Changes the sub selected view immediately without any scroll animation.
* @param subPosition index of the alignment from [DpadViewHolder.getSubPositionAlignments]
Expand All @@ -1023,6 +1035,18 @@ open class DpadRecyclerView @JvmOverloads constructor(
requireLayout().selectPosition(position, subPosition, smooth = true)
}

/**
* Performs a task on a ViewHolder at a given position and sub position after scrolling to it.
*
* @param position Adapter position of the item to select
* @param subPosition index of the alignment from [DpadViewHolder.getSubPositionAlignments]
* @param task Task to executed on the ViewHolder at the given position
*/
fun setSelectedSubPositionSmooth(position: Int, subPosition: Int, task: ViewHolderTask) {
viewHolderTaskExecutor.schedule(position, subPosition, task)
requireLayout().selectPosition(position, subPosition, smooth = true)
}

/**
* @return the current selected position or [RecyclerView.NO_POSITION] if there's none
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ import androidx.recyclerview.widget.RecyclerView
internal class ViewHolderTaskExecutor : OnViewHolderSelectedListener {

private var targetPosition = RecyclerView.NO_POSITION
private var targetSubPosition = RecyclerView.NO_POSITION
private var pendingTask: ViewHolderTask? = null

fun schedule(position: Int, task: ViewHolderTask) {
targetPosition = position
targetSubPosition = RecyclerView.NO_POSITION
pendingTask = task
}

fun schedule(position: Int, subPosition: Int, task: ViewHolderTask) {
targetPosition = position
targetSubPosition = subPosition
pendingTask = task
}

Expand All @@ -36,6 +44,7 @@ internal class ViewHolderTaskExecutor : OnViewHolderSelectedListener {
) {
if (position == targetPosition
&& child != null
&& (targetSubPosition == RecyclerView.NO_POSITION || targetSubPosition == subPosition)
&& pendingTask?.executeWhenAligned == false
) {
executePendingTask(child)
Expand All @@ -50,6 +59,7 @@ internal class ViewHolderTaskExecutor : OnViewHolderSelectedListener {
) {
if (position == targetPosition
&& child != null
&& (targetSubPosition == RecyclerView.NO_POSITION || targetSubPosition == subPosition)
&& pendingTask?.executeWhenAligned == true
) {
executePendingTask(child)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ internal class PivotSelector(

fun update(newPosition: Int, newSubPosition: Int = 0): Boolean {
val previousPosition = position
val previousSubPosition = subPosition
position = constrainPivotPosition(
position = newPosition,
itemCount = layoutManager.itemCount
)
subPosition = newSubPosition
return position != previousPosition || newSubPosition != subPosition
return position != previousPosition || subPosition != previousSubPosition
}

fun consumePendingSelectionChanges(state: RecyclerView.State): Boolean {
Expand Down

0 comments on commit 8abe96f

Please sign in to comment.