From b24ca98e1462f8e92fb6487890611b54fd587ea5 Mon Sep 17 00:00:00 2001 From: KremeCN <3265494849@qq.com> Date: Wed, 26 Aug 2026 11:33:18 +0800 Subject: [PATCH] fix: correct ABox switch semantics and motion --- .../ABoxComponentSemanticsTest.kt | 82 ++++++++++++++----- .../abox/manager/designsystem/Components.kt | 11 ++- .../abox/manager/designsystem/Theme.kt | 36 +++++++- 3 files changed, 106 insertions(+), 23 deletions(-) diff --git a/android/core/designsystem/src/androidTest/kotlin/com/flagship/abox/manager/designsystem/ABoxComponentSemanticsTest.kt b/android/core/designsystem/src/androidTest/kotlin/com/flagship/abox/manager/designsystem/ABoxComponentSemanticsTest.kt index 7451c90..e5657d7 100644 --- a/android/core/designsystem/src/androidTest/kotlin/com/flagship/abox/manager/designsystem/ABoxComponentSemanticsTest.kt +++ b/android/core/designsystem/src/androidTest/kotlin/com/flagship/abox/manager/designsystem/ABoxComponentSemanticsTest.kt @@ -1,35 +1,38 @@ package com.flagship.abox.manager.designsystem +import android.os.Build import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertHeightIsAtLeast import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertWidthIsAtLeast import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.flagship.abox.manager.designsystem.icon.ABoxIconKey import com.flagship.abox.manager.preferences.AppThemeMode import com.flagship.abox.manager.preferences.DesignRenderer import com.flagship.abox.manager.preferences.UiPreferences import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import androidx.compose.ui.Modifier -import androidx.compose.material3.Text -import com.flagship.abox.manager.designsystem.icon.ABoxIconKey - - @RunWith(AndroidJUnit4::class) class ABoxComponentSemanticsTest { @get:Rule @@ -39,27 +42,58 @@ class ABoxComponentSemanticsTest { fun allInteractiveControlsHaveAtLeast48DpBounds() { composeRule.setContent { ABoxTheme { - ABoxButton(onClick = {}, modifier = androidx.compose.ui.Modifier.testTag("button")) { - androidx.compose.material3.Text("确定") + ABoxButton(onClick = {}, modifier = Modifier.testTag("button")) { + Text("确定") } ABoxIconButton( - key = com.flagship.abox.manager.designsystem.icon.ABoxIconKey.ADD, + key = ABoxIconKey.ADD, contentDescription = "添加设备", onClick = {}, - modifier = androidx.compose.ui.Modifier.testTag("icon"), + modifier = Modifier.testTag("icon"), ) ABoxSwitch( checked = false, onCheckedChange = {}, contentDescription = "启用设备", - modifier = androidx.compose.ui.Modifier.testTag("switch"), + modifier = Modifier.testTag("switch"), ) } } - composeRule.onNodeWithTag("button").assertHeightIsAtLeast(48.dp) - composeRule.onNodeWithTag("icon").assertHeightIsAtLeast(48.dp) - composeRule.onNodeWithTag("switch").assertHeightIsAtLeast(48.dp) + listOf("button", "icon", "switch").forEach { tag -> + composeRule.onNodeWithTag(tag) + .assertWidthIsAtLeast(48.dp) + .assertHeightIsAtLeast(48.dp) + } + } + + @Test + fun operableSwitchExposesChineseLabelRoleStateAndToggleAction() { + var checked = false + composeRule.setContent { + ABoxTheme { + ABoxSwitch( + checked = checked, + onCheckedChange = { checked = it }, + contentDescription = "启用设备", + modifier = Modifier.testTag("operable-switch"), + ) + } + } + + val switchNode = composeRule.onNodeWithTag("operable-switch") + .assertContentDescriptionEquals("启用设备") + .assertIsOff() + .assertHasClickAction() + .assertWidthIsAtLeast(48.dp) + .assertHeightIsAtLeast(48.dp) + .fetchSemanticsNode() + assertEquals(Role.Switch, switchNode.config[SemanticsProperties.Role]) + assertTrue(switchNode.config.contains(SemanticsProperties.ToggleableState)) + + composeRule.onNodeWithTag("operable-switch").performClick() + composeRule.waitForIdle() + assertEquals(true, checked) } @Test @@ -71,7 +105,7 @@ class ABoxComponentSemanticsTest { subtitle = "在线", onClick = {}, quickAction = ABoxCardQuickAction( - key = com.flagship.abox.manager.designsystem.icon.ABoxIconKey.REFRESH, + key = ABoxIconKey.REFRESH, contentDescription = "刷新设备", onClick = {}, ), @@ -108,9 +142,19 @@ class ABoxComponentSemanticsTest { } @Test - fun reducedMotionExposesStaticLoadingState() { + fun reducedMotionExposesStaticLoadingStateThroughThemeSeam() { composeRule.setContent { - CompositionLocalProvider(LocalABoxReducedMotion provides true) { + ABoxThemeContent( + preferences = UiPreferences( + renderer = DesignRenderer.MATERIAL3, + themeMode = AppThemeMode.LIGHT, + dynamicColorEnabled = false, + ), + systemInDarkTheme = false, + sdkInt = Build.VERSION.SDK_INT, + dynamicColorTokens = null, + reducedMotion = true, + ) { ABoxLoadingState(contentDescription = "正在加载") } } @@ -126,7 +170,7 @@ class ABoxComponentSemanticsTest { composeRule.setContent { ABoxTheme { ABoxIconButton( - key = com.flagship.abox.manager.designsystem.icon.ABoxIconKey.BACK, + key = ABoxIconKey.BACK, contentDescription = "返回", onClick = {}, ) diff --git a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt index 614e7a6..a47ef34 100644 --- a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt +++ b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt @@ -54,6 +54,8 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription +import androidx.compose.foundation.selection.toggleable +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -135,6 +137,12 @@ fun ABoxSwitch( Box( modifier = modifier .defaultMinSize(minWidth = 48.dp, minHeight = 48.dp) + .toggleable( + value = checked, + enabled = enabled && onCheckedChange != null, + role = Role.Switch, + onValueChange = { onCheckedChange?.invoke(it) }, + ) .semantics { this.contentDescription = contentDescription }, @@ -142,8 +150,9 @@ fun ABoxSwitch( ) { Switch( checked = checked, - onCheckedChange = onCheckedChange, + onCheckedChange = null, enabled = enabled, + modifier = Modifier.clearAndSetSemantics { }, ) } } diff --git a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt index ad6eb22..bed7e71 100644 --- a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt +++ b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt @@ -1,6 +1,7 @@ package com.flagship.abox.manager.designsystem import android.os.Build +import android.provider.Settings import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme @@ -35,12 +36,12 @@ fun ABoxTheme( content: @Composable () -> Unit, ) { val systemInDarkTheme = androidx.compose.foundation.isSystemInDarkTheme() + val context = LocalContext.current val isDarkTheme = when (preferences.themeMode) { AppThemeMode.SYSTEM -> systemInDarkTheme AppThemeMode.LIGHT -> false AppThemeMode.DARK -> true } - val context = LocalContext.current val dynamicTokens = if (preferences.dynamicColorEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val dynamicColorScheme = if (isDarkTheme) { dynamicDarkColorScheme(context) @@ -51,11 +52,30 @@ fun ABoxTheme( } else { null } - val resolution = ABoxThemeResolver.resolve( + ABoxThemeContent( preferences = preferences, systemInDarkTheme = systemInDarkTheme, sdkInt = Build.VERSION.SDK_INT, dynamicColorTokens = dynamicTokens, + reducedMotion = systemAnimatorDurationScale(context) == 0f, + content = content, + ) +} + +@Composable +internal fun ABoxThemeContent( + preferences: UiPreferences, + systemInDarkTheme: Boolean, + sdkInt: Int, + dynamicColorTokens: ABoxColorTokens?, + reducedMotion: Boolean, + content: @Composable () -> Unit, +) { + val resolution = ABoxThemeResolver.resolve( + preferences = preferences, + systemInDarkTheme = systemInDarkTheme, + sdkInt = sdkInt, + dynamicColorTokens = dynamicColorTokens, ) val renderer = ABoxRendererAdapters.forRenderer(resolution.renderer) @@ -65,7 +85,7 @@ fun ABoxTheme( LocalABoxTypography provides ABoxTypography, LocalABoxShapes provides resolution.shapeTokens, LocalABoxRenderer provides resolution.renderer, - LocalABoxReducedMotion provides false, + LocalABoxReducedMotion provides reducedMotion, LocalABoxBlurEnabled provides resolution.blurEnabled, ) { renderer.Render( @@ -75,6 +95,16 @@ fun ABoxTheme( } } +private fun systemAnimatorDurationScale(context: android.content.Context): Float = + runCatching { + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) + }.getOrDefault(1f) + + @Composable fun ABoxTheme( darkTheme: Boolean = androidx.compose.foundation.isSystemInDarkTheme(),