|
1 | 1 | package no.nordicsemi.android.nrftoolbox.view |
2 | 2 |
|
3 | | -import androidx.annotation.DrawableRes |
4 | | -import androidx.annotation.StringRes |
5 | | -import androidx.compose.foundation.Image |
6 | 3 | import androidx.compose.foundation.layout.Arrangement |
7 | 4 | import androidx.compose.foundation.layout.Column |
| 5 | +import androidx.compose.foundation.layout.FlowRow |
8 | 6 | import androidx.compose.foundation.layout.Row |
9 | 7 | import androidx.compose.foundation.layout.fillMaxWidth |
| 8 | +import androidx.compose.foundation.layout.height |
10 | 9 | import androidx.compose.foundation.layout.padding |
11 | 10 | import androidx.compose.foundation.layout.size |
| 11 | +import androidx.compose.foundation.layout.wrapContentHeight |
| 12 | +import androidx.compose.material3.Badge |
| 13 | +import androidx.compose.material3.ElevatedAssistChip |
| 14 | +import androidx.compose.material3.FilterChip |
| 15 | +import androidx.compose.material3.Icon |
| 16 | +import androidx.compose.material3.ListItem |
| 17 | +import androidx.compose.material3.LocalContentColor |
12 | 18 | import androidx.compose.material3.MaterialTheme |
13 | 19 | import androidx.compose.material3.OutlinedCard |
| 20 | +import androidx.compose.material3.SuggestionChip |
14 | 21 | import androidx.compose.material3.Text |
15 | 22 | import androidx.compose.runtime.Composable |
16 | 23 | import androidx.compose.ui.Alignment |
17 | 24 | import androidx.compose.ui.Modifier |
18 | | -import androidx.compose.ui.graphics.ColorFilter |
19 | | -import androidx.compose.ui.graphics.vector.ImageVector |
| 25 | +import androidx.compose.ui.graphics.Color |
| 26 | +import androidx.compose.ui.graphics.painter.Painter |
20 | 27 | import androidx.compose.ui.res.painterResource |
21 | 28 | import androidx.compose.ui.res.stringResource |
22 | 29 | import androidx.compose.ui.tooling.preview.Preview |
23 | 30 | import androidx.compose.ui.unit.dp |
| 31 | +import no.nordicsemi.android.common.theme.NordicTheme |
24 | 32 | import no.nordicsemi.android.nrftoolbox.R |
| 33 | +import kotlin.math.absoluteValue |
25 | 34 |
|
26 | 35 | @Composable |
27 | 36 | internal fun FeatureButton( |
28 | | - @DrawableRes iconId: Int, |
29 | | - @StringRes description: Int, |
30 | | - profileNames: List<String> = listOf(stringResource(description)), |
| 37 | + icon: Painter, |
| 38 | + description: String, |
| 39 | + profileNames: List<String> = listOf(description), |
31 | 40 | deviceName: String?, |
32 | 41 | deviceAddress: String, |
33 | 42 | onClick: () -> Unit |
34 | 43 | ) { |
35 | | - OutlinedCard(onClick = onClick, modifier = Modifier.fillMaxWidth()) { |
36 | | - Row( |
37 | | - modifier = Modifier |
38 | | - .padding(16.dp) |
39 | | - .fillMaxWidth(), |
40 | | - verticalAlignment = Alignment.CenterVertically, |
41 | | - horizontalArrangement = Arrangement.spacedBy(8.dp) |
42 | | - ) { |
43 | | - Image( |
44 | | - painter = painterResource(iconId), |
45 | | - contentDescription = stringResource(id = description), |
46 | | - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), |
47 | | - modifier = Modifier |
48 | | - .size(40.dp) |
49 | | - ) |
50 | | - |
51 | | - Column( |
52 | | - verticalArrangement = Arrangement.spacedBy(8.dp), |
53 | | - horizontalAlignment = Alignment.CenterHorizontally, |
54 | | - ) { |
55 | | - Text( |
56 | | - modifier = Modifier.fillMaxWidth(), |
57 | | - text = deviceName ?: stringResource(R.string.unknown_device), |
58 | | - style = MaterialTheme.typography.titleMedium, |
59 | | - ) |
60 | | - Text( |
61 | | - modifier = Modifier.fillMaxWidth(), |
62 | | - text = profileNames.joinToString(", "), |
63 | | - style = MaterialTheme.typography.labelMedium, |
64 | | - color = MaterialTheme.colorScheme.onSurfaceVariant |
65 | | - ) |
66 | | - |
67 | | - |
68 | | - Text( |
69 | | - modifier = Modifier.fillMaxWidth(), |
70 | | - text = deviceAddress, |
71 | | - style = MaterialTheme.typography.labelMedium, |
72 | | - color = MaterialTheme.colorScheme.onSurfaceVariant |
| 44 | + OutlinedCard( |
| 45 | + onClick = onClick, |
| 46 | + ) { |
| 47 | + ListItem( |
| 48 | + headlineContent = { Text(deviceName ?: stringResource(R.string.unknown_device)) }, |
| 49 | + supportingContent = { |
| 50 | + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { |
| 51 | + Text(deviceAddress) |
| 52 | + FlowRow( |
| 53 | + horizontalArrangement = Arrangement.spacedBy(4.dp), |
| 54 | + maxLines = 1, |
| 55 | + ) { |
| 56 | + profileNames.forEach { |
| 57 | + Badge( |
| 58 | + containerColor = vibrantColorFromString(it), |
| 59 | + contentColor = MaterialTheme.colorScheme.onPrimary, |
| 60 | + ) { |
| 61 | + Text( |
| 62 | + text = it, |
| 63 | + modifier = Modifier.padding(1.dp), |
| 64 | + ) |
| 65 | + } |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | + }, |
| 70 | + leadingContent = { |
| 71 | + Icon( |
| 72 | + painter = icon, |
| 73 | + contentDescription = description, |
| 74 | + tint = MaterialTheme.colorScheme.primary, |
| 75 | + modifier = Modifier.size(24.dp), |
73 | 76 | ) |
74 | 77 | } |
75 | | - } |
| 78 | + ) |
76 | 79 | } |
77 | 80 | } |
78 | 81 |
|
79 | | -@Preview |
| 82 | +@Preview(heightDp = 100) |
80 | 83 | @Composable |
81 | 84 | private fun FeatureButtonPreview() { |
82 | | - FeatureButton( |
83 | | - R.drawable.ic_csc, |
84 | | - R.string.csc_module_full, |
85 | | - listOf("Cycling Speed and Cadence", "Cycling Speed Sensor"), |
86 | | - "Testing peripheral", |
87 | | - deviceAddress = "AA:BB:CC:DD:EE:FF", |
88 | | - ) { } |
| 85 | + NordicTheme { |
| 86 | + FeatureButton( |
| 87 | + icon = painterResource(R.drawable.ic_csc), |
| 88 | + description = stringResource(R.string.csc_module_full), |
| 89 | + profileNames = listOf("Cycling Speed and Cadence", "Battery", "Heart Rate", "Blood Pressure"), |
| 90 | + deviceName = "Testing peripheral", |
| 91 | + deviceAddress = "AA:BB:CC:DD:EE:FF", |
| 92 | + onClick = {} |
| 93 | + ) |
| 94 | + } |
89 | 95 | } |
90 | 96 |
|
91 | | -@Composable |
92 | | -internal fun FeatureButton( |
93 | | - iconId: ImageVector, |
94 | | - @StringRes description: Int, |
95 | | - profileNames: List<String> = listOf(stringResource(description)), |
96 | | - deviceName: String?, |
97 | | - deviceAddress: String, |
98 | | - onClick: () -> Unit |
99 | | -) { |
100 | | - OutlinedCard(onClick = onClick, modifier = Modifier.fillMaxWidth()) { |
101 | | - Row( |
102 | | - modifier = Modifier |
103 | | - .padding(16.dp) |
104 | | - .fillMaxWidth(), |
105 | | - verticalAlignment = Alignment.Top, |
106 | | - horizontalArrangement = Arrangement.spacedBy(8.dp) |
107 | | - ) { |
108 | | - Image( |
109 | | - imageVector = iconId, |
110 | | - contentDescription = deviceAddress, |
111 | | - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), |
112 | | - modifier = Modifier |
113 | | - .size(40.dp) |
114 | | - ) |
| 97 | +private fun vibrantColorFromString(input: String): Color { |
| 98 | + // Hash → 0..360 for hue |
| 99 | + val hue = (input.hashCode().absoluteValue % 360).toFloat() |
115 | 100 |
|
116 | | - Column( |
117 | | - verticalArrangement = Arrangement.spacedBy(8.dp), |
118 | | - horizontalAlignment = Alignment.CenterHorizontally, |
119 | | - ) { |
120 | | - Text( |
121 | | - modifier = Modifier.fillMaxWidth(), |
122 | | - text = deviceName ?: stringResource(R.string.unknown_device), |
123 | | - style = MaterialTheme.typography.titleMedium, |
124 | | - ) |
| 101 | + val saturation = 0.65f // vibrant |
| 102 | + val lightness = 0.45f // not too dark/light |
125 | 103 |
|
126 | | - Text( |
127 | | - modifier = Modifier.fillMaxWidth(), |
128 | | - text = profileNames.joinToString(", "), |
129 | | - style = MaterialTheme.typography.labelMedium, |
130 | | - color = MaterialTheme.colorScheme.onSurfaceVariant |
131 | | - ) |
| 104 | + return hslToColor(hue, saturation, lightness) |
| 105 | +} |
132 | 106 |
|
133 | | - Text( |
134 | | - modifier = Modifier.fillMaxWidth(), |
135 | | - text = deviceAddress, |
136 | | - style = MaterialTheme.typography.labelMedium, |
137 | | - color = MaterialTheme.colorScheme.onSurfaceVariant |
138 | | - ) |
139 | | - } |
140 | | - } |
| 107 | +/** |
| 108 | + * HSL → Color conversion for Jetpack Compose. |
| 109 | + */ |
| 110 | +private fun hslToColor(h: Float, s: Float, l: Float): Color { |
| 111 | + val c = (1 - kotlin.math.abs(2 * l - 1)) * s |
| 112 | + val x = c * (1 - kotlin.math.abs((h / 60) % 2 - 1)) |
| 113 | + val m = l - c / 2 |
| 114 | + |
| 115 | + val (r1, g1, b1) = when { |
| 116 | + h < 60 -> Triple(c, x, 0f) |
| 117 | + h < 120 -> Triple(x, c, 0f) |
| 118 | + h < 180 -> Triple(0f, c, x) |
| 119 | + h < 240 -> Triple(0f, x, c) |
| 120 | + h < 300 -> Triple(x, 0f, c) |
| 121 | + else -> Triple(c, 0f, x) |
141 | 122 | } |
| 123 | + |
| 124 | + return Color( |
| 125 | + red = r1 + m, |
| 126 | + green = g1 + m, |
| 127 | + blue = b1 + m |
| 128 | + ) |
142 | 129 | } |
0 commit comments