DpadRecyclerView

repository·master·Indexed 41 days ago

https://github.com/rubensousa/dpadrecyclerview

A specialized RecyclerView implementation for Android TV designed as a modern replacement for Leanback BaseGridView. It provides advanced anchor calculation via ParentAlignment and ChildAlignment, and offers deep integration with Jetpack Compose through DpadComposeFocusViewHolder and DpadComposeViewHolder.

Tokens
20.4K
Snippets
73
Records
91
Agent score
48%

What's inside DpadRecyclerView

  1. What is DpadRecyclerView?

    master
    DpadRecyclerView is a specialized RecyclerView implementation designed for Android TV. It serves as a modern replacement for Leanback's BaseGridView, offering more features and better compatibility with Jetpack Compose. It is particularly useful for developers maintaining existing TV applications who want to incrementally introduce Compose, as RecyclerView provides a stable bridge between traditional View-based TV UIs and Compose TV.
  2. Use ChildAlignment to position ViewHolder views

    master

    While ParentAlignment defines the keyline in the parent, ChildAlignment defines the anchor position for the views inside each ViewHolder.

    When used together, ParentAlignment sets the global target (the blue circle in visual guides), and ChildAlignment determines how the individual child view (the green circle) sits relative to that target. For example, setting ChildAlignment(fraction = 0f) will align the start of the child view to the parent's keyline.

    // Example: Combined configuration for a horizontal list
    // Parent anchor at 24dp from the start, and child aligned to the start of that anchor
    ParentAlignment(offset = 24.dp.toPx(), fraction = 0f)
    ChildAlignment(fraction = 0f)
  3. Understand Parent Alignment and its role in anchor calculation

    master

    In DpadRecyclerView, ParentAlignment is the primary configuration used to calculate the anchor position for all ViewHolders. It defines a 'keyline' position within the parent container.

    For a vertical list, the anchor position is calculated relative to the top edge. For a horizontal list, it is calculated relative to the start edge. Both offset and fraction values are measured from this minimum edge.

    // Example: Centering the anchor in the parent
    ParentAlignment(fraction = 0.5f)
  4. How focus and selection work in DpadRecyclerView

    master

    In DpadRecyclerView, focus and selection are distinct concepts:

    1. Selection changes can occur independently of focus (e.g., you can programmatically change the selection while focus is on a different view).
    2. Focus changes trigger selection events automatically. When a user navigates via DPAD and focus moves to a different ViewHolder, a selection event is fired.

    In summary: Selection can happen without focus requests, but focusing a new view will always trigger a selection event.

  5. Compare DpadRecyclerView features with Leanback and Compose TV

    master

    DpadRecyclerView provides several features that are currently missing or less mature in Leanback or Compose TV, including:

    • Scrolling & Alignment: Custom scrolling speeds, edge alignment preference, fading edges, smooth alignment changes, and discrete scrolling for text pages.
    • Layout & Navigation: Sub position selections, grids with uneven span sizes, extra layout space, reverse layout, and infinite layout with loop.
    • Advanced UI/UX: Scrollbars, drag and drop, circular and continuous focus, and scrolling without animation.
    • Developer Tools: Espresso testing helpers, child focus observer, and throttling scroll events.
  6. Install DpadRecyclerView

    master

    Add the following dependencies to your app's build.gradle file. Replace {{ dpadrecyclerview.version }} with the desired version of the library.

    • dpadrecyclerview: The core library.
    • dpadrecyclerview-compose: Recommended if you want to integrate with Jetpack Compose.
    • dpadrecyclerview-testing: Optional Espresso test helpers for instrumented tests.
    // Core library
    implementation "com.rubensousa.dpadrecyclerview:dpadrecyclerview:{{ dpadrecyclerview.version }}"
    
    // Recommended: To use Compose together with DpadRecyclerView
    implementation "com.rubensousa.dpadrecyclerview:dpadrecyclerview-compose:{{ dpadrecyclerview.version }}"
    
    // Optional: Espresso test helpers for your instrumented tests:
    androidTestImplementation "com.rubensousa.dpadrecyclerview:dpadrecyclerview-testing:{{ dpadrecyclerview.version }}"
  7. Receive focus inside Composables using DpadComposeFocusViewHolder

    master

    If you want your Jetpack Compose elements to handle focus directly (e.g., using Modifier.onFocusChanged or Modifier.focusable()), use DpadComposeFocusViewHolder. This ViewHolder passes the focus state directly into the Composable tree.

    To implement this:

    1. Create a ListAdapter using DpadComposeFocusViewHolder<T> as the ViewHolder type.
    2. In onCreateViewHolder, provide a lambda that defines how to render the Composable.
    3. In onBindViewHolder, call holder.setItemState(item) to update the content.
    4. Inside your Composable, use standard focus modifiers like .onFocusChanged { ... } and .focusable() to react to focus changes.
    class ComposeItemAdapter(
        private val onItemClick: (Int) -> Unit
    ) : ListAdapter<Int, DpadComposeFocusViewHolder<Int>>(Item.DIFF_CALLBACK) {
    
        override fun onCreateViewHolder(
            parent: ViewGroup,
            viewType: Int
        ): DpadComposeFocusViewHolder<Int> {
            return DpadComposeFocusViewHolder(parent) { item ->
                ItemComposable(
                    item = item,
                    onClick = {
                        onItemClick(item)
                    }
                )
            }
        }
    
        override fun onBindViewHolder(
            holder: DpadComposeFocusViewHolder<Int>, 
            position: Int
        ) {
            holder.setItemState(getItem(position))
        }
    }
    
    @Composable
    fun ItemComposable(
        item: Int, 
        onClick: () -> Unit,
        modifier: Modifier = Modifier,
    ) {
        var isFocused by remember { mutableStateOf(false) }
        val backgroundColor = if (isFocused) Color.White else Color.Black
        val textColor = if (isFocused) Color.Black else Color.White
        Box(
            modifier = modifier
                .background(backgroundColor)
                .onFocusChanged { focusState ->
                    isFocused = focusState.hasFocus
                }
                .focusable()
                .dpadClickable {
                    onClick()
                },
            contentAlignment = Alignment.Center,
        ) {
            Text(
                text = item.toString(),
                color = textColor,
                fontSize = 35.sp
            )
        }
    }
  8. Configure spacing via XML

    master

    You can configure basic item spacing and edge spacing directly in your layout XML using DpadRecyclerView attributes.

    Supported attributes:

    • app:dpadRecyclerViewItemSpacing: Sets the spacing between items.
    • app:dpadRecyclerViewItemEdgeSpacing: Sets the spacing at the edges of the RecyclerView.
    <com.rubensousa.dpadrecyclerview.DpadRecyclerView 
        android:id="@+id/recyclerView"
        android:layout_height="match_parent" 
        android:layout_width="match_parent"
        android:orientation="vertical"
        app:dpadRecyclerViewItemEdgeSpacing="@dimen/vertical_edge_spacing"
        app:dpadRecyclerViewItemSpacing="@dimen/vertical_item_spacing" />
  9. Implement sub position alignment with DpadViewHolder

    master

    Sub position alignment allows you to define custom alignment rules for children within a ViewHolder. Each sub position alignment extends ChildAlignment by allowing a view to align itself relative to another view within the same item.

    To implement this:

    1. Make your RecyclerView.ViewHolder implement the DpadViewHolder interface.
    2. Override getSubPositionAlignments() to return a list of SubPositionAlignment objects.

    Each SubPositionAlignment requires:

    • fraction: The alignment fraction.
    • alignmentViewId: The ID of the view used as the reference for alignment.
    • focusViewId: The ID of the view that is being aligned.
    class ExampleViewHolder(
        view: View
    ) : RecyclerView.ViewHolder(view), DpadViewHolder {
    
        private val alignments = ArrayList<SubPositionAlignment>()
    
        init {
            alignments.apply {
                add(
                    SubPositionAlignment(
                        fraction = 0f,
                        alignmentViewId = R.id.firstView,
                        focusViewId = R.id.firstView
                    )
                )
                add(
                    SubPositionAlignment(
                        fraction = 0.5f,
                        alignmentViewId = R.id.firstView,
                        focusViewId = R.id.secondView
                    )
                )
                add(
                    SubPositionAlignment(
                        fraction = 0f,
                        alignmentViewId = R.id.secondView,
                        focusViewId = R.id.thirdView
                    )
                )
            }
        }
    
        override fun getSubPositionAlignments(): List<SubPositionAlignment> {
            return alignments
        }
    
    }
  10. Define scrollbar appearance for DpadRecyclerView

    master

    Scrollbars in DpadRecyclerView consist of a Thumb (the position indicator) and a Track (the background).

    Note: Scrollbar appearances must be defined in XML using drawable shapes. You can create reusable styles by defining these shapes in your res/drawable directory.

    <!-- Thumb example: res/drawable/scrollbar_thumb.xml -->
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <corners android:radius="4dp" />
        <solid android:color="@color/white" />
    </shape>
    
    <!-- Track example: res/drawable/scrollbar_track.xml -->
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <solid android:color="@color/black" />
    </shape>
  11. Force scrollbar track to always appear

    master

    By default, the scrollbar only appears after scrolling begins and then disappears. To force the scrollbar track to remain visible at all times, use the android:scrollbarAlwaysDrawVerticalTrack attribute.

    <com.rubensousa.dpadrecyclerview.DpadRecyclerView
        android:scrollbarAlwaysDrawVerticalTrack="true" />