diff --git a/README.MD b/README.MD index c617395a..91fed953 100644 --- a/README.MD +++ b/README.MD @@ -17,7 +17,7 @@ Add the following dependency to your app's `build.gradle`: ```groovy implementation "com.rubensousa.dpadrecyclerview:dpadrecyclerview:$latestVersion" -// Optional: If you want to use Compose together with DpadRecyclerView +// Recommended: To use Compose together with DpadRecyclerView implementation "com.rubensousa.dpadrecyclerview:dpadrecyclerview-compose:$latestVersion" // Optional: Espresso test helpers for your instrumented tests: @@ -28,6 +28,20 @@ Check the official website for more information and recipes: https://rubensousa. ## New Features compared to Leanback's `BaseGridView` +### Layout + +- Supports grids with different span sizes +- Supports infinite/endless scrolling +- Supports reverse layout +- XML attributes for easier configuration + +### Scrolling and focus + +- Supports changing the alignment configuration smoothly +- Supports limiting the number of pending alignments +- Supports non smooth scroll changes +- Supports continuous and circular grid focus + ### Easier Compose Integration Documentation: https://rubensousa.github.io/DpadRecyclerView/compose/ @@ -61,20 +75,6 @@ class ComposeItemAdapter( } ``` -### Layout - -- Supports grids with different span sizes -- Supports infinite/endless scrolling -- Supports reverse layout -- XML attributes for easier configuration - -### Scrolling and focus - -- Supports changing the alignment configuration smoothly -- Supports limiting the number of pending alignments -- Supports non smooth scroll changes -- Supports continuous and circular grid focus - ## Sample app Nested lists: @@ -90,7 +90,7 @@ Grid with different span sizes: ## License - Copyright 2023 Rúben Sousa + 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. diff --git a/docs/compose.md b/docs/compose.md index 5dfc5dcf..974d66ca 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -6,12 +6,42 @@ The `dpadrecyclerview-compose` module contains the following: - `DpadComposeViewHolder`: ViewHolder that exposes a function to render a Composable but keeps the focus state in the View system - `RecyclerViewCompositionStrategy.DisposeOnRecycled`: a custom `ViewCompositionStrategy` that only disposes compositions when ViewHolders are recycled -!!! note - If you plan to use compose animations, check the performance during fast scrolling and consider - throttling key events using the APIs explained [here](recipes/scrolling.md#limiting-number-of-pending-alignments) +## Compose ViewHolder +### Receive focus inside Composables -## React to focus changes +Use `DpadComposeFocusViewHolder` to let your Composables receive the focus state. + +```kotlin linenums="1" +class ComposeItemAdapter( + private val onItemClick: (Int) -> Unit +) : ListAdapter>(Item.DIFF_CALLBACK) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): DpadComposeFocusViewHolder { + return DpadComposeFocusViewHolder(parent) { item -> + ItemComposable( + item = item, + onClick = { + onItemClick(item) + } + ) + } + } + + override fun onBindViewHolder( + holder: DpadComposeFocusViewHolder, + position: Int + ) { + holder.setItemState(getItem(position)) + } + +} +``` + +Then use the standard focus APIs to react to focus changes: ```kotlin linenums="1", hl_lines="13-16" @Composable @@ -44,46 +74,9 @@ fun ItemComposable( } ``` -## Handle clicks with sound - -Use `Modifier.dpadClickable` instead of `Modifier.clickable` because of this issue: -[/b/268268856](https://issuetracker.google.com/issues/268268856) +### Keep focus inside the view system - -## DpadComposeFocusViewHolder - -```kotlin linenums="1" -class ComposeItemAdapter( - private val onItemClick: (Int) -> Unit -) : ListAdapter>(Item.DIFF_CALLBACK) { - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): DpadComposeFocusViewHolder { - return DpadComposeFocusViewHolder(parent) { item -> - ItemComposable( - item = item, - onClick = { - onItemClick(item) - } - ) - } - } - - override fun onBindViewHolder( - holder: DpadComposeFocusViewHolder, - position: Int - ) { - holder.setItemState(getItem(position)) - } - -} -``` - -## DpadComposeViewHolder - -If you need to keep the focus in the View system, use this class instead. +If you want to keep the focus inside the View system, use `DpadComposeViewHolder` instead: ```kotlin linenums="1" class ComposeItemAdapter( @@ -112,6 +105,8 @@ class ComposeItemAdapter( } ``` +In this case, you receive the focus state as an input that you can pass to your Composables: + ```kotlin linenums="1" @Composable fun ItemComposable(item: Int, isFocused: Boolean) { @@ -130,4 +125,16 @@ fun ItemComposable(item: Int, isFocused: Boolean) { } ``` +## Handle clicks with sound + +Use `Modifier.dpadClickable` instead of `Modifier.clickable` because of this issue: +[/b/268268856](https://issuetracker.google.com/issues/268268856) + +## Performance optimizations + +- If you plan to use compose animations, check the performance during fast scrolling and consider throttling key events using the APIs explained [here](recipes/scrolling.md#limiting-number-of-pending-alignments) +- Consider using `dpadRecyclerView.setLayoutWhileScrollingEnabled(false)` to discard layout requests during scroll events. +This will skip unnecessary layout requests triggered by some compose animations. + + Check the sample on [Github](https://github.com/rubensousa/DpadRecyclerView/) for more examples that include simple animations. \ No newline at end of file diff --git a/docs/getting_started.md b/docs/getting_started.md index ca2dfc79..eacf8627 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -5,7 +5,7 @@ Add the following dependency to your app's `build.gradle`: ```groovy implementation "com.rubensousa.dpadrecyclerview:dpadrecyclerview:{{ dpadrecyclerview.version }}" -// Optional: If you want to use Compose together with DpadRecyclerView +// Recommended: To use Compose together with DpadRecyclerView implementation "com.rubensousa.dpadrecyclerview:dpadrecyclerview-compose:{{ dpadrecyclerview.version }}" // Optional: Espresso test helpers for your instrumented tests: @@ -27,11 +27,93 @@ Since `DpadRecyclerView` is a custom view that extends from `RecyclerView`, you !!! warning Don't set a `LayoutManager` because `DpadRecyclerView` already assigns one internally. -Follow the [official RecyclerView guides](https://developer.android.com/develop/ui/views/layout/recyclerview) to render Views on the screen or use any RecyclerView library as you would for mobile apps. +Follow the [official RecyclerView guides](https://developer.android.com/develop/ui/views/layout/recyclerview) to render Views on the screen +or use any RecyclerView library as you would for mobile apps. -## Recipes +You can also render Composables inside using the `dpadrecyclerview-compose` library. -Take a look at the sections inside "Recipes" on this website to customise `DpadRecyclerView` according to your needs. + +## Observe selection changes + +You can observe selection changes using the following: + +```kotlin linenums="1" +recyclerView.addOnViewHolderSelectedListener(object : OnViewHolderSelectedListener { + override fun onViewHolderSelected( + parent: RecyclerView, + child: RecyclerView.ViewHolder?, + position: Int, + subPosition: Int + ) {} + + override fun onViewHolderSelectedAndAligned( + parent: RecyclerView, + child: RecyclerView.ViewHolder?, + position: Int, + subPosition: Int + ) {} +}) +``` + +## Observe focus changes + +To react to focus changes, use this: + +```kotlin linenums="1" +recyclerView.addOnViewFocusedListener(object : OnViewFocusedListener { + override fun onViewFocused( + parent: RecyclerView.ViewHolder, + child: View, + ) { + // Child is now focused + } +}) +``` + +## How to use with Compose + +Check [this](compose.md) page to see more some examples with Compose + +```kotlin linenums="1", hl_lines="13-16" +@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 + } + .focusTarget() + .dpadClickable { + onClick() + }, + contentAlignment = Alignment.Center, + ) { + Text( + text = item.toString(), + color = textColor, + fontSize = 35.sp + ) + } +} +``` + +## More customizations + +Check the following recipes: + +1. [Layout](recipes/layout.md): for defining the type of layout (linear or grid) or to enable infinite carousels +2. [Spacing](recipes/spacing.md): add spacing between items +3. [Alignment](recipes/alignment.md): align items to different regions of the screen +4. [Focus](recipes/focus.md): configure how focus is handled +5. [Scrolling](recipes/scrolling.md): configure the scrolling speed ## Sample diff --git a/docs/recipes/focus.md b/docs/recipes/focus.md index c22ad2a0..9abf0e02 100644 --- a/docs/recipes/focus.md +++ b/docs/recipes/focus.md @@ -1,5 +1,22 @@ # Focus Recipes +## Observing child focus + +Use this to react to a child getting focus: + +```kotlin linenums="1" +recyclerView.addOnViewFocusedListener(object : OnViewFocusedListener { + override fun onViewFocused( + parent: RecyclerView.ViewHolder, + child: View, + ) { + // Child is now focused + } +}) +``` +!!! note + If you set this in a vertical RecyclerView that contains multiple horizontal RecyclerViews, the parent will also receive this callback + ## Disabling focus changes You might want to temporarily disable focus changes and prevent other views from being selected