Skip to content

Commit

Permalink
Merge pull request #280 from rubensousa/deselection
Browse files Browse the repository at this point in the history
Add deselection listener with previous ViewHolder that was selected
  • Loading branch information
rubensousa authored Oct 16, 2024
2 parents cca238e + d4288a1 commit 057a7ad
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.testfixtures

import androidx.recyclerview.widget.RecyclerView

data class DpadDeselectionEvent(val viewHolder: RecyclerView.ViewHolder)
9 changes: 8 additions & 1 deletion dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;I)V
public synthetic fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addOnChildLaidOutListener (Lcom/rubensousa/dpadrecyclerview/OnChildLaidOutListener;)V
public final fun addOnFocusLostListener (Lcom/rubensousa/dpadrecyclerview/OnFocusLostListener;)V
public final fun addOnLayoutCompletedListener (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnLayoutCompletedListener;)V
public final fun addOnViewFocusedListener (Lcom/rubensousa/dpadrecyclerview/OnViewFocusedListener;)V
public final fun addOnViewHolderSelectedListener (Lcom/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener;)V
public final fun addRecyclerListener (Landroidx/recyclerview/widget/RecyclerView$RecyclerListener;)V
public final fun clearOnChildLaidOutListeners ()V
public final fun clearOnFocusLostListeners ()V
public final fun clearOnLayoutCompletedListeners ()V
public final fun clearOnViewFocusedListeners ()V
Expand Down Expand Up @@ -132,6 +134,7 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle
public final fun onRtlPropertiesChanged (I)V
public fun onScrollStateChanged (I)V
protected fun onSizeChanged (IIII)V
public final fun removeOnChildLaidOutListener (Lcom/rubensousa/dpadrecyclerview/OnChildLaidOutListener;)V
public final fun removeOnFocusLostListener (Lcom/rubensousa/dpadrecyclerview/OnFocusLostListener;)V
public final fun removeOnLayoutCompletedListener (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnLayoutCompletedListener;)V
public final fun removeOnViewFocusedListener (Lcom/rubensousa/dpadrecyclerview/OnViewFocusedListener;)V
Expand Down Expand Up @@ -345,11 +348,13 @@ public abstract interface class com/rubensousa/dpadrecyclerview/OnViewFocusedLis
}

public abstract interface class com/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener {
public abstract fun onViewHolderDeselected (Landroidx/recyclerview/widget/RecyclerView;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)V
public abstract fun onViewHolderSelected (Landroidx/recyclerview/widget/RecyclerView;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;II)V
public abstract fun onViewHolderSelectedAndAligned (Landroidx/recyclerview/widget/RecyclerView;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;II)V
}

public final class com/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener$DefaultImpls {
public static fun onViewHolderDeselected (Lcom/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener;Landroidx/recyclerview/widget/RecyclerView;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)V
public static fun onViewHolderSelected (Lcom/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener;Landroidx/recyclerview/widget/RecyclerView;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;II)V
public static fun onViewHolderSelectedAndAligned (Lcom/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener;Landroidx/recyclerview/widget/RecyclerView;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;II)V
}
Expand Down Expand Up @@ -485,12 +490,14 @@ public final class com/rubensousa/dpadrecyclerview/layoutmanager/DpadLayoutParam
public final class com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager : androidx/recyclerview/widget/RecyclerView$LayoutManager, androidx/recyclerview/widget/ItemTouchHelper$ViewDropHandler, androidx/recyclerview/widget/RecyclerView$SmoothScroller$ScrollVectorProvider {
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;II)V
public fun <init> (Landroidx/recyclerview/widget/RecyclerView$LayoutManager$Properties;)V
public final fun addOnChildLaidOutListener (Lcom/rubensousa/dpadrecyclerview/OnChildLaidOutListener;)V
public final fun addOnLayoutCompletedListener (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnLayoutCompletedListener;)V
public final fun addOnViewFocusedListener (Lcom/rubensousa/dpadrecyclerview/OnViewFocusedListener;)V
public final fun addOnViewHolderSelectedListener (Lcom/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener;)V
public fun canScrollHorizontally ()Z
public fun canScrollVertically ()Z
public fun checkLayoutParams (Landroidx/recyclerview/widget/RecyclerView$LayoutParams;)Z
public final fun clearOnChildLaidOutListeners ()V
public final fun clearOnLayoutCompletedListeners ()V
public final fun clearOnViewFocusedListeners ()V
public final fun clearOnViewHolderSelectedListeners ()V
Expand Down Expand Up @@ -547,6 +554,7 @@ public final class com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutMana
public fun onSaveInstanceState ()Landroid/os/Parcelable;
public fun performAccessibilityAction (Landroidx/recyclerview/widget/RecyclerView$Recycler;Landroidx/recyclerview/widget/RecyclerView$State;ILandroid/os/Bundle;)Z
public fun prepareForDrop (Landroid/view/View;Landroid/view/View;II)V
public final fun removeOnChildLaidOutListener (Lcom/rubensousa/dpadrecyclerview/OnChildLaidOutListener;)V
public final fun removeOnLayoutCompletedListener (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnLayoutCompletedListener;)V
public final fun removeOnViewFocusedListener (Lcom/rubensousa/dpadrecyclerview/OnViewFocusedListener;)V
public final fun removeOnViewHolderSelectedListener (Lcom/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener;)V
Expand All @@ -571,7 +579,6 @@ public final class com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutMana
public final fun setLoopDirection (Lcom/rubensousa/dpadrecyclerview/DpadLoopDirection;)V
public final fun setMaxPendingAlignments (I)V
public final fun setMaxPendingMoves (I)V
public final fun setOnChildLaidOutListener (Lcom/rubensousa/dpadrecyclerview/OnChildLaidOutListener;)V
public final fun setOrientation (I)V
public final fun setParentAlignment (Lcom/rubensousa/dpadrecyclerview/ParentAlignment;Z)V
public final fun setRecycleChildrenOnDetach (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.rubensousa.dpadrecyclerview.OnViewHolderSelectedListener
import com.rubensousa.dpadrecyclerview.UnboundViewPool
import com.rubensousa.dpadrecyclerview.ViewHolderTask
import com.rubensousa.dpadrecyclerview.test.tests.AbstractTestAdapter
import com.rubensousa.dpadrecyclerview.testfixtures.DpadDeselectionEvent
import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent
import com.rubensousa.dpadrecyclerview.testfixtures.DpadSelectionEvent
import com.rubensousa.dpadrecyclerview.testing.R
Expand Down Expand Up @@ -59,6 +60,7 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
}

private val selectionEvents = ArrayList<DpadSelectionEvent>()
private val deselectionEvents = ArrayList<DpadDeselectionEvent>()
private val viewHolderSelections = ArrayList<Int>()
private val viewHolderDeselections = ArrayList<Int>()
private val tasks = ArrayList<DpadSelectionEvent>()
Expand All @@ -81,7 +83,7 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
}
recyclerView.addOnViewHolderSelectedListener(this)
recyclerView.addOnViewFocusedListener(this)
recyclerView.setOnChildLaidOutListener(this)
recyclerView.addOnChildLaidOutListener(this)

recyclerView.apply {
setReverseLayout(layoutConfig.reverseLayout)
Expand Down Expand Up @@ -127,6 +129,10 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
)
}

override fun onViewHolderDeselected(parent: RecyclerView, child: RecyclerView.ViewHolder) {
deselectionEvents.add(DpadDeselectionEvent(child))
}

override fun onViewHolderSelected(
parent: RecyclerView,
child: RecyclerView.ViewHolder?,
Expand Down Expand Up @@ -196,6 +202,8 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)

fun getSelectionEvents(): List<DpadSelectionEvent> = selectionEvents

fun getDeselectionEvents(): List<DpadDeselectionEvent> = deselectionEvents

fun getViewHolderSelections(): List<Int> = viewHolderSelections

fun getViewHolderDeselections(): List<Int> = viewHolderDeselections
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ class ReverseVerticalTest : DpadRecyclerViewTest() {

repeat(5) {
KeyEvents.pressDown()
waitForIdleScrollState()
column.scrollDown()
assertChildrenPositions(column)
}

repeat(5) {
KeyEvents.pressUp()
waitForIdleScrollState()
column.scrollUp()
assertChildrenPositions(column)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition
import com.rubensousa.dpadrecyclerview.test.helpers.waitForCondition
import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState
import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest
import com.rubensousa.dpadrecyclerview.testfixtures.DpadDeselectionEvent
import com.rubensousa.dpadrecyclerview.testfixtures.DpadSelectionEvent
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.R
Expand Down Expand Up @@ -283,4 +284,22 @@ class SelectionTest : DpadRecyclerViewTest() {
assertFocusAndSelection(numberOfItems + 9)
}

@Test
fun testDeselectionEventIsSent() {
// given
launchFragment()

// when
KeyEvents.pressDown()
waitForIdleScrollState()

// then
var receivedEvents: List<DpadDeselectionEvent> = emptyList()
executeOnFragment { fragment ->
receivedEvents = fragment.getDeselectionEvents()
}
assertThat(receivedEvents.size).isEqualTo(1)
assertThat(receivedEvents.first().viewHolder.layoutPosition).isEqualTo(0)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1560,8 +1560,37 @@ open class DpadRecyclerView @JvmOverloads constructor(
*
* @param listener the listener to be invoked.
*/
@Deprecated("Use addOnChildLaidOutListener")
fun setOnChildLaidOutListener(listener: OnChildLaidOutListener?) {
requireLayout().setOnChildLaidOutListener(listener)
requireLayout().clearOnLayoutCompletedListeners()
if (listener != null) {
requireLayout().addOnChildLaidOutListener(listener)
}
}

/**
* Registers a callback to be invoked when an item of this [DpadRecyclerView] has been laid out.
*
* @param listener the listener to be invoked.
*/
fun addOnChildLaidOutListener(listener: OnChildLaidOutListener) {
requireLayout().addOnChildLaidOutListener(listener)
}

/**
* @see addOnChildLaidOutListener
*
* @param listener the listener to be removed.
*/
fun removeOnChildLaidOutListener(listener: OnChildLaidOutListener) {
requireLayout().removeOnChildLaidOutListener(listener)
}

/**
* Clears all listeners added by [addOnChildLaidOutListener]
*/
fun clearOnChildLaidOutListeners() {
requireLayout().clearOnChildLaidOutListeners()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,14 @@ interface OnViewHolderSelectedListener {
) {
}

/**
* @param parent The RecyclerView where the deselection happened.
* @param child The ViewHolder within the RecyclerView that was deselected
*/
fun onViewHolderDeselected(
parent: RecyclerView,
child: RecyclerView.ViewHolder,
) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -706,8 +706,16 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
return layoutInfo.findLastCompletelyVisiblePosition()
}

fun setOnChildLaidOutListener(listener: OnChildLaidOutListener?) {
pivotLayout.setOnChildLaidOutListener(listener)
fun addOnChildLaidOutListener(listener: OnChildLaidOutListener) {
pivotLayout.addOnChildLaidOutListener(listener)
}

fun removeOnChildLaidOutListener(listener: OnChildLaidOutListener) {
pivotLayout.removeOnChildLaidOutListener(listener)
}

fun clearOnChildLaidOutListeners() {
pivotLayout.clearOnChildLaidOutListeners()
}

fun addOnLayoutCompletedListener(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.rubensousa.dpadrecyclerview.layoutmanager
import android.util.Log
import android.view.View
import android.view.ViewParent
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.RecyclerView.ViewHolder
Expand Down Expand Up @@ -65,7 +64,7 @@ internal class PivotSelector(
private val requestLayoutRunnable = Runnable {
layoutManager.requestLayout()
}
private var selectedViewHolder: DpadViewHolder? = null
private var selectedViewHolder: ViewHolder? = null
private var pendingChildFocus: View? = null

fun update(newPosition: Int, newSubPosition: Int = 0): Boolean {
Expand Down Expand Up @@ -219,10 +218,21 @@ internal class PivotSelector(
dispatchViewHolderSelected()
dispatchViewHolderSelectedAndAligned()
}
/**
* Always update the selected ViewHolder on every layout update
*/
if (position >= 0 && position < layoutInfo.getItemCount()) {
layoutInfo.findViewByAdapterPosition(position)?.let { view ->
layoutInfo.getChildViewHolder(view)?.let {
selectedViewHolder = it
}
}
}
}

fun getCurrentSubPositions(): Int {
return selectedViewHolder?.getSubPositionAlignments()?.size ?: 0
val dpadViewHolder = selectedViewHolder as? DpadViewHolder
return dpadViewHolder?.getSubPositionAlignments()?.size ?: 0
}

fun setSelectionUpdatePending() {
Expand Down Expand Up @@ -332,18 +342,20 @@ internal class PivotSelector(
}

if (viewHolder !== selectedViewHolder) {
selectedViewHolder?.onViewHolderDeselected()
selectedViewHolder?.let { viewHolder ->
if (viewHolder is DpadViewHolder) {
viewHolder.onViewHolderDeselected()
}
selectionListeners.forEach { listener ->
listener.onViewHolderDeselected(recyclerView, viewHolder)
}
}
if (viewHolder is DpadViewHolder) {
selectedViewHolder = viewHolder
viewHolder.onViewHolderSelected()
} else {
selectedViewHolder = null
}
}

if (!hasSelectionListeners()) {
return
}
selectedViewHolder = viewHolder

if (viewHolder != null) {
selectionListeners.forEach { listener ->
Expand Down Expand Up @@ -393,10 +405,6 @@ internal class PivotSelector(
viewHolder.onViewHolderSelectedAndAligned()
}

if (!hasSelectionListeners()) {
return
}

if (viewHolder != null) {
selectionListeners.forEach { listener ->
listener.onViewHolderSelectedAndAligned(
Expand Down Expand Up @@ -446,7 +454,16 @@ internal class PivotSelector(
subPosition = 0
positionOffset = 0
}
selectedViewHolder?.onViewHolderDeselected()
selectedViewHolder?.let { viewHolder ->
if (viewHolder is DpadViewHolder) {
viewHolder.onViewHolderDeselected()
}
recyclerView?.let {
selectionListeners.forEach { listener ->
listener.onViewHolderDeselected(it, viewHolder)
}
}
}
selectedViewHolder = null
}

Expand All @@ -456,9 +473,7 @@ internal class PivotSelector(
* We might need to resize rows when wrap_content is used, so schedule a new layout request
*/
private fun scheduleNewLayout(recyclerView: RecyclerView) {
ViewCompat.postOnAnimation(recyclerView, requestLayoutRunnable)
recyclerView.postOnAnimation(requestLayoutRunnable)
}

private fun hasSelectionListeners(): Boolean = selectionListeners.isNotEmpty()

}
Loading

0 comments on commit 057a7ad

Please sign in to comment.