Skip to content

Commit 57053dd

Browse files
committed
Improve accessibility and dark splash screen
Add multiple accessibility fixes and align Android launch visuals: register semantic actions for bottom nav and center FAB so TalkBack/UiAutomator can activate tabs; add descriptive Semantics/ExcludeSemantics wrappers for Add Pet wizard (progress bar + animal options), pet profile (stats, header buttons, Add Pet avatar), home/marketplace badges and promo banner to prevent duplicated or missing screen-reader output; enhance Pet Care progress semantics and ProgressRing labels. Also replace the white launch background with a dark splash color (#0F0E0C) via a new colors.xml and update styles.xml to use the dark launch theme to eliminate the white flash. These changes improve accessibility, automated testing reliability, and brand-consistent launch visuals.
1 parent 9539c89 commit 57053dd

10 files changed

Lines changed: 613 additions & 269 deletions

File tree

android/app/src/main/res/drawable/launch_background.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<!-- Modify this file to customize your launch splash screen -->
33
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4-
<item android:drawable="@android:color/white" />
4+
<item android:drawable="@color/splashBackground" />
55

66
<!-- You can insert your own image assets here -->
77
<!-- <item>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<!-- PetSphere dark surface — matches app theme background (#0F0E0C) -->
4+
<color name="splashBackground">#0F0E0C</color>
5+
</resources>
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3-
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
4-
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
3+
<!-- Theme applied to the Android Window while the process is starting.
4+
Always uses the dark theme so the splash matches PetSphere's dark UI. -->
5+
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
56
<!-- Show a splash screen on the activity. Automatically removed when
67
the Flutter engine draws its first frame -->
78
<item name="android:windowBackground">@drawable/launch_background</item>
@@ -12,7 +13,7 @@
1213
running.
1314
1415
This Theme is only used starting with V2 of Flutter's Android embedding. -->
15-
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
16-
<item name="android:windowBackground">?android:colorBackground</item>
16+
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
17+
<item name="android:windowBackground">@color/splashBackground</item>
1718
</style>
1819
</resources>

docs/plan/cursor_ui_ux_audit_of_flutter_app.md

Lines changed: 233 additions & 0 deletions
Large diffs are not rendered by default.

lib/views/add_pet_screen.dart

Lines changed: 61 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,18 @@ class _AddPetScreenState extends ConsumerState<AddPetScreen>
355355
],
356356
),
357357
const SizedBox(height: 8),
358-
ClipRRect(
359-
borderRadius: BorderRadius.circular(4),
360-
child: LinearProgressIndicator(
361-
value: (_currentStep + 1) / 3,
362-
minHeight: 4,
363-
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
364-
valueColor:
365-
AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.primary),
358+
Semantics(
359+
label: 'Step ${_currentStep + 1} of 3: ${_stepTitle()}',
360+
value: '${((_currentStep + 1) / 3 * 100).round()} percent complete',
361+
child: ClipRRect(
362+
borderRadius: BorderRadius.circular(4),
363+
child: LinearProgressIndicator(
364+
value: (_currentStep + 1) / 3,
365+
minHeight: 4,
366+
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
367+
valueColor:
368+
AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.primary),
369+
),
366370
),
367371
),
368372
],
@@ -434,50 +438,58 @@ class _AddPetScreenState extends ConsumerState<AddPetScreen>
434438
final option = _animalOptions[index];
435439
final isSelected = _selectedAnimalType == option.label;
436440
final optionColor = Theme.of(context).colorScheme.primary;
437-
return GestureDetector(
441+
return Semantics(
442+
button: true,
443+
selected: isSelected,
444+
label: option.label,
438445
onTap: () => setState(() => _selectedAnimalType = option.label),
439-
child: AnimatedContainer(
440-
duration: const Duration(milliseconds: 250),
441-
curve: Curves.easeOut,
442-
decoration: BoxDecoration(
443-
color: isSelected
444-
? optionColor.withAlpha(26)
445-
: Theme.of(context).colorScheme.surfaceContainerHigh,
446-
borderRadius: BorderRadius.circular(20),
447-
border: Border.all(
448-
color: isSelected ? optionColor : Theme.of(context).colorScheme.onSurfaceVariant,
449-
width: isSelected ? 2.5 : 1,
450-
),
451-
boxShadow: isSelected
452-
? [
453-
BoxShadow(
454-
color: optionColor.withAlpha(51),
455-
blurRadius: 12,
456-
offset: const Offset(0, 4),
457-
)
458-
]
459-
: [],
460-
),
461-
child: Column(
462-
mainAxisAlignment: MainAxisAlignment.center,
463-
children: [
464-
Icon(
465-
option.icon,
466-
size: 36,
467-
color: isSelected ? optionColor : Theme.of(context).colorScheme.onSurfaceVariant,
468-
),
469-
const SizedBox(height: 8),
470-
Text(
471-
option.label,
472-
style: TextStyle(
473-
fontSize: 15,
474-
fontWeight:
475-
isSelected ? FontWeight.bold : FontWeight.w500,
476-
color:
477-
isSelected ? optionColor : Theme.of(context).colorScheme.onSurfaceVariant,
446+
child: ExcludeSemantics(
447+
child: GestureDetector(
448+
onTap: () => setState(() => _selectedAnimalType = option.label),
449+
child: AnimatedContainer(
450+
duration: const Duration(milliseconds: 250),
451+
curve: Curves.easeOut,
452+
decoration: BoxDecoration(
453+
color: isSelected
454+
? optionColor.withAlpha(26)
455+
: Theme.of(context).colorScheme.surfaceContainerHigh,
456+
borderRadius: BorderRadius.circular(20),
457+
border: Border.all(
458+
color: isSelected ? optionColor : Theme.of(context).colorScheme.onSurfaceVariant,
459+
width: isSelected ? 2.5 : 1,
478460
),
461+
boxShadow: isSelected
462+
? [
463+
BoxShadow(
464+
color: optionColor.withAlpha(51),
465+
blurRadius: 12,
466+
offset: const Offset(0, 4),
467+
)
468+
]
469+
: [],
479470
),
480-
],
471+
child: Column(
472+
mainAxisAlignment: MainAxisAlignment.center,
473+
children: [
474+
Icon(
475+
option.icon,
476+
size: 36,
477+
color: isSelected ? optionColor : Theme.of(context).colorScheme.onSurfaceVariant,
478+
),
479+
const SizedBox(height: 8),
480+
Text(
481+
option.label,
482+
style: TextStyle(
483+
fontSize: 15,
484+
fontWeight:
485+
isSelected ? FontWeight.bold : FontWeight.w500,
486+
color:
487+
isSelected ? optionColor : Theme.of(context).colorScheme.onSurfaceVariant,
488+
),
489+
),
490+
],
491+
),
492+
),
481493
),
482494
),
483495
);

lib/views/home_screen.dart

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,25 +1144,27 @@ class _NotificationIconButton extends ConsumerWidget {
11441144
Positioned(
11451145
right: -4,
11461146
top: -4,
1147-
child: Container(
1148-
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
1149-
decoration: BoxDecoration(
1150-
color: Theme.of(context).colorScheme.primary,
1151-
borderRadius: BorderRadius.circular(10),
1152-
border: Border.all(
1153-
color: Theme.of(context).colorScheme.surface,
1154-
width: 1.5,
1147+
child: ExcludeSemantics(
1148+
child: Container(
1149+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
1150+
decoration: BoxDecoration(
1151+
color: Theme.of(context).colorScheme.primary,
1152+
borderRadius: BorderRadius.circular(10),
1153+
border: Border.all(
1154+
color: Theme.of(context).colorScheme.surface,
1155+
width: 1.5,
1156+
),
11551157
),
1156-
),
1157-
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
1158-
child: Text(
1159-
unread > 99 ? '99+' : '$unread',
1160-
style: TextStyle(
1161-
color: Theme.of(context).colorScheme.onPrimary,
1162-
fontSize: 10,
1163-
fontWeight: FontWeight.bold,
1158+
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
1159+
child: Text(
1160+
unread > 99 ? '99+' : '$unread',
1161+
style: TextStyle(
1162+
color: Theme.of(context).colorScheme.onPrimary,
1163+
fontSize: 10,
1164+
fontWeight: FontWeight.bold,
1165+
),
1166+
textAlign: TextAlign.center,
11641167
),
1165-
textAlign: TextAlign.center,
11661168
),
11671169
),
11681170
),
@@ -1191,25 +1193,27 @@ class _MessageIconButton extends ConsumerWidget {
11911193
Positioned(
11921194
right: -4,
11931195
top: -4,
1194-
child: Container(
1195-
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
1196-
decoration: BoxDecoration(
1197-
color: Theme.of(context).colorScheme.primary,
1198-
borderRadius: BorderRadius.circular(10),
1199-
border: Border.all(
1200-
color: Theme.of(context).colorScheme.surface,
1201-
width: 1.5,
1196+
child: ExcludeSemantics(
1197+
child: Container(
1198+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
1199+
decoration: BoxDecoration(
1200+
color: Theme.of(context).colorScheme.primary,
1201+
borderRadius: BorderRadius.circular(10),
1202+
border: Border.all(
1203+
color: Theme.of(context).colorScheme.surface,
1204+
width: 1.5,
1205+
),
12021206
),
1203-
),
1204-
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
1205-
child: Text(
1206-
unread > 99 ? '99+' : '$unread',
1207-
style: TextStyle(
1208-
color: Theme.of(context).colorScheme.onPrimary,
1209-
fontSize: 10,
1210-
fontWeight: FontWeight.bold,
1207+
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
1208+
child: Text(
1209+
unread > 99 ? '99+' : '$unread',
1210+
style: TextStyle(
1211+
color: Theme.of(context).colorScheme.onPrimary,
1212+
fontSize: 10,
1213+
fontWeight: FontWeight.bold,
1214+
),
1215+
textAlign: TextAlign.center,
12111216
),
1212-
textAlign: TextAlign.center,
12131217
),
12141218
),
12151219
),

lib/views/main_layout.dart

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ class GlassNavBar extends StatelessWidget {
114114
NavItem(Icons.person_outline, Icons.person),
115115
];
116116

117+
/// TalkBack / VoiceOver labels (center index 2 uses [centerFabSemanticLabel]).
118+
static const List<String> tabSemanticLabels = [
119+
'Home',
120+
'Discover',
121+
'', // placeholder; never read — center slot is not an Expanded tab
122+
'Marketplace',
123+
'Profile',
124+
];
125+
126+
static const String centerFabSemanticLabel = 'Pet Care';
127+
117128

118129
@override
119130
Widget build(BuildContext context) {
@@ -140,24 +151,31 @@ class GlassNavBar extends StatelessWidget {
140151
: colorScheme.onSurfaceVariant;
141152

142153
if (isCenter) {
143-
return GestureDetector(
154+
return Semantics(
155+
button: true,
156+
label: centerFabSemanticLabel,
157+
hint: 'Opens pet care diary, goals, and daily checklist',
144158
onTap: () => onTap(i),
145-
child: AnimatedContainer(
146-
duration: const Duration(milliseconds: 280),
147-
curve: Curves.easeOut,
148-
width: 48,
149-
height: 48,
150-
decoration: BoxDecoration(
151-
color: colorScheme.primary,
152-
shape: BoxShape.circle,
153-
boxShadow: Theme.of(
154-
context,
155-
).extension<PetfolioShadows>()?.button,
156-
),
157-
child: Icon(
158-
Icons.add,
159-
color: colorScheme.onPrimary,
160-
size: 28,
159+
excludeSemantics: true,
160+
child: GestureDetector(
161+
onTap: () => onTap(i),
162+
child: AnimatedContainer(
163+
duration: const Duration(milliseconds: 280),
164+
curve: Curves.easeOut,
165+
width: 48,
166+
height: 48,
167+
decoration: BoxDecoration(
168+
color: colorScheme.primary,
169+
shape: BoxShape.circle,
170+
boxShadow: Theme.of(
171+
context,
172+
).extension<PetfolioShadows>()?.button,
173+
),
174+
child: Icon(
175+
Icons.add,
176+
color: colorScheme.onPrimary,
177+
size: 28,
178+
),
161179
),
162180
),
163181
);
@@ -179,24 +197,34 @@ class GlassNavBar extends StatelessWidget {
179197
}
180198

181199
return Expanded(
182-
child: GestureDetector(
200+
child: Semantics(
201+
button: true,
202+
selected: isActive,
203+
label: tabSemanticLabels[i],
204+
hint: i == 4 && profileImageUrl.isEmpty
205+
? 'Your profile and pets'
206+
: null,
183207
onTap: () => onTap(i),
184-
behavior: HitTestBehavior.opaque,
185-
child: Center(
186-
child: AnimatedContainer(
187-
duration: const Duration(milliseconds: 280),
188-
curve: Curves.easeOut,
189-
padding: const EdgeInsets.symmetric(
190-
horizontal: 14,
191-
vertical: 8,
192-
),
193-
decoration: ShapeDecoration(
194-
color: isActive
195-
? colorScheme.primary.withValues(alpha: 0.10)
196-
: Colors.transparent,
197-
shape: const StadiumBorder(),
208+
excludeSemantics: true,
209+
child: GestureDetector(
210+
onTap: () => onTap(i),
211+
behavior: HitTestBehavior.opaque,
212+
child: Center(
213+
child: AnimatedContainer(
214+
duration: const Duration(milliseconds: 280),
215+
curve: Curves.easeOut,
216+
padding: const EdgeInsets.symmetric(
217+
horizontal: 14,
218+
vertical: 8,
219+
),
220+
decoration: ShapeDecoration(
221+
color: isActive
222+
? colorScheme.primary.withValues(alpha: 0.10)
223+
: Colors.transparent,
224+
shape: const StadiumBorder(),
225+
),
226+
child: child,
198227
),
199-
child: child,
200228
),
201229
),
202230
),

0 commit comments

Comments
 (0)