Si vous avez déjà exécuté une application Flutter sur un système avec une mise à l’échelle des pixels non intégrale, comme Windows à 150 %, vous avez peut-être remarqué que votre belle application Flutter semble soudainement floue. Mais pourquoi? Vous n’utilisez aucun bitmap, Flutter dessine tout avec le vecteur. Alors d’où vient le flou ?
Pixels logiques vs physiques
Le système de coordonnées Flutter utilise des pixels logiques, ce qui signifie que tous les rembourrages, encarts, tailles et largeurs de bordure sont spécifiés en termes de pixels logiques. Cependant, l’affichage est constitué de pixels physiques et le rapport entre les pixels logiques et physiques est appelé rapport de périphérique de pixel. Si le ratio de pixels de l’appareil est de 1, un pixel logique représente exactement 1 pixel physique. Si le ratio de périphérique est de 2, un pixel logique sera converti en deux pixels physiques.
Le problème survient lorsque le rapport de pixels de l’appareil n’est pas un nombre entier. Dans ce cas, un pixel logique avec un rapport de 1,5 donnera 1,5 pixel physique. Une ligne d’une largeur logique de 1 pixel sera restituée avec une largeur de 1,5 pixel physique. Puisqu’il n’est pas possible d’éclairer des valeurs fractionnaires de pixels physiques, la ligne sera anticrénelée et paraîtra donc floue.
La bordure de 1px est floue à une mise à l’échelle de 150 % des pixels.
Les ratios de pixels non intégraux sont très courants sous Windows. Si vous développez votre application sur un Mac avec une mise à l’échelle 2.0 (qui est probablement la plus indulgente), vous ne savez peut-être même pas que vous avez un problème jusqu’à ce que vous exécutiez votre application sur une machine Windows pour la première fois.
Trait de 2 pixels aligné sur la limite des pixels | AVC 2px ne pas aligné sur la limite des pixels |
---|---|
![]() |
![]() |
Comment résoudre ce problème ?
En essayant vraiment dur pour s’assurer que tout atterrit sur la limite physique des pixels bien sûr 🙂
Supposons que vous ayez une mise à l’échelle de 125 % et que vous souhaitiez dessiner une bordure d’exactement 1 pixel physique de large. Vous ne pouvez pas simplement définir la largeur de la bordure sur 1 pixel logique, car cela se traduira par 1,25 pixels physiques. Au lieu de cela, vous devez définir la largeur de la bordure sur 0,8 pixel logique. Cela se traduira par exactement 1 pixel physique.
Vous devrez faire la même chose avec le rembourrage, les encarts et tout type de taille explicite que vous avez.
Mais cela ne suffit pas. De plus, vous devrez vous assurer que :
- Tout type de widget qui se dimensionne lui-même doit adapter la taille aux pixels physiques.
- Tout widget qui positionne les enfants (c’est-à-dire Aligner, Flex) doit s’assurer que l’enfant atterrira sur la limite physique des pixels. Par exemple, le centrage du widget dans Flutter peut parfois entraîner un enfant flou même à une mise à l’échelle de 100 %, car l’enfant peut être positionné à un demi-pixel physique.
- Le widget de mise en page qui remplit la zone avec plusieurs enfants doit s’assurer que les tailles des enfants sont correctement alignées sur les pixels physiques, tout en s’assurant que la zone est couverte. Si vous avez une ligne avec 3 enfants remplissant 100 pixels physiques, les enfants devront être dimensionnés à 33, 33 et 34 pixels physiques exactement.
- Chaque fois que le ratio de pixels de l’appareil change, vous devez recalculer la mise en page pour vous assurer que les conditions ci-dessus sont remplies.
Faire toutes ces choses ad hoc et manuellement serait beaucoup de travail et peut-être très sujet aux erreurs. Heureusement, vous n’êtes pas obligé. PixelSnap peut vous aider de plusieurs manières :
Méthodes d’extension pour une capture facile des pixels
Vous pouvez utiliser le pixelSnap()
méthode d’extension aux valeurs numériques d’accrochage aux pixels, Size
, EdgeInsets
, Rect
, Offset
, Decoration
et d’autres classes Flutter de base.
Par exemple:
final widget = Container(
width: 10.pixelSnap(),
padding: const EdgeInsets.all(10).pixelSnap(),
decoration: BoxDecoration(
border: Border.all(
width: 1.pixelSnap(),
color: Colors.black,
),
),
);
// You can use .ps shorthand for numeric values.
final width = 10.ps; // same as 10.pixelSnap()
Widgets capturés par pixel
Maintenant, cela ressemble déjà à une amélioration, mais semble toujours très manuel. Et qu’en est-il de la mise en page ? Comment cela aidera-t-il avec Align
, Row
ou Column
? Sûr qu’on peut faire mieux ?
Pourquoi oui, nous le pouvons. PixelSnap est livré avec de minces enveloppes autour de nombreux widgets Flutter qui effectuent déjà la capture de pixels pour vous. Pour l’utiliser, importez simplement
import 'package:pixel_snap/widgets.dart';
au lieu de la norme
import 'package:flutter/widgets.dart';
Cela remplace certains des widgets Flutter de base par des alternatives de capture de pixels et réexporte tous les autres widgets Flutter.
Si vous utilisez du matériel ou cupertino, importez ceci à la place :
import 'package:pixel_snap/material.dart';
import 'package:pixel_snap/cupertino.dart';
Notez que cela réexportera le matériau d’origine (non modifié) et les widgets cupertino en plus des alternatives de capture de pixels aux widgets standard.
Avec cela en place, l’exemple ci-dessus peut être réécrit comme suit :
final widget = Container(
width: 10,
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
border: Border.all(
width: 1,
color: Colors.black,
),
),
);
Cette importation vous donnera également modifié Flex
, Row
et Column
widgets qui garantiront que tous les enfants sont correctement capturés en pixels.
Voici la liste des widgets qui ont été modifiés pour s’accrocher automatiquement aux pixels :
- Colonne
- Texte
- Texte riche
- Centre
- FractionallySizedBox
- Aligner
- Ligne de base
- BoîteContrainte
- DécoréBoîte
- Récipient
- Boîte ajustée
- LargeurIntrinsèque
- LimitedBox
- OverflowBox
- Rembourrage
- SizeBox
- SizeOverflowBox
- Positionné
- ModèlePhysique
- Peinture personnalisée
- Icône
- Image
- ImageIcône
- AniméAligner
- ConteneurAnimé
- FonduCroiséAnimé
- AniméPositionné
- ModèlePhysiqueAnimé
- Taille animée
Si vous vous en tenez à ceux-ci, votre application devrait être parfaite au pixel près avec très peu de travail supplémentaire.
-
Si vous avez besoin d’utiliser un widget qui se dimensionne lui-même, vous pouvez l’envelopper dans un
PixelSnapSize
widget. Cela étendra la taille du widget au pixel physique le plus proche et garantira ainsi que ce widget, lorsqu’il sera placé dans votre hiérarchie de widgets capturés par pixel, ne perturbera pas la disposition globale. -
Si vous utilisez des widgets étrangers qui ne reconnaissent pas les pixels physiques mais qui sont suffisamment personnalisables pour vous permettre de spécifier un remplissage, des encarts ou une bordure, vous pouvez utiliser le
.pixelSnap()
méthode d’extension pour les capturer en pixels.
Simulation de différents ratios de pixels d’appareils
PixelSnap est livré avec PixelSnapDebugBar
widget. Vous pouvez le placer au-dessus de votre widget d’application (ce devrait être le widget de niveau supérieur) et il vous donnera une barre qui peut être utilisée pour basculer entre les ratios de pixels de l’appareil simulé et activer et désactiver la capture de pixels.
Voir l’exemple d’application pour plus de détails.
Capture d’écran de
PixelSnapDebugBar
en action. L’image peut être affichée floue, mais l’application réelle a des lignes de bordure de 2 pixels de large parfaitement nettes sur un rapport de pixels de périphérique simulé de 1,75x.
Même application avec la capture de pixels désactivée. Si vous effectuez un zoom avant, vous pouvez voir qu’au lieu de bordures noires nettes de 2 pixels, la plupart des lignes sont de 3 pixels avec une nuance de gris variable (en raison de l’anticrénelage).
Fonction de capture de pixels
La fonction de capture de pixels par défaut a été choisie pour donner le résultat suivant :
Pixels logiques | Facteur d’échelle | Pixels physiques |
---|---|---|
1 | 1 | 1 |
1 | 1.25 | 1 |
1 | 1.5 | 1 |
1 | 1,75 | 2 |
1 | 2.0 | 2 |
1 | 2.25 | 2 |
1 | 2.5 | 2 |
1 | 2,75 | 3 |
… | … | … |
Autres considérations
Capture de pixels et transformation arbitraire
Dans Flutter, les objets de rendu ne connaissent généralement pas leur position à l’écran lors de la mise en page. Pour que la capture de pixels fonctionne, les objets de rendu ne peuvent pas avoir une transformation d’échelle/rotation arbitraire dans leur ancêtre. Et toutes les transformations de traduction doivent être correctement capturées en pixels.
Cela ne devrait pas poser de problème dans la pratique. Les applications de bureau n’utilisent généralement pas de transformations d’échelle/rotation arbitraires, et si elles le sont, elles sont généralement localisées et utilisées uniquement lors d’événements temporaires tels que des transitions.
L’utilisation de la capture de pixels avec des transformations arbitraires produira un résultat « légèrement faux », mais comme la transformation déplace probablement les choses en dehors des limites de pixel de toute façon, la distorsion devrait être difficile à remarquer.
Reconstruction après modification du ratio de pixels de l’appareil
PixelSnap détectera les changements dans le rapport de pixels de l’appareil et forcera le réassemblage de l’ensemble de l’application. Étant donné qu’il existe potentiellement de nombreux calculs nécessitant le rapport de pixels du périphérique de fenêtre actuel, l’obtention du rapport via MediaInfo.of(context)
est trop cher. C’est également peu pratique (nous avons besoin du rapport lors de la mise en page dans les objets de rendu et dans les méthodes d’extension). Contournement MediaInfo
signifie que Flutter ne sait pas quels widgets dépendent du ratio de pixels de l’appareil, nous finissons donc par reconstruire l’arbre entier.
Ce n’est pas idéal, mais changer le ratio de pixels de l’appareil est déjà une opération coûteuse, donc le compromis semble acceptable. Le fait qu’il manque peut-être une image d’animation dans une situation où la fenêtre se redimensionne lorsqu’elle est déplacée sur les moniteurs ne semble pas être un gros problème.
GitHub
Voir Github