Audit UI/UX
Cette application a un axe de design excellent et quatre axes manquants. Le système de couleurs est de qualité référence — documenté, contrôlé en contraste, verrouillé par la CI, partagé entre l'écran et le PDF. La typographie est restée le gabarit d'Android Studio. Il n'y a aucune iconographie, aucune échelle de formes, aucune échelle d'espacement, et quasiment aucun mouvement. L'écart jusqu'à « une application Android parfaite » n'est pas une question de goût : c'est du travail de système.
Verdict
Périmètre : la couche présentation telle qu'elle existe aujourd'hui — branche aaa/phase0, les 24 fichiers Compose de ui/ et theme/, plus MainScreen.kt, MainActivity.kt, themes.xml et strings.xml. Le DSP, la métrologie et les ViewModels sont hors périmètre sauf là où ils contraignent la mise en page.
theme/Color.kt est le meilleur travail de design du dépôt, et l'un des meilleurs systèmes de couleurs qu'on trouve dans une application d'instrumentation : 60 tokens nommés, chacun documenté avec son ratio de contraste mesuré contre la surface précise sur laquelle il est employé, une décision produit assumée et argumentée de refuser la couleur dynamique et le thème clair (D4), une source de vérité unique en ARGB partagée entre l'écran et le PDF — le rapport ne peut pas contredire l'affichage sur la couleur d'un ordre suivi — et un PaletteContrastTest qui casse le build quand un ratio régresse. Il ne faut pas y toucher. C'est le modèle de ce que les autres axes devraient être.
Tout le reste du système de design est soit un gabarit, soit absent :
theme/Type.ktest le gabarit d'Android Studio, mot pour mot. Il définit exactement 1 des 15 rôles typographiques de Material 3 (bodyLarge) et laisse les 14 autres en commentaire de remplissage. L'application compense avec 159 littérauxfontSizerépartis sur 20 fichiers — dont 23 sites entre 9.sp et 10.5.sp, sous le plus petit rôle défini par Material (labelSmall, 11.sp) et sous tout seuil de lisibilité défendable pour un appareil lu à bout de bras dans un véhicule en mouvement.- Il n'y a aucune iconographie. Pas un seul appel
Icon()dans l'application ; l'artefactmaterial-iconsn'est même pas une dépendance. Chaque affordance est soit un libellé texte, soit un emoji intégré dans une ressource de chaîne traduisible. - Aucune échelle de formes, aucune échelle d'espacement. Aucun
Shapes()n'est passé àMaterialTheme, donc 8 rayons d'angle différents (3 à 16 dp) sont appliqués au cas par cas. 275 littérauxdpportent l'espacement, dont 36 en2.dpet 22 en1.dp— hors de toute grille de 4 dp, et la raison pour laquelle l'interface paraît serrée plutôt que dense. - Il n'y a quasiment aucun mouvement. Six appels d'animation existent dans toute l'application ; trois sont la pulsation des balises du canvas et trois sont un fondu de couleur dans
ReportModeScreen. Les changements de mode, le figeage, les menus et l'apparition du lecteur WAV surgissent d'un coup. Sur Android, cette absence est une esthétique — elle se lit comme « inachevé ».
MainScreen.kt fait 1 484 lignes, et AppScreen est un composable unique de ~1 020 lignes qui inclut en ligne le chrome, les deux panneaux, cinq popups et huit appels de dialogue — en choisissant couleur, taille et espacement à chaque site. Il n'y a pas de couche de composants : ni NvhButton, ni NvhCard, ni NvhSectionHeader. Chaque décision visuelle est donc prise ~30 fois et peut dériver de ~30 manières. Corriger les tokens sans extraire les composants ne tiendra pas — c'est pourquoi la feuille de route place le système de design (phase 1) avant le travail de mise en page (phase 4).Notes par axe
L'écart entre les notes est le constat : la couleur a été traitée comme un vrai problème d'ingénierie ; tous les autres axes sont restés à ce que le gabarit livrait.
| Axe | Note | Constat |
|---|---|---|
| Système de couleurs | A− | 60 tokens documentés, contraste mesuré par token et par surface, verrouillé par PaletteContrastTest, source ARGB partagée écran + PDF. Exemplaire. |
| Typographie | D | Fichier gabarit. 1 rôle sur 15 défini. 159 littéraux fontSize, dont 23 sous 11.sp. |
| Iconographie | F | Zéro appel Icon(). Aucune dépendance d'icônes. Emojis utilisés comme icônes, dans des chaînes traduisibles. |
| Formes | D | Aucun Shapes() ; 8 rayons (3–16 dp) sur 40 sites d'appel. |
| Espacement & densité | C− | Aucune échelle. 275 littéraux dp, dont 58 en 1–2 dp. Cela produit du serrage, pas de la densité. |
| Mise en page & réactivité | C− | Structure à deux panneaux saine. Mais un seul point de rupture sur le ratio d'aspect, pas de WindowSizeClass, poids imbriqués morts, largeurs de popup figées en dp. |
| Contrôles & affordances | C | Cibles de 48 dp respectées partout. Annulé par l'état d'activation rendu comme un simple échange de couleur de conteneur, l'absence de hiérarchie de boutons, deux idiomes de dialogue concurrents. |
| Mouvement | D | 6 appels d'animation ; 3 sont une pulsation de canvas. Aucune transition d'état, de navigation ou de visibilité. |
| Occupation de l'écran | C | La barre inférieure dépense sa propre largeur en marges de 2 dp et libellés de 10 sp. Pas de redimensionnement des panneaux, pas de mode immersif pour le canvas. |
| Accessibilité | B− | Réellement soignée : planchers de 48 dp, contentDescription sur chaque contrôle emoji, la couleur n'est jamais le seul canal. Annulé par le texte sous 11.sp et softWrap = false. |
| Outillage & process | F | 0 composable @Preview. 0 test de capture d'écran. 0 test instrumenté. Itérer visuellement coûte une installation complète par regard. |
Constats bloquants
L'un est un défaut fonctionnel qui part en production aujourd'hui. L'un contredit le soin d'accessibilité pris dans les mêmes fichiers. L'un bloque directement l'objectif : « visuellement parfait » s'atteint en itérant, et itérer coûte ici une installation Gradle complète par regard.
Les décalages de Popup sont en pixels : les deux menus de la barre inférieure sont mal placés sur chaque appareil
MainScreen.kt:300-306, 359-365
Popup(
alignment = Alignment.TopCenter,
offset = IntOffset(0, -100), // bouton Exporter
)
Popup(
alignment = Alignment.TopCenter,
offset = IntOffset(0, -250), // menu de source audio
onDismissRequest = { showAudioModeMenu = false },
)Le paramètre offset: IntOffset de Popup est appliqué en pixels bruts, pas en unités indépendantes de la densité. Ces valeurs ont visiblement été réglées à l'œil sur un seul appareil.
- Sur un téléphone à 2,75× (440 dpi, tout à fait courant) : le bouton Exporter censé se placer ~100 dp au-dessus du bouton Figer se place ~36 dp au-dessus — il chevauche la barre.
- Le menu de source à trois entrées censé dégager la barre de ~250 dp ne la dégage que de ~91 dp : ses entrées basses recouvrent la barre inférieure qui les a ouvertes.
- Sur une tablette à 1×, le même menu flotte ~250 dp plus haut, détaché de son déclencheur.
Correction — ce ne sont pas des popups : c'est un menu et une action contextuelle. DropdownMenu se positionne contre son ancre, s'anime et se ferme au toucher extérieur, gratuitement. Si un Popup doit rester, il faut convertir via LocalDensity : with(LocalDensity.current) { IntOffset(0, -(100.dp).roundToPx()) }.
23 sites de texte sont rendus sous 11.sp, sur un appareil lu à bout de bras dans un véhicule
MainScreen.kt (13 sites) · KinematicsDialog.kt (7) · EmergenceReportDialog.kt (2) · SettingsDialog.kt (1)
| Taille | Nombre | État |
|---|---|---|
| 9.sp | 7 | Sous toute recommandation de lisibilité |
| 9.5.sp | 1 | Sous toute recommandation de lisibilité |
| 10.sp | 12 | Sous le plus petit rôle de Material |
| 10.5.sp | 3 | Sous le plus petit rôle de Material |
| 11.sp | 51 | labelSmall — le plancher, utilisé comme taille de corps par défaut |
| 12.sp | 30 | — |
| 13–40.sp | ~55 | — |
labelSmall (11.sp) est le plus petit rôle que Material 3 définisse, destiné à des étiquettes rares. Cette application l'utilise comme sa taille la plus fréquente, puis passe dessous 23 fois. Les sites à 9.sp de KinematicsDialog.kt sont des affichages de rapports et de paramètres — des nombres qu'un ingénieur doit lire correctement pour faire confiance à un régime moteur.
Cela contredit directement le soin pris ailleurs dans les mêmes fichiers : MIN_TOUCH_TARGET = 48.dp est documenté comme étant destiné aux opérateurs « portant des gants, dans un véhicule en mouvement » — et le même opérateur, dans les mêmes conditions, doit lire du texte à 9.sp.
Correction — définir l'échelle typographique (UX-M1), poser le plancher à labelMedium (12.sp) pour toute étiquette et bodyMedium (14.sp) pour toute valeur lue comme une mesure, puis supprimer les 159 littéraux au profit des rôles.
Aucune preview et aucun test de capture : le travail visuel n'a ni boucle de retour ni filet
grep -rn "@Preview" app/src/main/java → 0 résultat · app/src/androidTest → n'existe pas
ci/checks.sh verrouille les hexadécimaux de couleur, les binaires suivis et l'unicité de la version. PaletteContrastTest verrouille les ratios de contraste. Les deux sont bons. Mais aucun gate et aucun outil n'observe la mise en page.
Il n'existe aucun moyen de voir un dialogue sans compiler et installer, aucun moyen de vérifier un composant à l'échelle de police 1,3 ou 2,0, aucun moyen de vérifier le paysage sans faire tourner un appareil, et rien qui échoue quand un changement d'espacement tronque un libellé. Les dépendances de test Compose sont déjà déclarées dans app/build.gradle.kts:101-102 — et ne sourcent rien.
Correction, dans l'ordre — (a) @Preview pour chaque dialogue et les deux panneaux, avec @PreviewFontScale et @PreviewScreenSizes ; (b) câbler androidTest et vérifier les invariants qui comptent — aucun libellé tronqué à l'échelle 2,0, cibles de 48 dp, les deux orientations ; (c) Roborazzi ou Paparazzi pour que les régressions de mise en page échouent en CI sans appareil.
Constats majeurs
theme/Type.kt est le gabarit d'Android Studio, mot pour mot
theme/Type.kt:1-36
val Typography = Typography(
bodyLarge = TextStyle(/* … */)
/* Other default text styles to override
titleLarge = TextStyle(…),
labelSmall = TextStyle(…)
*/
)Quatorze des quinze rôles sont les valeurs par défaut de Material, et aucune famille de police n'est choisie. Critique pour cette application : il n'y a aucun rôle monospace ni à chiffres tabulaires. Un instrument qui affiche régime, km/h, Hz et dB en lecture continue a besoin de chiffres qui ne se décalent pas horizontalement quand les valeurs changent — or tous les affichages numériques sont en sans-serif à chiffres proportionnels, donc la ligne de KPI tremble à chaque mise à jour.
Correction — définir les quinze rôles ; ajouter un rôle NvhTypography.readout avec fontFeatureSettings = "tnum" pour chaque valeur de mesure ; envisager une famille condensée pour la barre inférieure afin que les libellés tiennent sans descendre à 10 sp.
Zéro icône ; des emojis en guise d'iconographie, dans des chaînes traduisibles
res/values/strings.xml (30+ sites) · WavPlayerBar.kt:98,111,118 · MainScreen.kt:1368,1425
<!-- strings.xml --> <string name="gmpe_button">⚙️ GMPe</string> <string name="export_frozen">📸 Exporter</string> <string name="player_reading">📂 LECTURE : %1$s</string> <string name="notice_with_close">%1$s ✕</string> // WavPlayerBar.kt IconButton(onClick = …) { Text("⏪", fontSize = 14.sp) }
Six problèmes indépendamment disqualifiants :
- Non thématisable. Les emojis sont rendus dans la police emoji système, à ses propres couleurs —
color = NvhOnSurfacesurText("⏸")est ignoré. La palette soigneusement spécifiée de l'application s'arrête à l'icône. - Dépendant de l'appareil. Samsung, Pixel, Xiaomi et Huawei livrent des polices emoji différentes.
⚙️,🚘et📸diffèrent matériellement sur chacun ; certains diffèrent assez pour changer le sens perçu. - Mauvaises métriques. Les emojis portent leur propre ligne de base et leur propre avance — d'où le réglage de
fontSizeglyphe par glyphe, et leur décentrage visuel dans unIconButtonde 48 dp. - Dans des ressources traduisibles. Un traducteur reçoit
📸 Exporteret peut réordonner, supprimer ou dupliquer le glyphe. Une icône n'est pas une langue. ✕comme affordance de fermeture vit à l'intérieur d'une chaîne formatée — ce n'est pas une cible tactile, pas 48 dp, pas étiqueté indépendamment.- Glyphes texte pour l'état —
●/▲/✕dansGpsLedIndicator. L'intention est admirable et documentée (la forme comme second canal pour un opérateur daltonien) et doit être préservée — mais avec trois formes vectorielles, pas trois caractères enFontWeight.Black.
Correction — ajouter material-icons-extended, ou mieux pour un instrument, dessiner ~20 vecteurs pour que le jeu soit délibéré et propre en licence. Retirer chaque emoji de strings.xml. Chaque contentDescription existant est conservé tel quel.
Aucun Shapes() dans le thème ; huit rayons d'angle appliqués au cas par cas
theme/Theme.kt:44-50 (aucun argument shapes =) · 40 sites d'appel
Mesuré : 8.dp×15, 4.dp×8, 6.dp×7, 12.dp×4, 16.dp×3, plus un chacun de 3.dp, 10.dp, 14.dp. MaterialTheme utilise donc les formes par défaut pour ses propres composants, tandis que les surfaces construites à la main autour d'eux emploient 3 à 16 dp — une Card à 12 dp contenant une Surface à 4 dp dans un dialogue à 16 dp, à côté d'un Button au défaut de Material. Quatre rayons dans un seul groupe visuel.
Correction — passer un Shapes(extraSmall = 4.dp, small = 8.dp, medium = 12.dp, large = 16.dp, extraLarge = 28.dp) explicite, utiliser MaterialTheme.shapes.* partout, et laisser ci/checks.sh verrouiller les RoundedCornerShape( bruts hors du paquet theme — exactement le motif déjà éprouvé pour les couleurs.
Aucune échelle d'espacement ; 275 littéraux dp, et les petits provoquent le serrage
tout le dépôt
| Valeur | Emplois | Valeur | Emplois |
|---|---|---|---|
| 8.dp | 50 | 2.dp | 36 |
| 6.dp | 44 | 1.dp | 22 |
| 4.dp | 42 | 16 / 12 / 10.dp | 14 chacun |
Deux problèmes distincts. D'abord, 1, 2, 3, 5, 6, 10 et 14 dp sont hors de toute grille de 4 dp, donc rien ne s'aligne sur rien et aucun rythme vertical ne s'établit.
Ensuite, et c'est plus visible : 58 emplois d'espacement de 1 à 2 dp — dont le contentPadding = PaddingValues(horizontal = 2.dp, …) de la barre inférieure sur cinq boutons — ne sont pas « denses », ils sont collés. Les interfaces professionnelles denses (Bloomberg, Ableton, Logic) emploient un rythme serré mais constant de 4/8 px avec un espacement interne généreux sur les contrôles. Cette application fait l'inverse : marges externes lâches, et des contrôles dont les libellés touchent leur propre bordure.
Correction — object NvhSpacing { val xs = 4.dp; val sm = 8.dp; val md = 12.dp; val lg = 16.dp; val xl = 24.dp }, plancher d'espacement interne à sm, et verrouillage des dp bruts hors du paquet theme.
La barre inférieure se dispute sa propre place
MainScreen.kt:237-518
Cinq contrôles se partagent la barre en Modifier.weight(1f) chacun, avec contentPadding = PaddingValues(horizontal = 2.dp, …), des libellés à 10–11 sp, maxLines = 1 et softWrap = false. Chacune de ces cinq décisions est le symptôme d'une même cause : les libellés sont trop longs pour la place, et plutôt que de les raccourcir ou de passer aux icônes, on a réduit la typographie et supprimé l'espacement jusqu'à ce que ça rentre sur l'appareil du développeur.
weight(1f)donne à « Rapport Manuel » (15 caractères) la même largeur qu'à « Figer » (5) — l'un est tronqué pendant que l'autre a du mou.softWrap = falseavecmaxLines = 1signifie une troncature brutale, pas une ellipse, dès que l'échelle de police dépasse 1,0. Le libellé devient un fragment.- 2 dp de marge horizontale placent le texte à ~2 dp du bord du bouton — visuellement, le libellé est hors de son propre conteneur.
- La barre est la navigation principale et le sélecteur de mode de l'application, et c'est la surface la moins lisible de l'application.
Correction — avec quatre modes persistants plus une action de figeage, NavigationBar pour les modes et un FloatingActionButton pour figer/exporter est la réponse Android idiomatique, et elle libère toute la largeur de la barre.
L'état d'activation est communiqué en échangeant la couleur de conteneur d'un bouton plein
MainScreen.kt:250-256, 274-280, 337-343, 385-405
containerColor = if (kinematicsConfig.isEnabled) NvhActiveContainer else colorScheme.primary containerColor = if (isReportModeActive) NvhReportMode else colorScheme.primary containerColor = if (isFrozen) NvhRecording else colorScheme.secondary
Un bouton plein bleu qui devient plein vert est un signal marche/arrêt faible : les deux états paraissent également « pressables », aucun ne paraît « engagé », et le sens doit être appris. L'application le sait en partie — gmpe_button_active ajoute le mot (Actif) — ce qui est le bon réflexe appliqué au mauvais widget.
Material 3 possède des contrôles faits pour cela, qui rendent la sélection sans ambiguïté par conteneur + contour + coche optionnelle. Et FilterChip est déjà employé correctement ailleurs dans ce même fichier (MainScreen.kt:1145-1160, le sélecteur de métrique) — l'application contient donc sa propre solution et ne l'applique pas à ses contrôles les plus importants.
Correction — SegmentedButton pour les modes de source exclusifs, FilterChip pour les bascules de fonctionnalité, IconToggleButton pour le figeage.
Deux idiomes de dialogue concurrents, et aucune bottom sheet
7 fichiers utilisent AlertDialog · 3 utilisent un Dialog { Card { … } } brut
Deux langages visuels pour le même travail — rayons, typographie de titre, placement des boutons et marges différents, choisis fichier par fichier. Et SettingsDialog (571 lignes, trois sections Card bordées, cinq Slider, un AddFilterDialog imbriqué) est un AlertDialog défilant : le conteneur le moins approprié de la plateforme pour une surface de réglages de cette taille, plafonné à ~60 % de la hauteur d'écran avec sa propre barre de défilement dans un dialogue dans un voile.
Correction — un seul wrapper NvhDialog pour les vraies confirmations ; déplacer SettingsDialog, KinematicsDialog et EmergenceReportDialog vers ModalBottomSheet (poignée, expansion pleine hauteur, conscient de l'edge-to-edge) ou vers un véritable écran de réglages.
Le mouvement est absent, et son absence se lit
6 appels d'animation : animateFloat×3 (pulsation canvas) · animateColorAsState×3 (ReportModeScreen)
Rien d'autre ne bouge. Aucune transition ne couvre :
- L'entrée et la sortie du mode rapport — un remplacement plein écran à
MainScreen.kt:196-199qui coupe net. - L'apparition du menu de source audio ; l'apparition du bouton Exporter au figeage.
- L'apparition de
WavPlayerBarau chargement d'un fichier — il surgit dans la mise en page et pousse la carte de télémétrie vers le bas. - Les changements de couleur de mode sur la barre inférieure.
Correction — AnimatedContent sur le basculement de mode rapport ; AnimatedVisibility avec expandVertically sur le lecteur et le bouton Exporter ; animateColorAsState sur chaque couleur de conteneur dépendant du mode. Budget 150–250 ms. C'est le meilleur rendement visuel par ligne de code de tout l'audit.
La réactivité tient à un seul test de ratio d'aspect ; aucun WindowSizeClass
MainScreen.kt:1206-1223
BoxWithConstraints(…) {
if (maxWidth > maxHeight) { Row { spectrogramPane(…); vehicleDataPane(…) } }
else { Column { spectrogramPane(…); vehicleDataPane(…) } }
}Le motif « deux panneaux déclarés une seule fois » est réellement bon, et le commentaire documente un vrai bug qu'il a corrigé. Mais le ratio d'aspect est le mauvais axe. Une tablette de 10 pouces en portrait fait 800×1280 dp — maxWidth < maxHeight, donc elle reçoit l'empilement téléphone, dans une fenêtre assez large pour trois panneaux. Un grand pliable déplié subit la même chose. À l'inverse, un petit téléphone en paysage (640×360) reçoit la division côte à côte avec un spectrogramme de 352 dp de large.
Correction — material3-window-size-class n'est pas une dépendance actuellement. L'ajouter et brancher sur WindowWidthSizeClass : Compact → empilement, Medium → division, Expanded → division avec un panneau de réglages visible en permanence au lieu d'un dialogue. Conserver le test d'aspect seulement comme critère de départage.
Des poids imbriqués morts rendent les proportions du panneau de données fictives
MainScreen.kt:884-925, 913-916
Column(modifier = paneModifier) {
if (…) { WavPlayerBar(…) } // sans poids, hauteur intrinsèque
Card(modifier = Modifier.weight(0.45f) …) // le SEUL enfant pondéré
}weight(0.45f) sur l'unique enfant pondéré d'une Column lui distribue tout l'espace restant — le 0.45f n'a aucun effet. La même constante morte figure sur VideoPlayerView. La prochaine personne qui règlera cette mise en page croira raisonnablement que la carte prend 45 % du panneau et que 55 % vont ailleurs ; cet ailleurs n'existe pas.
Pendant ce temps, le comportement réel — WavPlayerBar prenant sa hauteur intrinsèque en haut et la carte absorbant le reste — fait que charger un WAV rétrécit silencieusement le graphique de télémétrie de toute la hauteur du lecteur, sans transition (UX-M8).
Correction — supprimer les poids sans effet (fillMaxSize() exprime le comportement réel), ou introduire le second enfant pondéré que les constantes sous-entendent.
themes.xml configure les barres système via des API que targetSdk 36 ignore
res/values/themes.xml:19-21 · MainActivity.kt:22
<item name="android:statusBarColor">@color/nvh_primary_container</item> <item name="android:navigationBarColor">@color/nvh_background</item>
Ces deux attributs sont dépréciés et ignorés depuis l'API 35, et l'application est en targetSdk = 36 avec enableEdgeToEdge(). Les deux entrées sont de la configuration morte. L'edge-to-edge est donc actif avec une apparence de barres système non gérée, et aucune gestion de WindowInsets n'existe dans l'application au-delà de ce que Scaffold, TopAppBar et BottomAppBar appliquent par défaut. (windowSplashScreenBackground juste au-dessus reste correct et fonctionne.)
Les défauts de Material 3 dégagent probablement le contenu des barres, mais rien ne le vérifie, l'application n'exprime jamais d'intention sur le contraste des icônes de barre d'état, et les Popup de UX-B1 sont entièrement hors du périmètre d'insets de Scaffold.
Correction — supprimer les entrées mortes, piloter l'apparence des icônes via WindowInsetsControllerCompat.isAppearanceLightStatusBars, et ajouter une vérification d'insets aux tests UI (actuellement absents).
Constats modérés & mineurs
| ID | Constat | Emplacement |
|---|---|---|
| UX-D1 | Aucune couche de composants. AppScreen est un composable unique de ~1 020 lignes incluant chrome, deux panneaux, 5 popups et 8 appels de dialogue, avec couleur/taille/espacement choisis par site. C'est le mécanisme derrière UX-M1/M3/M4. | MainScreen.kt:96-1347 |
| UX-D2 | Largeurs dp figées sur le contenu des popups — 105.dp, 115.dp, 130.dp — combinées à softWrap = false. Troncature garantie à partir de l'échelle 1,3, dans les contrôles dont la hauteur a été correctement rendue flexible. | MainScreen.kt:288-291, 372 |
| UX-D3 | Aucune hiérarchie de boutons. 29 Button pleins, 12 TextButton, 6 OutlinedButton, 6 IconButton. Le plein est le défaut partout, donc rien n'est visuellement primaire ; FilledTonalButton et ElevatedButton ne sont pas utilisés. | tout le dépôt |
| UX-D4 | Élévation non systématique — defaultElevation 4/6 dp et tonalElevation 4/6 dp, mêlés à des border(1.dp, …) manuels. Deux mécanismes pour une seule hiérarchie. Choisir l'élévation tonale seule — correcte en thème sombre, où les ombres sont presque invisibles. | MainScreen.kt:924, WavPlayerBar.kt:53 |
| UX-D5 | FontWeight.Bold est le poids par défaut. Appliqué si largement qu'il ne marque plus l'emphase. FontWeight.Black apparaît une fois, pour couvrir le gras environnant. L'emphase doit venir de l'échelle et de la couleur. | tout le dépôt |
| UX-D6 | Le canvas — surface principale — n'a pas de mode immersif. Un Box à poids fixe 0,55, encadré en permanence. ~45 % d'un écran de téléphone est du chrome + télémétrie en permanence. | MainScreen.kt:522-527, 1456 |
| UX-D7 | Division des panneaux figée à 0,55/0,45 sans contrôle utilisateur. Lire le spectrogramme et surveiller le régime veulent des divisions différentes. Un séparateur déplaçable est l'affordance attendue. | MainScreen.kt:1456-1459 |
| UX-D8 | Les actions du TopAppBar sont deux TextButton plus un logo — l'un en italique sans raison énoncée, et la seule italique de l'application. Trois éléments concurrents consommant l'essentiel de la largeur. | MainScreen.kt:210-234 |
| UX-D9 | Français uniquement, dans le dossier de ressources par défaut. Les 293 chaînes sont en français dans values/ sans values-en/. La mise en page n'a jamais été éprouvée contre des chaînes anglaises/allemandes plus longues — ce que la barre en softWrap = false ne supportera pas. | res/values/strings.xml |
| UX-D10 | Les glyphes d'état sont du texte non mis à l'échelle dans une Row avec un libellé, donc le poids visuel du voyant change avec l'échelle de police indépendamment de son libellé. Un vecteur à taille dp fixe est la construction correcte. | MainScreen.kt:1362-1372, 1416-1432 |
| UX-D11 | EmergenceReportButton est une Surface avec Modifier.clickable à 4 dp de rayon, 6×2 dp d'espacement et 10.sp — une puce faite main bien sous 48 dp, sans bornes de ripple ni sémantique Role.Button. AssistChip fournit tout cela correctement. | MainScreen.kt:1387-1405 |
| UX-D12 | Les sections de SettingsDialog sont Card + bordure manuelle + titre coloré 12.sp, répétées trois fois avec des accents différents et aucun composant partagé — les trois sections d'un même dialogue diffèrent en espacement de son propre rythme. | SettingsDialog.kt:89-300 |
| UX-D13 | Aucun design d'état vide/chargement/erreur. Un voile sur le canvas, une bannière de notice, du texte avec un emoji : trois traitements visuels sans lien pour « rien à afficher ou quelque chose a échoué ». | MainScreen.kt, strings.xml:61 |
HorizontalDivider(thickness = 0.5.dp) au rendu inconstant selon la densité · logo_vibratec.png en raster mono-densité mis à l'échelle 28 dp (flou en xxxhdpi ; devrait être un vecteur) · WAV_NAME_MAX_CHARS = 14 tronquant par nombre de caractères et non par mise en page, donc le point de coupe ignore la largeur disponible · valeurs d'alpha réglées à la main (0,22 / 0,6 / 0,7 / 0,8) plutôt qu'un jeu défini · aucun retour haptique, sur un appareil que la doc de l'application dit opéré sans regarder · Arrangement.spacedBy variant entre conteneurs frères qui se lisent comme pairs.Ce qui est déjà juste
Listé explicitement pour que la remédiation ne le régresse pas. Plusieurs de ces points sont meilleurs que ce que livrent la plupart des applications Android en production — et l'un d'eux, la décision D4, doit être activement défendu contre les recommandations de cet audit lui-même.
theme/Color.kten entier. 60 tokens documentés, ratios de contraste mesurés par token et par surface,PaletteContrastTest, le gate CI sur les hexadécimaux, et la listeNvhOrderTraceArgbpartagée qui rend l'accord PDF/écran démontrable. Travail de qualité référence, et le modèle pour tous les autres axes.- Décision D4 — sombre fixe, pas de couleur dynamique. Correctement argumentée : une couleur dérivée du fond d'écran rendrait les tracés d'ordres et les badges de criticité dépendants de l'appareil, ce qu'une UI de mesure ne peut pas accepter. Toute suggestion « ajouter un thème clair / Material You » doit être refusée sur ce fondement — y compris pendant l'application des recommandations ci-dessus.
- Cibles tactiles de 48 dp, avec le raisonnement documenté sur les opérateurs gantés en véhicule, et la note que les hauteurs fixes de 34–38 dp précédentes tronquaient leurs propres libellés à échelle de police élevée.
- La couleur n'est jamais le seul canal.
GpsLedIndicatorassocie à chaque état une forme distincte et des mots ; la rampe d'émergence est associée à des badges et du texte. Cet invariant doit survivre à la migration vers les icônes. contentDescriptionsur chaque contrôle emoji, avec la note explicite que « sous TalkBack, "⏪" n'est pas un mot ». Les emojis doivent partir ; les libellés doivent rester.- Le
SplashScreende la plateforme remplaçant un splash Compose àdelay(2000), etwindowBackgroundsurnvh_backgroundpour tuer le flash de lancement. - Le motif « deux panneaux déclarés une fois » — les panneaux comme closures
@Composable (Modifier) -> Unit. Le point de rupture est faux (UX-M9) mais la structure est juste, et elle rend la correction peu coûteuse. FilterChippour le sélecteur de métrique. Le bon widget, bien employé. C'est le modèle pour UX-M6.String.format(Locale.ROOT, …)pour chaque affichage numérique, pour qu'une mesure ne change jamais avec la locale.
Feuille de route
La numérotation est un ordre de dépendance réel, pas une décoration. La phase 0 n'est pas optionnelle — toutes les phases suivantes sont invérifiables sans elle, et chaque phase est livrable et relisable indépendamment.
Boucle de retour
Ajouter @Preview pour les deux panneaux et les neuf dialogues, avec @PreviewFontScale et @PreviewScreenSizes. Câbler app/src/androidTest — les dépendances sont déjà déclarées et ne sourcent rien. Ajouter des tests de capture JVM (Roborazzi/Paparazzi) pour que les régressions de mise en page échouent en CI.
Compléter le système de design
Écrire les quinze rôles typographiques plus un rôle readout à chiffres tabulaires. Ajouter Shapes(). Ajouter NvhSpacing. Étendre ci/checks.sh avec les gates qui marchent déjà pour la couleur : pas de fontSize = brut, pas de RoundedCornerShape( brut, pas de dp brut hors du paquet theme. Puis remplacer mécaniquement les 159 littéraux de police et les 275 littéraux dp, en remontant chaque site sous 11.sp au nouveau plancher.
ci/checks.sh passe avec les trois nouveaux gates armés ; diffs de capture relus.Iconographie
Ajouter la dépendance d'icônes ou dessiner ~20 vecteurs. Retirer chaque emoji de strings.xml. Convertir les glyphes d'état en formes vectorielles, en préservant l'invariant « la forme comme second canal ». Vectoriser le logo. Conserver chaque contentDescription.
res/ ; zéro Text("<glyphe>") employé comme icône ; passe TalkBack inchangée.Couche de composants
Extraire NvhButton (avec une vraie hiérarchie primaire/secondaire/tertiaire), NvhCard, NvhSection, NvhReadout, NvhStatusChip, NvhEmptyState. Définir l'échelle d'élévation tonale. Sortir les deux panneaux et le chrome de MainScreen.kt.
AppScreen sous ~300 lignes ; aucun littéral de couleur, taille ou espacement sur un site d'appel.Mise en page & occupation de l'écran
Remplacer les deux Popup par DropdownMenu/ModalBottomSheet, ce qui corrige le bug de densité. Reconstruire la barre inférieure en NavigationBar + FAB. Ajouter material3-window-size-class et brancher sur WindowWidthSizeClass. Supprimer les poids imbriqués morts et les entrées mortes de themes.xml ; gérer les insets explicitement. Ajouter le mode immersif du canvas et un séparateur de panneaux déplaçable.
Contrôles & mouvement
Convertir les bascules de mode en SegmentedButton/FilterChip/IconToggleButton. Unifier les dialogues sur un NvhDialog et déplacer les trois gros vers ModalBottomSheet. Ajouter AnimatedContent sur le mode rapport, AnimatedVisibility sur le lecteur et le bouton Exporter, animateColorAsState sur chaque couleur dépendant du mode. Ajouter l'haptique sur figer, enregistrer et changement de mode.
Finition
Espacements sous-pixel, filets, troncature de nom de fichier par mise en page, jeu d'alpha défini, écarts constants entre frères. Puis un défaut anglais avec surcharge values-fr/ si l'internationalisation est dans le périmètre — en notant que c'est ce qui éprouvera enfin la barre inférieure.
Méthode & limites
Chaque affirmation quantitative de cette page est reproductible depuis la racine du dépôt. Quelques-unes des plus structurantes :
# 159 littéraux de taille de police ; 23 sous 11.sp grep -rn "fontSize = [0-9.]*\.sp" app/src/main/java | wc -l grep -rn "fontSize = \(9\|9\.5\|10\|10\.5\)\.sp" app/src/main/java # Zéro icône, aucune dépendance d'icônes grep -rn "Icon(\|Icons\." app/src/main/java | wc -l grep -rn "icons" app/build.gradle.kts gradle/libs.versions.toml # 275 littéraux dp ; 8 rayons ; aucun Shapes() grep -rho "[0-9.]\+\.dp" app/src/main/java | wc -l grep -rho "RoundedCornerShape([0-9]*\.dp)" app/src/main/java | sort | uniq -c grep -rn "shapes =" app/src/main/java # 6 appels d'animation ; décalages de popup en pixels grep -rho "animate[A-Za-z]*\|AnimatedVisibility\|Crossfade" app/src/main/java | sort | uniq -c grep -rn "IntOffset" app/src/main/java/com/example/nvhspectro/MainScreen.kt # Zéro preview, zéro test instrumenté grep -rn "@Preview" app/src/main/java | wc -l ls app/src/androidTest
SpectrogramColormap.kt représente 1 206 lignes de rendu d'axes, de dégradés de colormap et de lisibilité des surimpressions sur la surface de mesure elle-même. C'est de la présentation de mesure plutôt que du chrome applicatif, et en juger correctement demande de regarder de vrais spectrogrammes. Cela mérite une passe dédiée.