Redface 2 · #989 · spike device

Les smileys perso du picker sont trop petits

Sept géométries de grille rendues par le vrai composant, sur un Galaxy S10e réel, avec le corpus perso réel chargé depuis HFR. Objectif : choisir laquelle on livre.

Le constat de départ était juste. La grille est en GridCells.Adaptive(minSize = 48.dp) avec 16 dp de marge et 8 dp d'écart : sur un S10e (360 dp de large) ça tombe sur exactement 6 colonnes de 48,0 dp, et le cap image est un 44 dp en dur.

Mais il y a deux causes distinctes, pas une seule — et un seul des sept presets traite la seconde.

Pourquoi ils sont petits

J'ai téléchargé les 74 smileys perso du top 100 HFR (fixture smileys_top100_roger21.json) et lu leurs dimensions natives dans les octets servis. Au passage : 22 des 74 sont des JPEG servis sous extension .gif avec Content-Type: image/gif — Coil renifle le contenu donc l'app n'en souffre pas, mais l'extension ne dit pas le format sur HFR.

Taille nativeOccurrencesRendu actuel (cap 44)Cause
70×5023 / 7444×31capé en largeur → 17 dp de hauteur perdus
50×50744×44remplit la cellule
67×50 · 64×50 · 70×47644×33même cause que le dominant
39×15 [:rofl]339×15no-upscale → illisible
30×15330×15no-upscale
15×15 → 20×308natif15 dp perdus dans 48 dp de vide

Cause 1 — la cellule est trop petite pour le format dominant. Le corpus est massivement paysage 7:5 (70×50 à lui seul fait 31 % du top 100). Une cellule carrée de 48 dp cape ce format sur la largeur et laisse 17 dp de hauteur inutilisés. C'est structurel : on gaspille du vertical sur presque un tiers du corpus.

Cause 2 — le no-upscale laisse la traîne des petits sprites illisible. La règle scale = min(cap/w, cap/h, 1) interdit tout agrandissement : un [:rofl] de 39×15 natif reste 39×15, quelle que soit la taille de la cellule. Agrandir la cellule ne le touche pas — ça le rend même plus perdu.

Les 7 géométries

Chiffres mesurés sur le device, pas calculés à la main : le bench affiche la géométrie que le solveur de colonnes a réellement résolue. Le nombre de vignettes visibles vient du budget de hauteur de la grille (0,62 × 760 dp = 471 dp).

PresetConfigCol.CelluleCap70×50 rendu[:rofl] 39×15Visibles
A Actuelpad 16 · gap 8 · min 48648,0×48,044,044×3139×1548
B 5 colpad 16 · gap 8 · min 56559,2×59,255,255×39 +25 %39×1535 −13
C Margespad 8 · gap 4 · min 48654,0×54,050,050×36 +14 %39×1548 =
D Les deuxpad 8 · gap 4 · min 56565,3×65,361,361×44 +40 %39×1530 −18
E + paysageD · ratio 7:5565,3×48,061,3×44,061×44 +40 %39×1545 −3
F + upscaleE · plafond ×1,5565,3×48,061,3×44,061×44 +40 %59×23 +51 %45
G Borne hautepad 8 · gap 4 · min 72483,0×59,379,0×55,370×50 natif39×1528 −20

Ce que le tableau dit. Passer à 5 colonnes (B) coûte 13 vignettes pour +25 %. Rogner les marges seules (C) est le seul gain gratuit (+14 %, densité intacte) mais reste modeste. E est le meilleur rapport : même +40 % que D, mais 45 vignettes visibles au lieu de 30, parce qu'abandonner la cellule carrée récupère le vertical que le corpus paysage n'utilisait pas. À la cellule 65,3×48, le 70×50 sature les deux axes en même temps — plus rien n'est perdu. G prouve que le natif plein ne vaut pas le prix : +13 % de plus que E contre 17 vignettes en moins.

Comparaison directe

Les trois premières rangées de A, E, F et G, à l'échelle réelle du device. C'est ici que le flou de l'upscale de F se juge.

Comparaison des presets A, E, F et G sur les trois premières rangées
A → E : la surface du perso dominant double. E → F : les petits sprites (le lapin, le smiley jaune) décollent enfin, au prix d'un ré-échantillonnage visible sur les sprites pixel-art. G : le natif plein, et le vide autour des petits.

Les 7 presets en entier

Preset A
A · actuel
Preset B
B · 5 colonnes
Preset C
C · marges rognées
Preset D
D · les deux, carré
Preset E
E · + cellule paysage
Preset F
F · + upscale ×1,5
Preset G
G · borne haute, 4 col

Le liseré

Un filet de 1 dp en outlineVariant, coins arrondis 8 dp, sur la cellule entière. Sur un corpus aussi hétérogène il donne une limite franche entre deux vignettes de tailles très différentes — au prix d'une grille plus chargée visuellement.

E sans liseré
E sans liseré — les vignettes flottent, les petits sprites n'ont pas d'ancrage visuel.
E avec liseré
E avec liseré — chaque cellule est lisible comme une cible, mais la grille gagne 45 rectangles.

Mode debug — les containers

Le mode que tu as demandé pour aider au choix : il peint la boîte de la cellule, la boîte de l'image, et écrit la taille native mesurée de chaque smiley.

boîte de l'image (ce que la policy a calculé) perso mesuré (sonde Coil arrivée) perso non mesuré (repli cap-filling)
A en mode debug
A · debug — on voit le vide : les boîtes 44×31 dans des cellules 48×48, et les mini-sprites au milieu de rien.
E en mode debug
E · debug — les boîtes 61×44 remplissent la cellule 65,3×48 sur les deux axes.

L'intérêt au-delà de l'esthétique : une vignette peut paraître petite parce que son fichier source est petit, parce que le cap a mordu, ou parce que la sonde de mesure n'a pas encore atterri. Le liseré coloré distingue les trois — sans ça, on risque de juger la politique sur une capture à cache froid.

Régressions vérifiées sur device

Les quatre points que le cadrage désignait comme risques, photographiés plutôt que supposés.

fontScale 1,5 et 2,0

Géométrie strictement inchangée — 5 colonnes, cellule 65,3×48,0, cap 61,3×44,0 aux deux échelles : seule la légende grossit. Aucune régression de layout, la grille du picker est en dp.

Mais ça révèle un écart de fond, qui complète le constat de #989 : le picker est en dp, les smileys d'un post sont en sp. À gros fontScale, un perso est donc plus grand dans le post que dans le sélecteur qui a servi à le choisir — le désaccord picker/renderer déjà noté sur les builtins (20 dp contre 16 sp) a un second visage.

Preset E à fontScale 1,5 puis 2,0
Preset E à fontScale 1,5 (haut) et 2,0 (bas) : mêmes vignettes, légende plus grosse.

Paysage

11 colonnes de 59,1×48,0 dp (cap 55,1×44,0). Le comportement responsive est confirmé : aucune cellule étirée, ce qu'un Fixed(5) en dur aurait produit. À noter : 11 et non les 12 que prédit le test unitaire pour 744 dp — les insets système mangent ~54 dp en paysage, donc la largeur réellement disponible est ~690 dp. La fonction est juste, l'entrée diffère.

Le ratio 7:5 ne s'applique plus ici : 59,1/1,4 = 42,2 dp passerait sous le minimum tactile, donc le plancher de 48 dp gagne — exactement le garde-fou prévu.

Preset E en paysage, 11 colonnes
Preset E en paysage : la grille se re-résout à 11 colonnes.

Onglet Standard — le piège de #816

Les deux onglets partagent cette grille, donc une géométrie choisie pour les persos remodèle aussi les builtins. Et les builtins sont à 20 dp fixes : ils ne suivent pas la cellule. Résultat : plus la cellule grandit, plus la grille Standard se clairseme. Sur A c'est déjà lâche, sur G c'est franchement vide.

Onglet Standard sur les presets A, E et G
Onglet Standard sur A, E et G, liseré activé. Les sprites restent à 20 dp ; seul le vide autour d'eux grandit. Le liseré devient nettement plus utile ici que sur les persos — sans lui, la grille de G n'a plus de structure.

Conséquence actionnable, et elle est bon marché : la géométrie est un paramètre passé par onglet. On peut donc n'appliquer le nouveau spec qu'à l'onglet Wiki et laisser l'onglet Standard sur les 6 colonnes de 48 dp actuelles. C'est une ligne de code, ça évite le compromis de #816, et ça respecte le fait que les deux corpus n'ont pas le même problème : les persos sont trop petits, les builtins sont à la bonne taille.

Ce qu'il reste à trancher

La policy est déjà extraite et testée (14 tests verts, dont 8 qui prouvent que l'extraction n'a rien changé au comportement livré) : le preset retenu devient le fix en changeant les valeurs par défaut, pas en réécrivant du code.

Méthode & garanties

Un bloquant corrigé après relecture

Le gate de code a trouvé un défaut réel dans mon solveur de colonnes : je résolvais en dp avec un epsilon « de prudence », alors que GridCells.Adaptive résout en pixels entiers. À une largeur réelle de 327,5 dp sous densité 2.0, ma version répondait 6 colonnes de 47,92 dp — sous le minimum tactile — là où Adaptive répond 5. L'epsilon censé absorber les arrondis était précisément ce qui faisait basculer le cas limite.

Le solveur travaille désormais en pixels comme l'API, sans epsilon. Effet secondaire bénéfique : cellWidth ≥ minCellWidth devient vrai par construction, donc le minimum tactile horizontal n'a plus besoin de garde séparée. Effet secondaire visible : la cellule compacte vaut 65,3 dp et non 65,6 (division entière en px), et le perso dominant rend à 61×44 au lieu de 62×44 — 1 dp, invisible à l'œil, mais les captures et les chiffres de cette page ont été refaits pour correspondre au code corrigé. Deux tests pinnent le cas : le contre-exemple exact, et un balayage de 7 densités × 115 largeurs vérifiant que la cellule ne descend jamais sous le minimum.

Ce qui n'est pas encore couvert (à faire si un preset est retenu) : fontScale 1,5 et 2,0, le paysage et le split-screen photographiés, et l'onglet Standard (les builtins à 20 dp partagent cette grille — le piège de #816, où un réglage unique rendait les builtins gros et flous et les persos à l'étroit).