Sharp looking Flutter applications with fractional device pixel ratios.

Related tags

Templates pixel_snap
Overview

PixelSnap - Sharp looking applications at any pixel scaling factor

If you have ever run a Flutter application on a system with non-integral pixel scaling, such as Windows at 150%, you may have noticed that your beautiful Flutter application suddenly looks blurry. But why? You're not using any bitmaps, Flutter draws everything with vector. So where's the blurryness coming from?

Logical vs Physical Pixels

The Flutter coordinate system uses logical pixels, which means that all paddings, insets, sizes, and border widths are specified in terms of logical pixels. The display however, is make of physical pixels and the ratio between logical and physical pixels is called pixel device ratio. If device pixel ratio is 1, one logical pixel represents exactly 1 physical pixel. If the device ratio is 2, one logical pixel will translate to two physical pixels.

The problem arises when the device pixel ratio is not an integer. In this case, a logical pixel with a ratio of 1.5 will result in 1.5 physical pixels. A line with a logical width of 1 pixel will render as 1.5 physical pixels wide. Because it is not possible to lit up fractional values of physical pixels, the line will be anti-aliased and thus will look blurry.

blurry-border

1px border being blurry at 150% pixel scaling.

Non integral pixel ratios are very common on Windows. If you're developing your application on a Mac with 2.0 scaling (which is probably the most forgiving one), you might not even be aware that you have a problem until you run your application on a Windows machine for the first time.

2px stroke aligned to pixel boundary 2px stroke not aligned to pixel boundary

How do we solve this?

By trying really hard to make sure that everything lands on physical pixel boundary of course :)

Let's say that you have 125% scaling and want to draw border that will be exactly 1 physical pixel wide. You can't just set the border width to 1 logical pixel, because that will result in 1.25 physical pixels. Instead, you need to set the border width to 0.8 logical pixels. This will result in exactly 1 physical pixel.

You will need to do the same thing with padding, insets and any kind of explicit size that you have.

But that's not enough. In additon you'll need to make sure that:

  • Any kind of widget that sizes itself needs snap the size to physical pixels.
  • Any widget that positiones children (i.e. Align, Flex) needs to make sure that the child will land on physical pixel boundary. For example centering widget in Flutter can sometimes result in blurry child even at 100% scaling, because the child may be positioned at half physical pixel.
  • Layout widget that fills area with multiple children needs to make sure that children sizes are properly snapped to physical pixels, while ensuring that the area is covered. If you have row with 3 children filling 100 physical pixels, the children will need to be sized at 33, 33 and 34 physical pixels exactly.
  • Whenever the device pixel ratio changes, you need to recompute the layout to ensure that the conditons above are met.

PixelSnap for the rescue

Doing all these things ad-hoc and manualy would by a lot of work and possibly very error prone. Fortunately, you don't have to. PixelSnap can help in multiple ways:

Extension methods for easy pixel snapping

You can use the pixelSnap() extension method to pixel snap numeric values, Size, EdgeInsets, Rect, Offset, Decoration and other basic Flutter classes.

For example:

    final ps = PixelSnap.of(context);
    final widget = Container(
        width: 10.pixelSnap(ps),
        padding: const EdgeInsets.all(10).pixelSnap(ps),
        decoration: BoxDecoration(
            border: Border.all(
                width: 1.pixelSnap(ps),
                color: Colors.black,
            ),
        ),
    );

    // To snap single value you can use:
    final width = 10.pixelSnap(ps);

    // Or call the PixelSnap instance directly:
    final width = ps(10);

Pixel-snapped widgets

Now this already looks like an impovement, but still seems very manual. And what about layout? How will this help with Align, Row or Column? Sure we can do better?

Why yes, we can. PixelSnap comes with thin wrappers around many Flutter widgets that already do the pixel snapping for you. To use this, simply import

import 'package:pixel_snap/widgets.dart';

instead of the standard

import 'package:flutter/widgets.dart';

This replaces some of the basic Flutter widgets with pixel-snapping alternatives and reexport all other Flutter widgets.

If you're using material or cupertino import this instead:

import 'package:pixel_snap/material.dart';
import 'package:pixel_snap/cupertino.dart';

Note that this will reexport the original (unmodified) material and cupertino widgets in additon to the pixel-snapped alternatives to standard widgets.

With this in place, the example above can be rewritten as:

    final widget = Container(
        width: 10,
        padding: const EdgeInsets.all(10),
        decoration: const BoxDecoration(
            border: Border.all(
                width: 1,
                color: Colors.black,
            ),
        ),
    );

This import will also give you modified Flex, Row and Column widgets that will ensure that the all children are properly pixel-snapped.

Here is list of Widgets that has been amended to pixel-snap automatically:

  • Column
  • Text
  • RichText
  • Center
  • FractionallySizedBox
  • Align
  • Baseline
  • ConstrainedBox
  • DecoratedBox
  • Container
  • FittedBox
  • IntrinsicWidth
  • LimitedBox
  • OverflowBox
  • Padding
  • SizedBox
  • SizedOverflowBox
  • Positioned
  • PhysicalModel
  • CustomPaint
  • Icon
  • Image
  • ImageIcon
  • AnimatedAlign
  • AnimatedContainer
  • AnimatedCrossFade
  • AnimatedPositioned
  • AnimatedPhysicalModel
  • AnimatedSize

If you stick to these, you application should be pixel-perfect with very little additional work.

  • If you're need to use Widget that sizes itself, you can wrap it in a PixelSnapSize widget. This will extend the size of widget to nearest physical pixel and thus ensure that this widget, when put in your pixel-snapped widget hierarchy won't disturb the overal layout.

  • If you are using foreign widgets that are not physical pixels aware but are customizable enough so that they let you specify padding, insets or border, you can use the .pixelSnap() extension method to pixel snap them.

  • For scroll views you can use PixelSnapScrollController or simply ScrollController after importing pixel_snap/widgets.dart or pixel_snap/material.dart. The scroll controller will ensure that the scroll offset is always snapped to physical pixels.ƒ

Simulating different device pixel ratios

PixelSnap comes with PixelSnapDebugBar widget. You can put it above your applicaiton widget (it should be the top level widget) and it will give you a bar that can be used to switch between simulated device pixel ratios and turn pixel snapping on and off.

See the example app for more details.

Screenshot 2022-12-05 at 23 38 25

Screenshot of PixelSnapDebugBar in action. The image might be displayed blurred, but the actual application has perfectly sharp 2px wide border lines on simulated 1.75x device pixel ratio.


Screenshot 2022-12-05 at 23 42 27

Same application with pixel snapping disabled. If you zoom-in, you can see that instead of sharp 2px black borders, most of the lines are 3px with varing shade of gray (due to antialiasing).

Pixel snapping function

The default pixel snapping function was chosen to give the following result:

Logical pixels Scaling Factor Physical pixels
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
... ... ...

Other considerations

Pixel snapping and arbitrary transform

In Flutter, render objects do not generally know their position on screen during layout. For pixel snapping to work, the render objects can not have an arbitrary scale / rotation transform in their ancestor. And all the translation transform must be properly pixel-snapped.

This should not be a problem in practice. Desktop application are generally not using arbitrary scale / rotation transforms, and if they are, they are usually localized and only used during temporary events such as transitions.

Using pixel snapping with arbitrary transforms will produce a result that is "slightly wrong", but since the transform likely shift things outside of pixel boundaries anyway, the distortion should be hard to notice.

Disabling pixel snapping for part of the application

It is possible to disable pixel snapping for arbitrary subviews using the PixelSnapOverride widget:

PixelSnapOverride(
    pixelSnapFunction: (value, _, __) => value, // Disable pixel snapping
    child: ...

Please note that this may result in a widget that has a size that is not properly pixel snapped and might affect other widgets around it. To prevent that, you can wrap the widget in a PixelSnapSize widget:

PixelSnapSize( // Ensure that child size is properly pixel snapped
    child: PixelSnapOverride(
    pixelSnapFunction: (value, _, __) => value,
    child: ...
You might also like...

Upload Files To Firebase Storage with Flutter. Pick images, videos, or other files from your device and upload them to Firebase.

Upload Files To Firebase Storage with Flutter. Pick images, videos, or other files from your device and upload them to Firebase.

Flutter Tutorial - Upload Files To Firebase Storage Upload Files To Firebase Storage with Flutter. Pick images, videos, or other files from your devic

Dec 28, 2022

A Flutter plugin to request the device unlock screen.

device_unlock A Flutter plugin to request the device unlock screen on Android and iOS. How does it work The following attempts and fallbacks are made:

Sep 7, 2022

A Flutter widget that forces the device rotates into the set of orientations the application interface can be displayed in.

A Flutter widget that forces the device rotates into the set of orientations the application interface can be displayed in. Features Force device keep

Nov 30, 2021

A Flutter plugin for iOS and Android allowing access to the device cameras.

Camera Plugin A Flutter plugin for iOS and Android allowing access to the device cameras. Note: This plugin is still under development, and some APIs

Mar 17, 2020

A Flutter repo with a ready-to-go architecture containing flavors, bloc, device settings, json serialization and connectivity

A Flutter repo with a ready-to-go architecture containing flavors, bloc, device settings, json serialization and connectivity

Flutter Ready to Go A Flutter repo with a ready-to-go architecture containing flavors, bloc, device settings, json serialization and connectivity. Why

Nov 11, 2022

A responsive scaffold widget that adjusts to your device size, for your flutter mobile and web apps.

A responsive scaffold widget that adjusts to your device size, for your flutter mobile and web apps.

scaffold_responsive A responsive scaffold widget that adjusts to your device size, for your flutter mobile and web apps. Check out the Live demo here

Sep 27, 2022

A simple flutter app that downloads a file from the internet, shows a custom-made download progress dialog and saves the file to device's internal storage

http_downloader A simple flutter app that downloads a file from the internet using the http plugin. It has a custom-designed progress dialog which dis

Apr 6, 2021

Flutter Plugin used to query audios/songs infos [title, artist, album, etc..] from device storage.

Flutter Plugin used to query audios/songs infos [title, artist, album, etc..] from device storage.

on_audio_query on_audio_query is a Flutter Plugin used to query audios/songs 🎶 infos [title, artist, album, etc..] from device storage. Help: Any pro

Dec 10, 2022

A Flutter plugin for Android and iOS allowing access to the device cameras, a bit deeper!!

flutter_cameraview A Flutter plugin for Android and iOS allowing access to the device cameras, a bit deeper!!. This plugin was created to offer more a

Oct 22, 2022
Comments
  • Feature request: ListView

    Feature request: ListView

    Feature request

    Thank you for your amazing work. I'm having a problem with ListView, as shown in the following code and images. When I use import 'package:flutter/material.dart';, there seems to be a spacing between the images in some window sizes.

    import 'package:flutter/gestures.dart';
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Tests',
          home: const MyHomePage(),
          debugShowCheckedModeBanner: false,
          scrollBehavior: const MaterialScrollBehavior().copyWith(
            dragDevices: PointerDeviceKind.values.toSet(),
          ),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      const MyHomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.black,
          body: ListView(
            children: List.generate(10, (index) {
              return const Image(
                image: NetworkImage('https://placehold.jp/fff/ccc/3333x317.png'),
              );
            }),
          ),
        );
      }
    }
    
    

    Snipaste_2022-12-15_12-31-41

    When I try to use import 'package:pixel_snap/material.dart';, the behavior changes to always having one pixel between images at some window sizes.

    Snipaste_2022-12-15_12-34-49

    Although this looks slightly better, it's still not "pixel-perfect". Could you please provide support for ListView, or give me any other suggestions to try?

    If you think this is not a feature of the current plugin and that this is a problem with Flutter itself, I will close this feature request. Again, thank you for your work.

    Environments

    • Flutter version: 3.3.3
    • Windows scale: 125%
    • Windows version: 21H2
    • Test application size: 1584 x 892 px (Default)
    opened by hgraceb 5
  • applyParentData

    applyParentData

    Hello,

    First off, thank you for this package. I found it by accident where I was so frustrated about my app's look on Windows. This package actually solves a part of the problem. The other part on Windows is how fonts look. I wish you had a solution for that too :)

    Anyway I have this custom class which overrides applyParentData on Positioned widget. When switching to pixel snap, I get this error:

    The method 'applyParentData' isn't defined in a superclass of 'CustomPositioned'.
    Try correcting the name to the name of an existing method, or defining a method named 'applyParentData' in a superclass.
    
    class CustomPositioned extends Positioned
    {
      const CustomPositioned({
        super.key,
        super.left,
        super.top,
        super.right,
        super.bottom,
        super.width,
        super.height,
        required super.child,
      }) : assert(left == null || right == null || width == null),
           assert(top == null || bottom == null || height == null);
    
    	@override
    	void applyParentData(RenderObject renderObject)
    	{
    		// Some logic codes here
    
    		super.applyParentData(renderObject);
    	}
    }
    

    For that class, I had to revert back to material/widgets.dart

    opened by asmith20002 2
Owner
null
SSH no ports provides ssh to a remote Linux device with out that device having any ports open

Ssh! No ports ssh no ports provides a way to ssh to a remote linux host/device without that device having any open ports (not even 22) on external int

The Atsign Foundation 224 Dec 21, 2022
A collection of pixel-perfect iOS-styled components and properties for Flutter, following the official guidelines.

cupertinew ⚠️ Experimental and work in progress ⚠️ A collection of pixel-perfect iOS-styled components and properties for Flutter, following the offic

Jeroen Meijer (Jay) 30 Nov 10, 2022
🔥🚀 Flutter package to create Pin code input text field with every pixel customization possibility 🎨 with beautiful animations

Flutter PinPut From Tornike ?? ?? Flutter package to create Pin code input (OTP) text field with every pixel customization possibility ?? and beautifu

Tornike 451 Jan 2, 2023
1000+ Pixel-perfect svg unicons for your next flutter project

flutter_unicons 1000+ Pixel-perfect svg unicons for your next flutter project ispired by Unicons and vue-unicons Demo Download the gallery here. Insta

charlot Tabade 23 Oct 9, 2022
A pixel perfect dashboard for mobile, tablet and desktop.

A pixel perfect dashboard for mobile, tablet and desktop.

BotCoder 25 Dec 27, 2022
A Flutter application about finding a specific person for a job you're looking for to hire

flutter_app2 A new Flutter application. Getting Started This project is a starting point for a Flutter application. A few resources to get you started

Niyaz Irfan 1 Dec 16, 2021
Cross-platform flutter plugin for reading and writing NFC tags. Not maintained anymore - not looking for new maintainer, fork instead.

nfc_in_flutter NFC in Flutter is a plugin for reading and writing NFC tags in Flutter. It works on both Android and iOS with a simple stream interface

Andi Semler 113 Sep 28, 2022
Do more - A good looking glorified todo list built with flutter.

do_more (DO>) A glorified todo list with a beautiful ui. Login with Google Events Organize your tasks by events, visualize them in their page and add

Mariano Uvalle 28 Nov 22, 2022
[Looking for maintainers] Application de Minitel

Minitel App Screenshots Features Authentication Apps Sogo Mail Portail EMSE Imprimante Wiki Minitel News Calendar Maps Report to Minitel Development M

Marc Nguyen 4 May 9, 2022
A Clean looking modren and colorful notes app using a local database with sqflite

A Clean looking modren and colorful notes app using a local database with sqflite

Moaz Talaat 3 Sep 23, 2022