Skip to content

Commit

Permalink
Merge pull request #201 from rubensousa/release-1.3.0-alpha01
Browse files Browse the repository at this point in the history
Release 1.3.0 alpha01
  • Loading branch information
rubensousa authored Mar 17, 2024
2 parents 7d8e5e3 + 313c867 commit c462f4f
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 97 deletions.
16 changes: 13 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-python@v4
with:
python-version: 3.x
- uses: actions/cache@v2

- uses: actions/setup-java@v3
with:
key: ${{ github.ref }}
path: .cache
distribution: 'zulu'
java-version: 17

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build docs
run: |
./gradlew dokkaHtmlMultiModule
mv ./build/dokka/htmlMultiModule docs/api
- name: Install dependencies
run: |
Expand Down
16 changes: 13 additions & 3 deletions .github/workflows/docs_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-python@v4
with:
python-version: 3.x
- uses: actions/cache@v2

- uses: actions/setup-java@v3
with:
key: ${{ github.ref }}
path: .cache
distribution: 'zulu'
java-version: 17

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build docs
run: |
./gradlew dokkaHtmlMultiModule
mv ./build/dokka/htmlMultiModule docs/api
- name: Install dependencies
run: |
Expand Down
37 changes: 35 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -42,6 +42,39 @@ Check the official website for more information and recipes: https://rubensousa.
- Supports non smooth scroll changes
- Supports continuous and circular grid focus

### Easier Compose Integration

Documentation: https://rubensousa.github.io/DpadRecyclerView/compose/

```kotlin
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))
}

}
```

## Sample app

Nested lists:
Expand All @@ -57,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.
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ plugins {
alias libs.plugins.kotlin.android apply false
alias libs.plugins.kotlin.kover apply false
alias libs.plugins.androidx.navigation.safeargs apply false
id 'org.jetbrains.dokka' version '1.9.20'
id 'org.jetbrains.kotlinx.binary-compatibility-validator' version "0.14.0"
}

Expand Down
25 changes: 25 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## Version 1.3.0

### 1.3.0-alpha01

2024-03-17

#### New Features

- Added `DpadComposeFocusViewHolder` that allows sending the focus state down to Composables ([#193](https://github.com/rubensousa/DpadRecyclerView/issues/193))
- Added `Modifier.dpadClickable` for playing the click sound after clicking on a Composable. Fix for: ([/b/268268856](https://issuetracker.google.com/issues/268268856))
- Allow skipping layout requests during scroll with `setLayoutWhileScrollingEnabled(false)` ([#196](https://github.com/rubensousa/DpadRecyclerView/issues/196))
- New `addOnViewFocusedListener` to observe focus changes independently from selection changes. ([#197](https://github.com/rubensousa/DpadRecyclerView/issues/197))

#### API Changes

- `DpadAbstractComposeViewHolder` is now removed. Replace it with either `DpadComposeFocusViewHolder` or `DpadComposeViewHolder`.

## Version 1.2.0

### 1.2.0
Expand All @@ -8,6 +25,14 @@

- No changes since 1.2.0-rc01

#### Important changes since 1.1.0

- Added new `RecyclerViewCompositionStrategy.DisposeOnRecycled` for compose interop
to re-use compositions when views are detached and attached from the window again.
- Added new `setSelectedSubPosition` that allows passing a callback for the target alignment ([#43](https://github.com/rubensousa/DpadRecyclerView/issues/43))
- Added support for scrollbars
- Added `DpadScroller` for scrolling without any alignment. Typical use case is for long text displays (terms & conditions and consent pages).

### 1.2.0-rc01

2024-02-03
Expand Down
147 changes: 75 additions & 72 deletions docs/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,67 @@

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
- `DpadComposeFocusViewHolder`: ViewHolder that exposes a function to render a Composable and sends the focus directly to Composables.
- `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

You can use these to easily render composables in your `RecyclerView`.
## Compose ViewHolder

The focus is kept in the `itemView` and not actually sent to the Composables inside due to these issues:
### Receive focus inside Composables

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)
Use `DpadComposeFocusViewHolder` to let your Composables receive the focus state.

!!! 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)
```kotlin linenums="1"
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)
}
)
}
}

## DpadComposeViewHolder
Example: `ItemComposable` that should render a text and different colors based on the focus state
override fun onBindViewHolder(
holder: DpadComposeFocusViewHolder<Int>,
position: Int
) {
holder.setItemState(getItem(position))
}

}
```

```kotlin linenums="1"
Then use the standard focus APIs to react to focus changes:

```kotlin linenums="1", hl_lines="13-16"
@Composable
fun ItemComposable(item: Int, isFocused: Boolean) {
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),
modifier = modifier
.background(backgroundColor)
.onFocusChanged { focusState ->
isFocused = focusState.hasFocus
}
.focusTarget()
.dpadClickable {
onClick()
},
contentAlignment = Alignment.Center,
) {
Text(
Expand All @@ -38,6 +74,10 @@ fun ItemComposable(item: Int, isFocused: Boolean) {
}
```

### Keep focus inside the view system

If you want to keep the focus inside the View system, use `DpadComposeViewHolder` instead:

```kotlin linenums="1"
class ComposeItemAdapter(
private val onItemClick: (Int) -> Unit
Expand All @@ -50,7 +90,7 @@ class ComposeItemAdapter(
return DpadComposeViewHolder(
parent,
onClick = onItemClick
) { item, isFocused, isSelected ->
) { item, isFocused ->
ItemComposable(item, isFocused)
}
}
Expand All @@ -65,73 +105,36 @@ class ComposeItemAdapter(
}
```

New compositions will be triggered whenever the following happens:

- New item is bound in `onBindViewHolder`
- Focus state changes
- Selection state changes

## Dpad AbstractComposeViewHolder

Extending from this class directly gives you more flexibility for customizations:
In this case, you receive the focus state as an input that you can pass to your Composables:

```kotlin linenums="1"
class ComposeItemAdapter(
private val onItemClick: (Int) -> Unit
) : ListAdapter<Int, ComposeItemViewHolder>(Item.DIFF_CALLBACK) {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ComposeItemViewHolder {
return ComposeItemViewHolder(parent, onItemClick)
}

override fun onBindViewHolder(
holder: ComposeItemViewHolder,
position: Int
@Composable
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),
contentAlignment = Alignment.Center,
) {
holder.setItemState(getItem(position))
}

override fun onViewRecycled(holder: ComposeItemViewHolder) {
holder.onRecycled()
Text(
text = item.toString(),
color = textColor,
fontSize = 35.sp
)
}
}
```

```kotlin linenums="1"
class ComposeItemViewHolder(
parent: ViewGroup,
onClick: (Int) -> Unit
) : DpadAbstractComposeViewHolder<Int>(parent) {
## Handle clicks with sound

private val itemAnimator = ItemAnimator(itemView)
Use `Modifier.dpadClickable` instead of `Modifier.clickable` because of this issue:
[/b/268268856](https://issuetracker.google.com/issues/268268856)

init {
itemView.setOnClickListener {
getItem()?.let(onItemClick)
}
}
## Performance optimizations

@Composable
override fun Content(item: Int, isFocused: Boolean, isSelected: Boolean) {
ItemComposable(item, isFocused)
}
- 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.

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.
Loading

0 comments on commit c462f4f

Please sign in to comment.