Skip to content

Commit a4cbd4b

Browse files
authored
Update gesture action selection UI (#5578)
* Prepare separate screen for gesture actions - Add category to GestureAction - Adds GestureActionsView, a full screen to select the action for a gesture grouped by category * Add Compose Navigation for gestures settings - Change the main view of the gestures settings to a new Composable powered by Compose Navigation, linking the previous GesturesView (now GesturesListView) and GestureActionsView together * Add keep annotation * Review feedback - Make functions private - Update function parameters and docs for consistency and to describe actual behavior instead of desired flow - Remove unnecessary text style merge - Fix toolbar up acting differently from back - Fix actions screen top/bottom padding
1 parent b613bd5 commit a4cbd4b

File tree

8 files changed

+316
-124
lines changed

8 files changed

+316
-124
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/settings/SettingsActivity.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,7 @@ class SettingsActivity : BaseActivity() {
205205
override fun onOptionsItemSelected(item: MenuItem): Boolean {
206206
return when (item.itemId) {
207207
android.R.id.home -> {
208-
if (supportFragmentManager.backStackEntryCount > 0) {
209-
supportFragmentManager.popBackStack()
210-
} else {
211-
onBackPressedDispatcher.onBackPressed()
212-
}
208+
onBackPressedDispatcher.onBackPressed()
213209
true
214210
}
215211
else -> super.onOptionsItemSelected(item)

app/src/main/kotlin/io/homeassistant/companion/android/settings/gestures/GesturesFragment.kt

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import androidx.compose.ui.platform.ComposeView
88
import androidx.fragment.app.Fragment
99
import androidx.fragment.app.viewModels
1010
import dagger.hilt.android.AndroidEntryPoint
11-
import io.homeassistant.companion.android.common.R as commonR
12-
import io.homeassistant.companion.android.settings.gestures.views.GesturesView
11+
import io.homeassistant.companion.android.settings.gestures.views.GesturesScreen
1312
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
1413
import kotlin.getValue
1514

@@ -22,17 +21,15 @@ class GesturesFragment : Fragment() {
2221
return ComposeView(requireContext()).apply {
2322
setContent {
2423
HomeAssistantAppTheme {
25-
GesturesView(
24+
GesturesScreen(
2625
gestureActions = viewModel.gestureActions,
2726
onSetAction = viewModel::setGestureAction,
27+
onToolbarTitleChanged = { title ->
28+
activity?.title = title
29+
},
2830
)
2931
}
3032
}
3133
}
3234
}
33-
34-
override fun onResume() {
35-
super.onResume()
36-
activity?.title = getString(commonR.string.gestures)
37-
}
3835
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package io.homeassistant.companion.android.settings.gestures.views
2+
3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.height
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.lazy.LazyColumn
9+
import androidx.compose.foundation.lazy.items
10+
import androidx.compose.foundation.selection.selectable
11+
import androidx.compose.foundation.selection.selectableGroup
12+
import androidx.compose.material.MaterialTheme
13+
import androidx.compose.material.RadioButton
14+
import androidx.compose.material.Text
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.res.stringResource
19+
import androidx.compose.ui.semantics.Role
20+
import androidx.compose.ui.tooling.preview.Preview
21+
import androidx.compose.ui.unit.dp
22+
import io.homeassistant.companion.android.common.util.GestureAction
23+
import io.homeassistant.companion.android.settings.views.SettingsSubheader
24+
import io.homeassistant.companion.android.util.plus
25+
import io.homeassistant.companion.android.util.safeBottomPaddingValues
26+
27+
/**
28+
* View showing all actions for a gesture, grouped by category, and the currently
29+
* configured action. Clicking on an action calls [onActionClicked] with the action.
30+
*
31+
* @param selectedAction The current action
32+
* @param onActionClicked Called when an action is selected
33+
*/
34+
@Composable
35+
fun GestureActionsView(selectedAction: GestureAction, onActionClicked: (GestureAction) -> Unit) {
36+
val actionsGrouped = GestureAction.entries.minus(GestureAction.NONE).groupBy { it.category }
37+
LazyColumn(
38+
contentPadding = PaddingValues(vertical = 16.dp) + safeBottomPaddingValues(applyHorizontal = false),
39+
modifier = Modifier.selectableGroup(),
40+
) {
41+
item {
42+
// GestureAction.NONE is here so it's always first in the list with no header
43+
GestureActionItem(
44+
action = GestureAction.NONE,
45+
checked = selectedAction == GestureAction.NONE,
46+
onClick = { onActionClicked(GestureAction.NONE) },
47+
)
48+
}
49+
actionsGrouped.forEach { (category, actions) ->
50+
item {
51+
SettingsSubheader(stringResource(category.description))
52+
}
53+
items(actions) { action ->
54+
GestureActionItem(
55+
action = action,
56+
checked = selectedAction == action,
57+
onClick = { onActionClicked(action) },
58+
)
59+
}
60+
}
61+
}
62+
}
63+
64+
@Composable
65+
private fun GestureActionItem(action: GestureAction, checked: Boolean, onClick: () -> Unit) {
66+
// Based on androidx.compose.material.samples.RadioGroupSample
67+
Row(
68+
Modifier
69+
.fillMaxWidth()
70+
.height(56.dp)
71+
.selectable(
72+
selected = checked,
73+
onClick = onClick,
74+
role = Role.RadioButton,
75+
)
76+
.padding(horizontal = 16.dp),
77+
verticalAlignment = Alignment.CenterVertically,
78+
) {
79+
RadioButton(
80+
selected = checked,
81+
onClick = null, // Handled by parent Row
82+
)
83+
Text(
84+
text = stringResource(action.description),
85+
style = MaterialTheme.typography.body1,
86+
modifier = Modifier.padding(start = 32.dp),
87+
)
88+
}
89+
}
90+
91+
@Preview
92+
@Composable
93+
private fun PreviewGestureActionsView() {
94+
GestureActionsView(
95+
selectedAction = GestureAction.QUICKBAR_DEFAULT,
96+
onActionClicked = {},
97+
)
98+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.homeassistant.companion.android.settings.gestures.views
2+
3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.foundation.layout.padding
5+
import androidx.compose.foundation.lazy.LazyColumn
6+
import androidx.compose.foundation.lazy.items
7+
import androidx.compose.material.Text
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.res.stringResource
11+
import androidx.compose.ui.tooling.preview.Preview
12+
import androidx.compose.ui.unit.dp
13+
import io.homeassistant.companion.android.common.R
14+
import io.homeassistant.companion.android.common.util.GestureAction
15+
import io.homeassistant.companion.android.common.util.HAGesture
16+
import io.homeassistant.companion.android.settings.views.SettingsRow
17+
import io.homeassistant.companion.android.settings.views.SettingsSubheader
18+
import io.homeassistant.companion.android.util.plus
19+
import io.homeassistant.companion.android.util.safeBottomPaddingValues
20+
21+
/**
22+
* Shows all gestures with the configured action for it. Clicking on a
23+
* gesture calls [onGestureClicked] with the gesture.
24+
*
25+
* @param gestureActions User settings (gestures and the current action)
26+
* @param onGestureClicked Called when a gesture is selected
27+
*/
28+
@Composable
29+
fun GesturesListView(gestureActions: Map<HAGesture, GestureAction>, onGestureClicked: (HAGesture) -> Unit) {
30+
LazyColumn(
31+
contentPadding = PaddingValues(vertical = 16.dp) + safeBottomPaddingValues(applyHorizontal = false),
32+
) {
33+
item {
34+
Text(
35+
text = stringResource(R.string.gestures_description),
36+
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp),
37+
)
38+
}
39+
40+
val gesturesGrouped = HAGesture.entries.groupBy { it.direction }
41+
gesturesGrouped.forEach { (direction, gestures) ->
42+
item {
43+
SettingsSubheader(stringResource(direction.description))
44+
}
45+
items(gestures) { gesture ->
46+
val action = gestureActions[gesture]
47+
SettingsRow(
48+
primaryText = stringResource(gesture.pointers.description),
49+
secondaryText = action?.let { stringResource(it.description) } ?: "",
50+
icon = null,
51+
onClicked = { onGestureClicked(gesture) },
52+
)
53+
}
54+
}
55+
}
56+
}
57+
58+
@Preview
59+
@Composable
60+
private fun PreviewGesturesListView() {
61+
GesturesListView(
62+
gestureActions = HAGesture.entries.associateWith { GestureAction.NONE },
63+
onGestureClicked = { _ -> },
64+
)
65+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.homeassistant.companion.android.settings.gestures.views
2+
3+
import androidx.compose.animation.core.tween
4+
import androidx.compose.animation.fadeIn
5+
import androidx.compose.animation.fadeOut
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.LaunchedEffect
8+
import androidx.compose.ui.platform.LocalContext
9+
import androidx.navigation.NavDestination.Companion.hasRoute
10+
import androidx.navigation.compose.NavHost
11+
import androidx.navigation.compose.composable
12+
import androidx.navigation.compose.rememberNavController
13+
import androidx.navigation.toRoute
14+
import io.homeassistant.companion.android.common.R
15+
import io.homeassistant.companion.android.common.util.GestureAction
16+
import io.homeassistant.companion.android.common.util.HAGesture
17+
import kotlinx.serialization.Serializable
18+
19+
@Serializable private data object GesturesRoute
20+
21+
@Serializable private data class ActionsRoute(val gesture: HAGesture)
22+
23+
/**
24+
* Main Composable for gesture settings, allowing navigation to:
25+
* - Gestures list (a list of all gestures with their action)
26+
* - Gesture actions (when a gesture is selected, to change the action for it)
27+
*
28+
* @param gestureActions User settings (gestures and the current action)
29+
* @param onSetAction Called when the action for a gesture should be changed
30+
* @param onToolbarTitleChanged Called when the screen changes, to update the
31+
* Activity's Toolbar title
32+
*/
33+
@Composable
34+
fun GesturesScreen(
35+
gestureActions: Map<HAGesture, GestureAction>,
36+
onSetAction: (HAGesture, GestureAction) -> Unit,
37+
onToolbarTitleChanged: (String) -> Unit,
38+
) {
39+
val context = LocalContext.current
40+
val navController = rememberNavController()
41+
NavHost(
42+
navController = navController,
43+
startDestination = GesturesRoute,
44+
// The apps' settings fragments do not have a transition. Compose Navigation forces a transition when
45+
// using predictive back linked to swipe progress, so we cannot set this to no transition as the UI
46+
// will look broken otherwise. To make the difference between fragments and Compose less jarring,
47+
// use a very short crossfade.
48+
enterTransition = { fadeIn(tween(200)) },
49+
exitTransition = { fadeOut(tween(200)) },
50+
) {
51+
composable<GesturesRoute> {
52+
GesturesListView(
53+
gestureActions = gestureActions,
54+
onGestureClicked = { gesture ->
55+
navController.navigate(ActionsRoute(gesture))
56+
},
57+
)
58+
}
59+
composable<ActionsRoute> { backStackEntry ->
60+
val action: ActionsRoute = backStackEntry.toRoute()
61+
GestureActionsView(
62+
selectedAction = gestureActions.getOrDefault(action.gesture, GestureAction.NONE),
63+
onActionClicked = { newAction ->
64+
onSetAction(action.gesture, newAction)
65+
navController.popBackStack(route = GesturesRoute, inclusive = false)
66+
},
67+
)
68+
}
69+
}
70+
LaunchedEffect(Unit) {
71+
navController.currentBackStackEntryFlow.collect { backStackEntry ->
72+
if (backStackEntry.destination.hasRoute(route = GesturesRoute::class)) {
73+
onToolbarTitleChanged(context.getString(R.string.gestures))
74+
} else if (backStackEntry.destination.hasRoute(ActionsRoute::class)) {
75+
val action: ActionsRoute = backStackEntry.toRoute()
76+
onToolbarTitleChanged(context.getString(action.gesture.fullDescription))
77+
}
78+
}
79+
}
80+
}

app/src/main/kotlin/io/homeassistant/companion/android/settings/gestures/views/GesturesView.kt

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)