Skip to content

Commit 9a91d1a

Browse files
authored
Merge pull request #161 from NordicSemiconductor/improvement/scanner
Scanner icons redesigned
2 parents 74794b1 + d85c121 commit 9a91d1a

File tree

5 files changed

+159
-43
lines changed

5 files changed

+159
-43
lines changed

scanner-ble/src/main/java/no/nordicsemi/android/common/scanner/ScannerScreen.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
package no.nordicsemi.android.common.scanner
3535

36+
import androidx.compose.foundation.layout.Arrangement
3637
import androidx.compose.foundation.layout.Column
3738
import androidx.compose.material3.Text
3839
import androidx.compose.runtime.Composable
@@ -42,11 +43,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
4243
import androidx.compose.runtime.setValue
4344
import androidx.compose.ui.Modifier
4445
import androidx.compose.ui.res.stringResource
46+
import androidx.compose.ui.unit.dp
4547
import no.nordicsemi.android.common.scanner.view.DeviceListItem
4648
import no.nordicsemi.android.common.scanner.view.FilterDialog
4749
import no.nordicsemi.android.common.scanner.view.ScannerAppBar
4850
import no.nordicsemi.android.common.scanner.view.ScannerView
4951
import no.nordicsemi.kotlin.ble.client.android.ScanResult
52+
import kotlin.time.Duration
5053

5154
/**
5255
* A scanner screen with an AppBar and a list of devices.
@@ -56,6 +59,7 @@ import no.nordicsemi.kotlin.ble.client.android.ScanResult
5659
* @param modifier Modifier to be applied to the screen.
5760
* @param state The state of the scan filter. Use this to set a static filter and dynamic filters.
5861
* @param title Composable function to display the title of the App Bar.
62+
* @param timeout The duration after which the scan will stop. Defaults to [Duration.INFINITE].
5963
* @param deviceItem Composable function to display each device in the list.
6064
*/
6165
@Composable
@@ -64,7 +68,9 @@ fun ScannerScreen(
6468
onResultSelected: (ScannerScreenResult) -> Unit,
6569
modifier: Modifier = Modifier,
6670
state: ScanFilterState = rememberFilterState(),
71+
timeout: Duration = Duration.INFINITE,
6772
title: @Composable () -> Unit = { Text(stringResource(id = R.string.scanner_screen)) },
73+
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp),
6874
deviceItem: @Composable (ScanResult) -> Unit = { scanResult ->
6975
DeviceListItem(scanResult)
7076
},
@@ -94,8 +100,10 @@ fun ScannerScreen(
94100

95101
ScannerView(
96102
state = state,
103+
timeout = timeout,
97104
onScanningStateChanged = { isScanning = it },
98105
onScanResultSelected = { onResultSelected(DeviceSelected(it)) },
106+
verticalArrangement = verticalArrangement,
99107
deviceItem = deviceItem,
100108
)
101109
}

scanner-ble/src/main/java/no/nordicsemi/android/common/scanner/view/ScanErrorView.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
package no.nordicsemi.android.common.scanner.view
3333

3434
import androidx.compose.foundation.layout.fillMaxSize
35-
import androidx.compose.foundation.layout.fillMaxWidth
3635
import androidx.compose.foundation.layout.padding
3736
import androidx.compose.runtime.Composable
3837
import androidx.compose.ui.Modifier
@@ -47,14 +46,17 @@ import no.nordicsemi.android.common.ui.view.WarningView
4746
internal fun ScanErrorView(
4847
error: String?,
4948
) {
50-
val message = error ?: "Unknown reason"
49+
var message = error ?: "Unknown reason"
50+
if (!message.endsWith(".")) {
51+
message += "."
52+
}
5153
WarningView(
5254
modifier = Modifier
5355
.fillMaxSize()
5456
.padding(16.dp),
5557
painterResource = painterResource(R.drawable.outline_bluetooth_searching_24),
5658
title = stringResource(id = R.string.scan_failed_title),
57-
hint = stringResource(id = R.string.scan_failed_info, message),
59+
hint = message,
5860
)
5961
}
6062

scanner-ble/src/main/java/no/nordicsemi/android/common/scanner/view/ScannerView.kt

Lines changed: 143 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
package no.nordicsemi.android.common.scanner.view
3535

3636
import androidx.annotation.DrawableRes
37+
import androidx.compose.foundation.background
3738
import androidx.compose.foundation.clickable
3839
import androidx.compose.foundation.layout.Arrangement
3940
import androidx.compose.foundation.layout.Box
@@ -44,7 +45,7 @@ import androidx.compose.foundation.layout.WindowInsetsSides
4445
import androidx.compose.foundation.layout.asPaddingValues
4546
import androidx.compose.foundation.layout.displayCutout
4647
import androidx.compose.foundation.layout.fillMaxSize
47-
import androidx.compose.foundation.layout.fillMaxWidth
48+
import androidx.compose.foundation.layout.heightIn
4849
import androidx.compose.foundation.layout.only
4950
import androidx.compose.foundation.layout.padding
5051
import androidx.compose.foundation.layout.union
@@ -54,6 +55,7 @@ import androidx.compose.foundation.lazy.items
5455
import androidx.compose.foundation.shape.RoundedCornerShape
5556
import androidx.compose.material3.ExperimentalMaterial3Api
5657
import androidx.compose.material3.MaterialTheme
58+
import androidx.compose.material3.OutlinedCard
5759
import androidx.compose.material3.Text
5860
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
5961
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
@@ -70,6 +72,7 @@ import androidx.compose.ui.graphics.painter.Painter
7072
import androidx.compose.ui.res.painterResource
7173
import androidx.compose.ui.res.stringResource
7274
import androidx.compose.ui.text.style.TextOverflow
75+
import androidx.compose.ui.tooling.preview.Preview
7376
import androidx.compose.ui.unit.dp
7477
import androidx.hilt.navigation.compose.hiltViewModel
7578
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -86,19 +89,25 @@ import no.nordicsemi.android.common.scanner.viewmodel.ScanningState
8689
import no.nordicsemi.android.common.scanner.viewmodel.UiState
8790
import no.nordicsemi.android.common.ui.view.CircularIcon
8891
import no.nordicsemi.android.common.ui.view.RssiIcon
92+
import no.nordicsemi.kotlin.ble.client.android.AdvertisingData
8993
import no.nordicsemi.kotlin.ble.client.android.ScanResult
90-
import kotlin.time.Duration.Companion.seconds
94+
import no.nordicsemi.kotlin.ble.client.android.preview.PreviewPeripheral
95+
import no.nordicsemi.kotlin.ble.core.Phy
96+
import no.nordicsemi.kotlin.ble.core.PrimaryPhy
97+
import kotlin.time.Duration
9198
import kotlin.uuid.ExperimentalUuidApi
9299

93100
@OptIn(ExperimentalMaterial3Api::class)
94101
@Composable
95102
fun ScannerView(
96103
onScanResultSelected: (ScanResult) -> Unit,
97104
state: ScanFilterState = rememberFilterState(),
105+
timeout: Duration = Duration.INFINITE,
98106
onScanningStateChanged: (Boolean) -> Unit = {},
107+
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp),
99108
deviceItem: @Composable (ScanResult) -> Unit = { scanResult ->
100109
DeviceListItem(scanResult)
101-
}
110+
},
102111
) {
103112
val viewModel = hiltViewModel<ScannerViewModel>()
104113
viewModel.setFilterState(state)
@@ -117,7 +126,7 @@ fun ScannerView(
117126
// This would start scanning on each orientation change,
118127
// but there is a flag set in the ViewModel to prevent that.
119128
// User needs to pull to refresh to start scanning again.
120-
viewModel.initiateScanning(timeout = 4.seconds)
129+
viewModel.initiateScanning(timeout = timeout)
121130
}
122131

123132
val pullToRefreshState = rememberPullToRefreshState()
@@ -132,6 +141,7 @@ fun ScannerView(
132141
}
133142
},
134143
state = pullToRefreshState,
144+
modifier = Modifier.background(MaterialTheme.colorScheme.background)
135145
) {
136146
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
137147
DisposableEffect(uiState.isScanning) {
@@ -144,7 +154,8 @@ fun ScannerView(
144154
isLocationRequiredAndDisabled = isLocationRequiredAndDisabled,
145155
uiState = uiState,
146156
onClick = onScanResultSelected,
147-
deviceItem = deviceItem
157+
verticalArrangement = verticalArrangement,
158+
deviceItem = deviceItem,
148159
)
149160
}
150161
}
@@ -156,6 +167,7 @@ internal fun ScannerContent(
156167
isLocationRequiredAndDisabled: Boolean,
157168
uiState: UiState,
158169
onClick: (ScanResult) -> Unit,
170+
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp),
159171
deviceItem: @Composable (ScanResult) -> Unit,
160172
) {
161173
when (uiState.scanningState) {
@@ -172,8 +184,9 @@ internal fun ScannerContent(
172184
modifier = Modifier.fillMaxSize(),
173185
contentPadding = WindowInsets.displayCutout
174186
.only(WindowInsetsSides.Horizontal)
175-
.union(WindowInsets(left = 8.dp, right = 8.dp, top = 8.dp, bottom = 8.dp))
176-
.asPaddingValues()
187+
.union(WindowInsets(left = 16.dp, right = 16.dp, top = 16.dp, bottom = 16.dp))
188+
.asPaddingValues(),
189+
verticalArrangement = verticalArrangement,
177190
) {
178191
DeviceListItems(
179192
devices = uiState.scanningState.result,
@@ -197,7 +210,7 @@ internal fun LazyListScope.DeviceListItems(
197210
items(devices) { device ->
198211
Box(
199212
modifier = Modifier
200-
.clip(RoundedCornerShape(16.dp))
213+
.clip(RoundedCornerShape(4.dp))
201214
.clickable { onScanResultSelected(device.latestScanResult) }
202215
) {
203216
deviceItem(device.latestScanResult)
@@ -233,35 +246,129 @@ fun DeviceListItem(
233246
subtitle: String,
234247
trailingContent: @Composable () -> Unit = { },
235248
) {
236-
Row(
237-
modifier = Modifier
238-
.fillMaxWidth()
239-
.padding(16.dp),
240-
verticalAlignment = Alignment.CenterVertically,
241-
horizontalArrangement = Arrangement.spacedBy(16.dp)
242-
) {
243-
peripheralIcon?.let {
244-
CircularIcon(
245-
painter = it,
246-
iconSize = 30.dp
247-
)
248-
}
249-
Column(
250-
verticalArrangement = Arrangement.spacedBy(4.dp),
251-
modifier = Modifier.weight(1.0f)
249+
OutlinedCard {
250+
Row(
251+
modifier = Modifier
252+
.heightIn(min = 80.dp)
253+
.padding(all = 16.dp),
254+
verticalAlignment = Alignment.CenterVertically,
255+
horizontalArrangement = Arrangement.spacedBy(16.dp)
252256
) {
253-
Text(
254-
text = title,
255-
color = MaterialTheme.colorScheme.onSurface,
256-
maxLines = 2,
257-
overflow = TextOverflow.Ellipsis,
258-
style = MaterialTheme.typography.titleMedium,
259-
)
260-
Text(
261-
text = subtitle,
262-
style = MaterialTheme.typography.bodyMedium,
263-
)
257+
peripheralIcon?.let {
258+
CircularIcon(
259+
painter = it,
260+
)
261+
}
262+
Column(
263+
verticalArrangement = Arrangement.spacedBy(4.dp),
264+
modifier = Modifier.weight(1.0f)
265+
) {
266+
Text(
267+
text = title,
268+
color = MaterialTheme.colorScheme.onSurface,
269+
maxLines = 2,
270+
overflow = TextOverflow.Ellipsis,
271+
style = MaterialTheme.typography.titleMedium,
272+
)
273+
Text(
274+
text = subtitle,
275+
style = MaterialTheme.typography.bodyMedium,
276+
maxLines = 1,
277+
)
278+
}
279+
trailingContent()
264280
}
265-
trailingContent()
266281
}
267282
}
283+
284+
@Preview
285+
@Composable
286+
private fun DeviceListItemPreview() {
287+
DeviceListItem(
288+
peripheralIcon = painterResource(R.drawable.outline_bluetooth_24),
289+
title = "Nordic HRM",
290+
subtitle = "12:34:56:78:9A:BC",
291+
trailingContent = {
292+
RssiIcon(rssi = -60)
293+
}
294+
)
295+
}
296+
297+
@Preview(showBackground = true)
298+
@Composable
299+
private fun ScannerContentPreview_empty() {
300+
ScannerContent(
301+
isLocationRequiredAndDisabled = false,
302+
uiState = UiState(
303+
isScanning = true,
304+
scanningState = ScanningState.Loading,
305+
),
306+
onClick = {},
307+
deviceItem = { DeviceListItem(it) }
308+
)
309+
}
310+
311+
@Preview(showBackground = true)
312+
@Composable
313+
private fun ScannerContentPreview_error() {
314+
ScannerContent(
315+
isLocationRequiredAndDisabled = false,
316+
uiState = UiState(
317+
isScanning = true,
318+
scanningState = ScanningState.Error("Internal error"),
319+
),
320+
onClick = {},
321+
deviceItem = { DeviceListItem(it) }
322+
)
323+
}
324+
325+
@Preview(showBackground = true)
326+
@Composable
327+
private fun ScannerContentPreview() {
328+
val scope = rememberCoroutineScope()
329+
330+
ScannerContent(
331+
isLocationRequiredAndDisabled = false,
332+
uiState = UiState(
333+
isScanning = true,
334+
scanningState = ScanningState.DevicesDiscovered(
335+
result = listOf(
336+
ScannedPeripheral(
337+
scanResult = ScanResult(
338+
peripheral = PreviewPeripheral(
339+
scope = scope,
340+
name = "Nordic HRM",
341+
address = "12:34:56:78:9A:BC",
342+
),
343+
isConnectable = true,
344+
advertisingData = AdvertisingData(raw = byteArrayOf(0x02, 0x01, 0x06)),
345+
rssi = -60,
346+
txPowerLevel = null,
347+
primaryPhy = PrimaryPhy.PHY_LE_1M,
348+
secondaryPhy = Phy.PHY_LE_1M,
349+
timestamp = System.currentTimeMillis(),
350+
),
351+
),
352+
ScannedPeripheral(
353+
scanResult = ScanResult(
354+
peripheral = PreviewPeripheral(
355+
scope = scope,
356+
name = "A device with a very long name",
357+
address = "00:11:22:33:44:55",
358+
),
359+
isConnectable = true,
360+
advertisingData = AdvertisingData(raw = byteArrayOf(0x02, 0x01, 0x06)),
361+
rssi = -60,
362+
txPowerLevel = null,
363+
primaryPhy = PrimaryPhy.PHY_LE_1M,
364+
secondaryPhy = Phy.PHY_LE_1M,
365+
timestamp = System.currentTimeMillis(),
366+
),
367+
)
368+
)
369+
),
370+
),
371+
onClick = {},
372+
deviceItem = { DeviceListItem(it) }
373+
)
374+
}

scanner-ble/src/main/res/values/strings.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,14 @@
3232
<resources>
3333
<string name="scanner_screen">Scanner</string>
3434
<string name="scan_empty_title">"CAN\'T SEE YOUR DEVICE?"</string>
35-
<string name="no_device_guide_info">1. Make sure the device is <b>turned on</b> and is connected to
36-
a power source.\n\n2. Make sure the appropriate firmware and SoftDevice are flashed.</string>
35+
<string name="no_device_guide_info">1. Make sure the device is connected to a power source and
36+
<b>powered on</b>.\n\n2. Make sure the appropriate firmware and SoftDevice are flashed.</string>
3737
<string name="no_device_guide_location_info">3. Location is turned off. Most Android phones
3838
require it in order to scan for Bluetooth LE devices. If you are sure your
3939
device is advertising and it doesn\'t show up here, click the button below to
4040
enable Location.</string>
4141

4242
<string name="scan_failed_title">SCANNING FAILED</string>
43-
<string name="scan_failed_info"> Scanning failed with %s</string>
4443
<string name="enable_location">Enable location</string>
4544

4645
<string name="filter_title">Filter</string>

ui/src/main/java/no/nordicsemi/android/common/ui/view/CircularIcon.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ fun CircularIcon(
6969
modifier: Modifier = Modifier,
7070
backgroundColor: Color = MaterialTheme.colorScheme.secondary,
7171
enabled: Boolean = true,
72-
size: Dp = 40.dp,
72+
size: Dp = 36.dp,
7373
iconSize: Dp = 24.dp,
7474
) {
7575
val padding = (size - iconSize) / 2

0 commit comments

Comments
 (0)