Skip to content

Commit

Permalink
Merge pull request #198 from rubensousa/expose_focus
Browse files Browse the repository at this point in the history
Add OnViewFocusedListener to observe focus changes
  • Loading branch information
rubensousa committed Mar 17, 2024
2 parents b89d9b2 + 5b8c3ac commit 4ac0912
Show file tree
Hide file tree
Showing 23 changed files with 807 additions and 8 deletions.
@@ -0,0 +1,22 @@
/*
* 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 android.view.View
import androidx.recyclerview.widget.RecyclerView

data class DpadFocusEvent(val parent: RecyclerView.ViewHolder, val child: View, val position: Int)
10 changes: 10 additions & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Expand Up @@ -45,9 +45,11 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle
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 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 clearOnLayoutCompletedListeners ()V
public final fun clearOnViewFocusedListeners ()V
public final fun clearOnViewHolderSelectedListeners ()V
protected fun dispatchDraw (Landroid/graphics/Canvas;)V
protected final fun dispatchGenericFocusedEvent (Landroid/view/MotionEvent;)Z
Expand Down Expand Up @@ -97,6 +99,7 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle
public fun onScrollStateChanged (I)V
protected fun onSizeChanged (IIII)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
public final fun removeView (Landroid/view/View;)V
public final fun removeViewAt (I)V
Expand Down Expand Up @@ -250,6 +253,10 @@ public abstract interface class com/rubensousa/dpadrecyclerview/OnChildLaidOutLi
public abstract fun onChildLaidOut (Landroidx/recyclerview/widget/RecyclerView;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)V
}

public abstract interface class com/rubensousa/dpadrecyclerview/OnViewFocusedListener {
public abstract fun onViewFocused (Landroidx/recyclerview/widget/RecyclerView$ViewHolder;Landroid/view/View;)V
}

public abstract interface class com/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener {
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
Expand Down Expand Up @@ -391,11 +398,13 @@ public final class com/rubensousa/dpadrecyclerview/layoutmanager/DpadLayoutParam
public final class com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager : androidx/recyclerview/widget/RecyclerView$LayoutManager {
public fun <init> (Landroidx/recyclerview/widget/RecyclerView$LayoutManager$Properties;)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 clearOnLayoutCompletedListeners ()V
public final fun clearOnViewFocusedListeners ()V
public final fun clearOnViewHolderSelectedListeners ()V
public fun collectAdjacentPrefetchPositions (IILandroidx/recyclerview/widget/RecyclerView$State;Landroidx/recyclerview/widget/RecyclerView$LayoutManager$LayoutPrefetchRegistry;)V
public fun collectInitialPrefetchPositions (ILandroidx/recyclerview/widget/RecyclerView$LayoutManager$LayoutPrefetchRegistry;)V
Expand Down Expand Up @@ -448,6 +457,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 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
public fun requestChildRectangleOnScreen (Landroidx/recyclerview/widget/RecyclerView;Landroid/view/View;Landroid/graphics/Rect;Z)Z
public fun scrollHorizontallyBy (ILandroidx/recyclerview/widget/RecyclerView$Recycler;Landroidx/recyclerview/widget/RecyclerView$State;)I
Expand Down
Expand Up @@ -22,15 +22,17 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
import com.rubensousa.dpadrecyclerview.OnChildLaidOutListener
import com.rubensousa.dpadrecyclerview.OnViewFocusedListener
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.DpadFocusEvent
import com.rubensousa.dpadrecyclerview.testfixtures.DpadSelectionEvent
import com.rubensousa.dpadrecyclerview.testing.R

open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container),
OnViewHolderSelectedListener, OnChildLaidOutListener {
OnViewHolderSelectedListener, OnChildLaidOutListener, OnViewFocusedListener {

companion object {

Expand Down Expand Up @@ -61,6 +63,7 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
private val tasks = ArrayList<DpadSelectionEvent>()
private val alignedEvents = ArrayList<DpadSelectionEvent>()
private val layoutEvents = ArrayList<RecyclerView.ViewHolder>()
private val focusEvents = ArrayList<DpadFocusEvent>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand All @@ -72,6 +75,7 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
recyclerView.setRecycledViewPool(viewPool)
}
recyclerView.addOnViewHolderSelectedListener(this)
recyclerView.addOnViewFocusedListener(this)
recyclerView.setOnChildLaidOutListener(this)

recyclerView.apply {
Expand Down Expand Up @@ -103,6 +107,10 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)
getDpadRecyclerView()?.requestFocus()
}

fun clearFocus() {
view?.findViewById<View>(R.id.focusPlaceholderView)?.requestFocus()
}

open fun createAdapter(
recyclerView: DpadRecyclerView,
adapterConfig: TestAdapterConfiguration
Expand Down Expand Up @@ -191,6 +199,12 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)

fun getLayoutEvents(): List<RecyclerView.ViewHolder> = layoutEvents

fun getFocusEvents() = focusEvents.toList()

override fun onViewFocused(parent: RecyclerView.ViewHolder, child: View) {
focusEvents.add(DpadFocusEvent(parent, child, parent.layoutPosition))
}

override fun onChildLaidOut(parent: RecyclerView, child: RecyclerView.ViewHolder) {
layoutEvents.add(child)
}
Expand All @@ -205,4 +219,5 @@ open class TestGridFragment : Fragment(R.layout.dpadrecyclerview_test_container)

private fun getDpadRecyclerView(): DpadRecyclerView? = view?.findViewById(R.id.recyclerView)


}
@@ -0,0 +1,117 @@
/*
* 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.test

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
import com.rubensousa.dpadrecyclerview.OnViewFocusedListener
import com.rubensousa.dpadrecyclerview.test.tests.AbstractTestAdapter
import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent

class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container) {

private val configuration = TestAdapterConfiguration(
itemLayoutId = R.layout.dpadrecyclerview_item_horizontal,
numberOfItems = 200
)
private val parentFocusEvents = arrayListOf<DpadFocusEvent>()
private val childFocusEvents = arrayListOf<DpadFocusEvent>()
private val parentFocusListener = object : OnViewFocusedListener {
override fun onViewFocused(
parent: RecyclerView.ViewHolder,
child: View,
) {
parentFocusEvents.add(DpadFocusEvent(parent, child, parent.layoutPosition))
}
}
private val childFocusListener = object : OnViewFocusedListener {
override fun onViewFocused(
parent: RecyclerView.ViewHolder,
child: View,
) {
childFocusEvents.add(DpadFocusEvent(parent, child, parent.layoutPosition))
}
}
private val nestedAdapter = NestedAdapter(configuration, childFocusListener)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = view.findViewById<DpadRecyclerView>(R.id.recyclerView)
recyclerView.adapter = nestedAdapter
recyclerView.addOnViewFocusedListener(parentFocusListener)
recyclerView.requestFocus()
}

fun getChildFocusEvents() = childFocusEvents.toList()

fun getParentFocusEvents() = parentFocusEvents.toList()

fun getRecyclerView(): DpadRecyclerView = requireView().findViewById(R.id.recyclerView)

class NestedAdapter(
private val configuration: TestAdapterConfiguration,
private val onViewFocusedListener: OnViewFocusedListener
) : 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
)
}

override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
holder.bind(position)
}

}


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

private 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)

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

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

}

}
Expand Up @@ -30,6 +30,7 @@ import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration
import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView
import com.rubensousa.dpadrecyclerview.test.helpers.waitForCondition
import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState
import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent
import com.rubensousa.dpadrecyclerview.testfixtures.DpadSelectionEvent
import com.rubensousa.dpadrecyclerview.testfixtures.LayoutManagerAssertions
import com.rubensousa.dpadrecyclerview.testfixtures.LayoutMatrix
Expand Down Expand Up @@ -128,6 +129,14 @@ abstract class DpadRecyclerViewTest {
return events
}

fun getFocusEvents(): List<DpadFocusEvent> {
var events = listOf<DpadFocusEvent>()
fragmentScenario.onFragment { fragment ->
events = fragment.getFocusEvents()
}
return events
}

fun getViewHolderDeselections(): List<Int> {
var events = listOf<Int>()
fragmentScenario.onFragment { fragment ->
Expand Down

0 comments on commit 4ac0912

Please sign in to comment.