|
| 1 | +--- |
| 2 | +title: Position and Size |
| 3 | +description: How to avoid partial-pixel offsets and sizes in golden tests. |
| 4 | +navOrder: 10 |
| 5 | +--- |
| 6 | +Partial pixel offsets and sizes guarantee that your golden tests will have flaky false |
| 7 | +failures when running on different platforms, or even running on the same platform |
| 8 | +through Docker containers. |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +Most of these situations are outside your control. Your UI is what it is, and that UI |
| 13 | +probably positions some things at partial-pixel offsets, e.g. `(45.7, 203.83)`, and/or |
| 14 | +partial-pixel sizes, e.g. `103.2 x 46.7`. |
| 15 | + |
| 16 | +That said, some steps can be taken to reduce the likelihood and severity of these |
| 17 | +partial-pixel offsets and sizes. |
| 18 | + |
| 19 | +## Golden Test Device Pixel Ratio |
| 20 | +It's important to understand the concept of device pixel ratios and how they relate to |
| 21 | +widget tests and golden tests. |
| 22 | + |
| 23 | +### Background of Logical Pixels |
| 24 | +Over time, screens have evolved to include higher and higher pixel densities. This posed |
| 25 | +a problem for app layouts in earlier years, where developers hard-coded various dimensions, |
| 26 | +which then looked comically tiny on newer device screens. |
| 27 | + |
| 28 | +To help port existing layouts to new screens with higher pixel densities, devices started |
| 29 | +reporting dimensions in terms of ["points" (Apple)](https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Explained/Explained.html) |
| 30 | +and ["density-independent pixels (DIPs)" (Android)](https://developer.android.com/training/multiscreen/screendensities). |
| 31 | +Both of these concepts are similar - they define dimensions in terms of physical distances, |
| 32 | +rather than pixel count. This way, a screen layout that's made for a 400px x 800px |
| 33 | +mobile device will work equally well on a screen with a size of 800px x 1,600px. |
| 34 | + |
| 35 | +Of course, Flutter honors measurements in terms of these physical distances rather than |
| 36 | +pixel distances. In fact, every width, height, x, and y value that you define in Flutter |
| 37 | +is actually a density-independent value, NOT a pixel value. Flutter calls these "logical pixels". |
| 38 | +Flutter tracks and reports the number of "physical pixels" per "logical pixel" in a property |
| 39 | +called [`devicePixelRatio`](https://api.flutter.dev/flutter/dart-ui/FlutterView/devicePixelRatio.html), |
| 40 | +which is published as part of Flutter's `MediaQuery`. |
| 41 | + |
| 42 | +### Device Pixel Ratios in Golden Tests |
| 43 | +With all of that background information, why do pixels, points, and DIPs matter for golden tests? They matter |
| 44 | +because **Flutter attempts to simulate real `devicePixelRatio`s in widget tests** (including golden tests). |
| 45 | +Speaking of which, another detail about widget tests you might know is that **every widget test, |
| 46 | +by default, is configured as if its running on an Android device**. These two facts, combined, |
| 47 | +means that every widget test, by default, simulates a `devicePixelRatio` greater than `1.0`. |
| 48 | +Historically, tests have used a `devicePixelRatio` of `3.0`. This number is likely subject to |
| 49 | +change based on the industry standard at any given time. |
| 50 | + |
| 51 | +Golden Scenes are rendered to physical pixels. Therefore, rendering Golden Scenes requires |
| 52 | +mapping from Flutter's logical pixels to the bitmap's physical pixels. This mapping can change |
| 53 | +whole-number pixel values into partial-pixel values depending on the `devicePixelRatio`. For |
| 54 | +example, imagine a `devicePixelRatio` of `1.75`, and a logical pixel value of `30`. Where would |
| 55 | +that `30` end up in the final bitmap? `30 * 1.75 = 52.5`. Thus, a whole value has become a |
| 56 | +partial-value, and it will now undergo anti-aliasing effects when rendering to the bitmap. |
| 57 | + |
| 58 | +### What To Do About It |
| 59 | +The answer is to change Flutter's test configuration to use a `devicePixelRatio` of `1.0` |
| 60 | +instead of the default. |
| 61 | + |
| 62 | +```dart |
| 63 | +testWidgets("my test", (tester) async { |
| 64 | + // Change the configuration for this test, and reset it after. |
| 65 | + tester.view.devicePixelRatio = 1.0; |
| 66 | + addTearDown(() => tester.view.reset()); |
| 67 | + |
| 68 | + // Now do your real test work... |
| 69 | +}); |
| 70 | +``` |
| 71 | + |
| 72 | +When you use `flutter_test_goldens` test runners, you don't need to worry about this. It's done |
| 73 | +automatically, on your behalf. |
| 74 | + |
| 75 | +```dart |
| 76 | +testGoldenScene("my golden test", (tester) async { |
| 77 | + // Just worry about your test, we've already changed the devicePixelRatio to 1.0... |
| 78 | +}); |
| 79 | +``` |
| 80 | + |
| 81 | +So I guess what we're saying is...if you use `flutter_test_goldens` you don't need to |
| 82 | +worry about anything you just read :) |
| 83 | + |
| 84 | +## Positioning and Sizing Widgets |
| 85 | +You may not think about it, but it's very common for widgets to be sized or positioned |
| 86 | +at partial pixel boundaries. |
| 87 | + |
| 88 | +For example: Imagine a `25x25px` square that's centered within a `100x100` area. The top/left |
| 89 | +offset of the centered square would be `(87.5, 87.5)`. |
| 90 | + |
| 91 | +For example: Imagine a `Row` that's `100px` wide, with 3 squares (`25x25px`) spaced evenly across it. |
| 92 | +Those squares would sit at x-values of `6.25px`, `37.6px`, and `68.75px`, respectively. |
| 93 | + |
| 94 | +You may not be able to control these details within your app UI, but there's one place |
| 95 | +where it's very important to control these values - and that's within Golden Scene layouts. |
| 96 | + |
| 97 | +Typical Golden Scene layouts include rows, columns, grids, and any other layout strategy |
| 98 | +that you choose to employ. A Golden Scene layout is responsible for placing every golden image |
| 99 | +in the scene. These golden images are later extracted for comparison within tests. Therefore, |
| 100 | +it's absolutely critical that a Golden Scene places golden images precisely on whole-pixel |
| 101 | +boundaries. If a Golden Scene positions an individual golden image on a partial boundary, the |
| 102 | +scene will interpolate the color of the pixels on the edge of the golden, which will change |
| 103 | +at least one pixel of detail all around the perimeter of the image. This will cause the golden |
| 104 | +to fail, even when run on the exact same machine that generated it. |
| 105 | + |
| 106 | +To help our team, and your team, to create expressive Golden Scenes without breaking golden |
| 107 | +comparison, we've published variations of a few of your favorite widgets, which now snap |
| 108 | +their children to whole-pixel locations and sizes. |
| 109 | + |
| 110 | + * `PixelSnapCenter`: Like `Center` but with offset and size snapping. |
| 111 | + * `PixelSnapAlign`: Like `Align` but with offset and size snapping. |
| 112 | + * `PixelSnapRow`: Like `Row` but with offset and size snapping. |
| 113 | + * `PixelSnapColumn`: Like `Column` but with offset and size snapping. |
| 114 | + * `PixelSnapFlex`: Like `Flex` but with offset and size snapping. |
| 115 | + |
| 116 | +Take the earlier example of a square centered in a larger area. We can fix that |
| 117 | +situation with a `PixelSnapCenter`. |
| 118 | + |
| 119 | +First, the bad version. |
| 120 | + |
| 121 | +```dart |
| 122 | +SizedBox( |
| 123 | + width: 50, |
| 124 | + height: 50, |
| 125 | + child: Center( |
| 126 | + child: Container( |
| 127 | + width: 24.5, |
| 128 | + height: 24.5, |
| 129 | + color: Colors.red, |
| 130 | + ), |
| 131 | + ), |
| 132 | +); |
| 133 | +``` |
| 134 | + |
| 135 | +The bad version includes a square with a partial-pixel size of `24.5x24.5px`, and that |
| 136 | +square is located at `(12.75, 12.75)`. |
| 137 | + |
| 138 | +Let's snap the offset and the size with `PixelSnapCenter`. |
| 139 | + |
| 140 | +```dart |
| 141 | +SizedBox( |
| 142 | + width: 50, |
| 143 | + height: 50, |
| 144 | + child: PixelSnapCenter( // <-- the change |
| 145 | + child: Container( |
| 146 | + width: 24.5, |
| 147 | + height: 24.5, |
| 148 | + color: Colors.red, |
| 149 | + ), |
| 150 | + ), |
| 151 | +); |
| 152 | +``` |
| 153 | + |
| 154 | +The version with `PixelSnapCenter` positions the square at `(12, 12)` and forces the |
| 155 | +square to become `(24, 24)`. No more partial pixels. |
| 156 | + |
| 157 | +These snapping widgets MUST be used when building Golden Scenes, to ensure that new/updated |
| 158 | +goldens are consistent with extracted goldens. However, if desired, these widgets can also |
| 159 | +be used in your regular widget trees. |
0 commit comments