diff --git a/docs/changelog.md b/docs/changelog.md index 209a93ba..7968d8d7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,37 @@ ## Version 1.0.0 +### 1.0.0-beta03 + +2023-05-03 + +#### Dependency updates + +- Updated compose-ui to version `1.4.2` + +#### New Features + +- Added `getSpanSizeLookup()` to `DpadRecyclerView` +- Added `onViewHolderSelectedAndAligned` to `DpadViewHolder` + +#### Compose + +- Added `DpadAbstractComposeViewHolder` to allow subclasses to get access to focus changes. Check [Compose interoperability](compose.md) for more information. + + +#### Testing + +See the documentation [here](testing.md) + +- Added `KeyEvents.back()` to easily press back key events +- Added `DpadRecyclerViewActions.scrollTo` and `DpadRecyclerViewActions.scrollToHolder` to scroll to specific ViewHolders using KeyEvents. +- Added `DpadViewAssertions` for asserting focus states: + - `DpadViewAssertions.hasFocus()` + - `DpadViewAssertions.doesNotHaveFocus()` + - `DpadViewAssertions.isFocused()` + - `DpadViewAssertions.isNotFocused()` + + ### 1.0.0-beta02 2023-04-18 diff --git a/docs/compose.md b/docs/compose.md index 11c42184..48417a6e 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -1,29 +1,32 @@ # Compose interoperability -The `dpadrecyclerview-compose` module contains a `DpadComposeViewHolder` -that you can extend to easily render composables in your `RecyclerView`. +The `dpadrecyclerview-compose` module contains the following: +- `DpadAbstractComposeViewHolder`: ViewHolder that exposes a `Content` function to render a Composable +- `DpadComposeViewHolder`: simple implementation of `DpadAbstractComposeViewHolder` that forwards a lambda to the `Content` function and handles clicks + +You can use these to easily render composables in your `RecyclerView`. + +The focus is kept in the `itemView` and not actually sent to the Composables inside due to these issues: + +1. Focus is not sent correctly from Views to Composables: [b/268248352](https://issuetracker.google.com/issues/268248352) +2. Clicking on a focused Composable does not trigger the standard audio feedback: [b/268268856](https://issuetracker.google.com/issues/268268856) + +!!! 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) + + +## DpadComposeViewHolder Example: `ItemComposable` that should render a text and different colors based on the focus state ```kotlin linenums="1" @Composable -fun ItemComposable( - item: Int, - isFocused: Boolean, - modifier: Modifier = Modifier, -) { - val backgroundColor = if (isFocused) { - Color.White - } else { - Color.Black - } - val textColor = if (isFocused) { - Color.Black - } else { - Color.White - } +fun ItemComposable(item: Int, isFocused: Boolean) { + val backgroundColor = if (isFocused) Color.White else Color.Black + val textColor = if (isFocused) Color.Black else Color.White Box( - modifier = modifier.background(backgroundColor), + modifier = Modifier.background(backgroundColor), contentAlignment = Alignment.Center, ) { Text( @@ -35,9 +38,6 @@ fun ItemComposable( } ``` -To render `ItemComposable` in a `RecyclerView.Adapter`, just use `DpadComposeViewHolder`: - - ```kotlin linenums="1" class ComposeItemAdapter( private val onItemClick: (Int) -> Unit @@ -49,25 +49,20 @@ class ComposeItemAdapter( ): DpadComposeViewHolder { return DpadComposeViewHolder( parent, - composable = { item, isFocused, _ -> - ItemComposable( - modifier = Modifier - .width(120.dp) - .aspectRatio(9 / 16f), - item = item, - isFocused = isFocused - ) - }, onClick = onItemClick - ) + ) { item, isFocused, isSelected -> + ItemComposable(item, isFocused) + } } - override fun onBindViewHolder(holder: DpadComposeViewHolder, position: Int) { + override fun onBindViewHolder( + holder: DpadComposeViewHolder, + position: Int + ) { holder.setItemState(getItem(position)) } } - ``` New compositions will be triggered whenever the following happens: @@ -76,4 +71,67 @@ New compositions will be triggered whenever the following happens: - Focus state changes - Selection state changes +## Dpad AbstractComposeViewHolder + +Extending from this class directly gives you more flexibility for customizations: + +```kotlin linenums="1" +class ComposeItemAdapter( + private val onItemClick: (Int) -> Unit +) : ListAdapter(Item.DIFF_CALLBACK) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ComposeItemViewHolder { + return ComposeItemViewHolder(parent, onItemClick) + } + + override fun onBindViewHolder( + holder: ComposeItemViewHolder, + position: Int + ) { + holder.setItemState(getItem(position)) + } + + override fun onViewRecycled(holder: ComposeItemViewHolder) { + holder.onRecycled() + } +} +``` + +```kotlin linenums="1" +class ComposeItemViewHolder( + parent: ViewGroup, + onClick: (Int) -> Unit +) : DpadAbstractComposeViewHolder(parent) { + + private val itemAnimator = ItemAnimator(itemView) + + init { + itemView.setOnClickListener { + getItem()?.let(onItemClick) + } + } + + @Composable + override fun Content(item: Int, isFocused: Boolean, isSelected: Boolean) { + ItemComposable(item, isFocused) + } + + override fun onFocusChanged(hasFocus: Boolean) { + if (hasFocus) { + itemAnimator.startFocusGainAnimation() + } else { + itemAnimator.startFocusLossAnimation() + } + } + + fun onRecycled() { + itemAnimator.cancel() + } + +} +``` + 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/testing.md b/docs/testing.md index dc0a6a61..0c625fb2 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,12 +4,15 @@ The module `dpadrecyclerview-testing` includes useful Espresso helpers for UI te Check the [Espresso training guide](https://developer.android.com/training/testing/espresso/basics) if you're not familiar with this testing framework. +The official project sample over at Github also contains example UI tests [here](https://github.com/rubensousa/DpadRecyclerView/blob/master/sample/src/androidTest/java/com/rubensousa/dpadrecyclerview/sample/test/SampleTests.kt) + ## Dispatching key events `KeyEvents` provides some utility methods for easily pressing keys a certain amount of times. ```kotlin -KeyEvents.pressKey(KeyEvent.KEYCODE_DPAD_CENTER) +KeyEvents.click() +KeyEvents.back() KeyEvents.pressDown(times = 5) // 50 ms between each key press KeyEvents.pressUp(times = 5, delay = 50) @@ -34,11 +37,14 @@ class UiTest() { `DpadViewActions` contains the following: * `getViewBounds`: returns the bounds of a view in the coordinate-space of the root view of the window +* `getRelativeViewBounds`: returns the bounds of a view in the coordinate-space of the parent view * `clearFocus`: clears the focus of a view if something else can take focus in its place * `requestFocus`: requests focus of a view `DpadRecyclerViewActions` contains the following: +* `scrollTo`: scrolls to a specific itemView using KeyEvents +* `scrollToHolder`: scrolls to a specific ViewHolder using KeyEvents * `selectLastPosition `: selects the last position of the adapter * `selectPosition `: selects a given position or position-subPosition pair * `selectSubPosition `: selects a given subPosition for the current selected position @@ -50,19 +56,27 @@ class UiTest() { Example: ```kotlin -Espresso.onView(withId(R.id.recyclerView)).perform(DpadRecyclerViewActions.selectPosition(5)) +Espresso.onView(withId(R.id.recyclerView)) + .perform(DpadRecyclerViewActions.scrollTo( + hasDescendant(withText("Some title")) + )) ``` ## ViewAssertions -`DpadRecyclerViewAssertions` contains the following: +`DpadRecyclerViewAssertions`: -* `isNotFocused`: checks if the `DpadRecyclerView` is not focused * `isFocused`: checks if a ViewHolder at a given position is focused * `isSelected`: checks if a ViewHolder at a given position or position-subPosition pair is selected +`DpadViewAssertions`: + +* `isFocused` and `isNotFocused` : checks if a View is focused +* `hasFocus` and `doesNotHaveFocus`: checks if a View or one of its descendants has focus + Example: ```kotlin -Espresso.onView(withId(R.id.recyclerView)).assert(DpadRecyclerViewAssertions.isFocused(position = 5)) +Espresso.onView(withId(R.id.recyclerView)) + .assert(DpadRecyclerViewAssertions.isFocused(position = 5)) ``` \ No newline at end of file diff --git a/dpadrecyclerview-compose/gradle.properties b/dpadrecyclerview-compose/gradle.properties index 0b88412a..69c2dea4 100644 --- a/dpadrecyclerview-compose/gradle.properties +++ b/dpadrecyclerview-compose/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -LIBRARY_VERSION=1.0.0-beta02 +LIBRARY_VERSION=1.0.0-beta03 LIBRARY_GROUP=com.rubensousa.dpadrecyclerview LIBRARY_ARTIFACT=dpadrecyclerview-compose # POM info diff --git a/dpadrecyclerview-testing/gradle.properties b/dpadrecyclerview-testing/gradle.properties index cc60ebb9..19f0c8fe 100644 --- a/dpadrecyclerview-testing/gradle.properties +++ b/dpadrecyclerview-testing/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -LIBRARY_VERSION=1.0.0-beta02 +LIBRARY_VERSION=1.0.0-beta03 LIBRARY_GROUP=com.rubensousa.dpadrecyclerview LIBRARY_ARTIFACT=dpadrecyclerview-testing # POM info diff --git a/dpadrecyclerview/gradle.properties b/dpadrecyclerview/gradle.properties index 6121edcc..b8fa1fd9 100644 --- a/dpadrecyclerview/gradle.properties +++ b/dpadrecyclerview/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -LIBRARY_VERSION=1.0.0-beta02 +LIBRARY_VERSION=1.0.0-beta03 LIBRARY_GROUP=com.rubensousa.dpadrecyclerview LIBRARY_ARTIFACT=dpadrecyclerview # POM info diff --git a/mkdocs.yml b/mkdocs.yml index eb34fa9f..d28d431f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,7 +24,7 @@ theme: extra: dpadrecyclerview: - version: '1.0.0-beta02' + version: '1.0.0-beta03' social: - icon: 'fontawesome/brands/github' link: 'https://github.com/rubensousa/DpadRecyclerView'