Skip to content

Commit 6135aea

Browse files
committed
perf: a11y semantics
1 parent 998a6e1 commit 6135aea

23 files changed

+448
-182
lines changed

app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import androidx.compose.material3.LocalTextStyle
2525
import androidx.compose.material3.MaterialTheme
2626
import androidx.compose.material3.OutlinedTextField
2727
import androidx.compose.material3.Scaffold
28-
import androidx.compose.material3.Switch
2928
import androidx.compose.material3.Text
3029
import androidx.compose.material3.TextButton
3130
import androidx.compose.material3.TopAppBarDefaults
@@ -73,6 +72,7 @@ import li.songe.gkd.ui.component.CustomIconButton
7372
import li.songe.gkd.ui.component.CustomOutlinedTextField
7473
import li.songe.gkd.ui.component.PerfIcon
7574
import li.songe.gkd.ui.component.PerfIconButton
75+
import li.songe.gkd.ui.component.PerfSwitch
7676
import li.songe.gkd.ui.component.PerfTopAppBar
7777
import li.songe.gkd.ui.component.SettingItem
7878
import li.songe.gkd.ui.component.TextSwitch
@@ -325,7 +325,7 @@ fun AdvancedPage() {
325325
PerfIcon(
326326
modifier = Modifier
327327
.clip(MaterialTheme.shapes.extraSmall)
328-
.clickable(onClick = throttle {
328+
.clickable(onClickLabel = "打开 Shizuku 状态弹窗", onClick = throttle {
329329
showShizukuState = true
330330
})
331331
.iconTextSize(textStyle = MaterialTheme.typography.titleSmall),
@@ -358,9 +358,11 @@ fun AdvancedPage() {
358358
)
359359
}
360360
},
361-
) {
362-
mainVm.switchEnableShizuku(it)
363-
}
361+
onCheckedChange = {
362+
mainVm.switchEnableShizuku(it)
363+
},
364+
onClick = null,
365+
)
364366

365367
val server by HttpService.httpServerFlow.collectAsState()
366368
val httpServerRunning = server != null
@@ -415,7 +417,7 @@ fun AdvancedPage() {
415417
}
416418
}
417419
}
418-
Switch(
420+
PerfSwitch(
419421
checked = httpServerRunning,
420422
onCheckedChange = throttle(fn = vm.viewModelScope.launchAsFn<Boolean> {
421423
if (it) {
@@ -433,6 +435,7 @@ fun AdvancedPage() {
433435
title = "服务端口",
434436
subtitle = store.httpServerPort.toString(),
435437
imageVector = PerfIcon.Edit,
438+
onClickLabel = "编辑服务端口",
436439
onClick = {
437440
showEditPortDlg = true
438441
}
@@ -441,12 +444,13 @@ fun AdvancedPage() {
441444
TextSwitch(
442445
title = "清除订阅",
443446
subtitle = "关闭服务时删除内存订阅",
444-
checked = store.autoClearMemorySubs
445-
) {
446-
storeFlow.value = store.copy(
447-
autoClearMemorySubs = it
448-
)
449-
}
447+
checked = store.autoClearMemorySubs,
448+
onCheckedChange = {
449+
storeFlow.update {
450+
it.copy(autoClearMemorySubs = !it.autoClearMemorySubs)
451+
}
452+
}
453+
)
450454

451455
Text(
452456
text = "快照",
@@ -499,18 +503,19 @@ fun AdvancedPage() {
499503
} else {
500504
ButtonService.stop()
501505
}
502-
}
506+
},
503507
)
504508

505509
TextSwitch(
506510
title = "音量快照",
507511
subtitle = "音量变化时保存快照",
508-
checked = store.captureVolumeChange
509-
) {
510-
storeFlow.value = store.copy(
511-
captureVolumeChange = it
512-
)
513-
}
512+
checked = store.captureVolumeChange,
513+
onCheckedChange = {
514+
storeFlow.value = store.copy(
515+
captureVolumeChange = it
516+
)
517+
},
518+
)
514519

515520
TextSwitch(
516521
title = "截屏快照",
@@ -519,6 +524,7 @@ fun AdvancedPage() {
519524
suffixIcon = {
520525
CustomIconButton(
521526
size = 32.dp,
527+
onClickLabel = "打开配置截屏快照弹窗",
522528
onClick = throttle {
523529
showCaptureScreenshotDlg = true
524530
},
@@ -529,34 +535,37 @@ fun AdvancedPage() {
529535
)
530536
}
531537
},
532-
) {
533-
storeFlow.value = store.copy(
534-
captureScreenshot = it
535-
)
536-
if (it && store.screenshotTargetAppId.isEmpty() || store.screenshotEventSelector.isEmpty()) {
537-
toast("请配置目标应用和特征事件选择器")
538+
onCheckedChange = {
539+
storeFlow.value = store.copy(
540+
captureScreenshot = it
541+
)
542+
if (it && store.screenshotTargetAppId.isEmpty() || store.screenshotEventSelector.isEmpty()) {
543+
toast("请配置目标应用和特征事件选择器")
544+
}
538545
}
539-
}
546+
)
540547

541548
TextSwitch(
542549
title = "隐藏状态栏",
543550
subtitle = "隐藏快照截图状态栏",
544-
checked = store.hideSnapshotStatusBar
545-
) {
546-
storeFlow.value = store.copy(
547-
hideSnapshotStatusBar = it
548-
)
549-
}
551+
checked = store.hideSnapshotStatusBar,
552+
onCheckedChange = {
553+
storeFlow.value = store.copy(
554+
hideSnapshotStatusBar = it
555+
)
556+
}
557+
)
550558

551559
TextSwitch(
552560
title = "保存提示",
553561
subtitle = "提示「正在保存快照」",
554-
checked = store.showSaveSnapshotToast
555-
) {
556-
storeFlow.value = store.copy(
557-
showSaveSnapshotToast = it
558-
)
559-
}
562+
checked = store.showSaveSnapshotToast,
563+
onCheckedChange = {
564+
storeFlow.value = store.copy(
565+
showSaveSnapshotToast = it
566+
)
567+
}
568+
)
560569

561570
SettingItem(
562571
title = "Github Cookie",

app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,12 @@ fun AuthA11yPage() {
7676
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
7777
Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
7878
PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {
79-
PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {
80-
mainVm.popBackStack()
81-
})
79+
PerfIconButton(
80+
imageVector = PerfIcon.ArrowBack,
81+
onClickLabel = "返回上级页面",
82+
onClick = {
83+
mainVm.popBackStack()
84+
})
8285
}, title = {
8386
Text(text = "授权状态")
8487
})

app/src/main/kotlin/li/songe/gkd/ui/BlockA11yAppListPage.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ import androidx.compose.runtime.setValue
3131
import androidx.compose.ui.Alignment
3232
import androidx.compose.ui.Modifier
3333
import androidx.compose.ui.input.nestedscroll.nestedScroll
34+
import androidx.compose.ui.semantics.clearAndSetSemantics
35+
import androidx.compose.ui.semantics.contentDescription
36+
import androidx.compose.ui.semantics.onClick
37+
import androidx.compose.ui.semantics.stateDescription
3438
import androidx.compose.ui.text.style.TextAlign
3539
import androidx.compose.ui.text.style.TextOverflow
3640
import androidx.compose.ui.unit.dp
@@ -183,6 +187,8 @@ fun BlockA11yAppListPage() {
183187
Row {
184188
PerfIconButton(
185189
imageVector = if (store.blockA11yAppListFollowMatch) PerfIcon.Lock else LockOpenRight,
190+
contentDescription = if (store.blockA11yAppListFollowMatch) "已设置为跟随应用白名单" else "已设置为独立无障碍白名单",
191+
onClickLabel = "切换模式",
186192
onClick = throttle {
187193
showSearchBar = false
188194
storeFlow.update { it.copy(blockA11yAppListFollowMatch = !it.blockA11yAppListFollowMatch) }
@@ -259,6 +265,7 @@ fun BlockA11yAppListPage() {
259265
floatingActionButton = {
260266
AnimationFloatingActionButton(
261267
visible = !editable && scrollBehavior.isFullVisible && !store.blockA11yAppListFollowMatch,
268+
onClickLabel = "进入白名单文本编辑模式",
262269
onClick = {
263270
editable = !editable
264271
},
@@ -313,11 +320,24 @@ private fun AppItemCard(
313320
appInfo: AppInfo,
314321
) {
315322
val scope = rememberCoroutineScope()
323+
val checked = remember(appInfo.id) {
324+
blockA11yAppListFlow.mapState(scope) {
325+
it.contains(appInfo.id)
326+
}
327+
}.collectAsState().value
316328
Row(
317329
modifier = Modifier
318330
.clickable(onClick = throttle {
319331
blockA11yAppListFlow.update { it.switchItem(appInfo.id) }
320332
})
333+
.clearAndSetSemantics {
334+
contentDescription = "应用:${appInfo.name}"
335+
stateDescription = if (checked) "已加入白名单" else "未加入白名单"
336+
onClick(
337+
label = if (checked) "从白名单中移除" else "加入白名单",
338+
action = null
339+
)
340+
}
321341
.appItemPadding(),
322342
horizontalArrangement = Arrangement.spacedBy(12.dp),
323343
verticalAlignment = Alignment.CenterVertically,
@@ -340,11 +360,7 @@ private fun AppItemCard(
340360
}
341361
PerfCheckbox(
342362
key = appInfo.id,
343-
checked = remember(appInfo.id) {
344-
blockA11yAppListFlow.mapState(scope) {
345-
it.contains(appInfo.id)
346-
}
347-
}.collectAsState().value,
363+
checked = checked,
348364
)
349365
}
350366
}

app/src/main/kotlin/li/songe/gkd/ui/component/AnimatedIcon.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import androidx.compose.material3.LocalContentColor
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.ui.Modifier
1111
import androidx.compose.ui.graphics.Color
12+
import li.songe.gkd.util.SafeR
1213

1314
@Composable
1415
fun AnimatedIcon(
1516
modifier: Modifier = Modifier,
1617
@DrawableRes id: Int,
1718
atEnd: Boolean = false,
1819
tint: Color = LocalContentColor.current,
20+
contentDescription: String? = getIconDesc(id, atEnd),
1921
) {
2022
val animation = AnimatedImageVector.animatedVectorResource(id)
2123
val painter = rememberAnimatedVectorPainter(
@@ -25,7 +27,12 @@ fun AnimatedIcon(
2527
Icon(
2628
modifier = modifier,
2729
painter = painter,
28-
contentDescription = null,
30+
contentDescription = contentDescription,
2931
tint = tint,
3032
)
31-
}
33+
}
34+
35+
private fun getIconDesc(@DrawableRes id: Int, atEnd: Boolean): String? = when (id) {
36+
SafeR.ic_anim_search_close -> if (atEnd) "关闭搜索" else "打开搜索"
37+
else -> null
38+
}

app/src/main/kotlin/li/songe/gkd/ui/component/AnimationFloatingActionButton.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import androidx.compose.runtime.setValue
1414
import androidx.compose.ui.Modifier
1515
import androidx.compose.ui.graphics.graphicsLayer
1616
import androidx.compose.ui.platform.LocalDensity
17+
import androidx.compose.ui.semantics.contentDescription
18+
import androidx.compose.ui.semantics.onClick
19+
import androidx.compose.ui.semantics.semantics
1720
import androidx.compose.ui.unit.dp
1821
import li.songe.gkd.util.throttle
1922

@@ -22,6 +25,8 @@ private const val elevationDurationMillis = 50
2225
@Composable
2326
fun AnimationFloatingActionButton(
2427
modifier: Modifier = Modifier,
28+
onClickLabel: String? = null,
29+
contentDescription: String? = null,
2530
visible: Boolean,
2631
onClick: () -> Unit,
2732
content: @Composable () -> Unit,
@@ -59,10 +64,19 @@ fun AnimationFloatingActionButton(
5964
}
6065
if (innerVisible) {
6166
FloatingActionButton(
62-
modifier = modifier.graphicsLayer(
63-
alpha = percent.value,
64-
translationX = (1f - percent.value) * maxTranslationX
65-
),
67+
modifier = modifier
68+
.graphicsLayer(
69+
alpha = percent.value,
70+
translationX = (1f - percent.value) * maxTranslationX
71+
)
72+
.semantics {
73+
if (contentDescription != null) {
74+
this.contentDescription = contentDescription
75+
}
76+
if (onClickLabel != null) {
77+
this.onClick(label = onClickLabel, action = null)
78+
}
79+
},
6680
elevation = FloatingActionButtonDefaults.elevation(defaultElevation = (defaultElevation.value * 6f).dp),
6781
onClick = throttle(onClick),
6882
content = content,

app/src/main/kotlin/li/songe/gkd/ui/component/CustomIconButton.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
2323
fun CustomIconButton(
2424
onClick: () -> Unit,
2525
modifier: Modifier = Modifier,
26+
onClickLabel: String? = null,
2627
size: Dp = 40.dp,
2728
enabled: Boolean = true,
2829
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
@@ -37,6 +38,7 @@ fun CustomIconButton(
3738
.background(color = colors.run { if (enabled) containerColor else disabledContainerColor })
3839
.clickable(
3940
onClick = onClick,
41+
onClickLabel = onClickLabel,
4042
enabled = enabled,
4143
role = Role.Button,
4244
interactionSource = interactionSource,

app/src/main/kotlin/li/songe/gkd/ui/component/FullscreenDialog.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ fun FullscreenDialog(
2525
dismissOnClickOutside = false,
2626
usePlatformDefaultWidth = false,
2727
decorFitsSystemWindows = false,
28+
windowTitle = "全局弹窗",
2829
)
2930
) {
3031
val activity = LocalActivity.current!!

0 commit comments

Comments
 (0)