По любви · Android × Figma · 10.09.2026
Десять экранов — от записи кружка до «Это по любви!» — собраны из данных нод: переменных, стилей, компонентов, дуг, матриц авто-лейаута и матриц обрезки картинок. Каждый кадр снят на устройстве в честном масштабе ×3 (1170×2532) и сведён с рендером Figma. Ниже — кадр рядом с рендером, четыре числа на кадр и разбор того, что оказалось не так. Это продолжение артефактов по кадрам S01–S10 и S11–S20.
Смотровая
Тяните шторку, чтобы совместить макет и приложение. «Карта» показывает, где остаётся расхождение по геометрии: синим закрыты системный статус-бар, полоска home, клавиатура и диалоги разрешений iOS — их рисует Android, они в сравнении не участвуют (спека 6.4).
Числа
Полное — доля пикселей, отличающихся сильнее ΔE 6 вне масок. Без текста — то же с исключёнными текстовыми блоками. ±1 px — пиксель считается расхождением, только если не совпал ни с одним пикселем рендера в окне ±1: сдвиг растра на долю пикселя не есть ошибка вёрстки. Геометрия — два последних вместе, то есть чистая форма, положение и материал; по ней и выносится вердикт. Шкала показывает геометрию относительно порога 0,3 %.
| Экран | Кадр | Состояние | Полное % | Без текста % | ±1 px % | Геометрия % | Шкала до порога 0,3 % |
|---|
Разбор
Каждая строка — расхождение из реестра docs/figma/fig/ledger.md:
что показала нода, что делало приложение и сколько это стоило в числах.
Прогресс записи кружка рисовался полным кольцом. В ноде это дуга: у эллипса
3463:9249 есть arcData с началом 0 и концом 2,29646 рад
(131,58°) на одном кадре и 0,41818 рад (23,96°) на другом, а сам эллипс повёрнут на
−90°. Согласие с рендером проверено по самому рендеру: дуги там ровно 0…132° и 0…24°.
Modifier.size зажимается родителемНа кадре «7 checking photos» фотография 602,8 dp лежит в рамке 338 со сдвигом
−216,76: в макете видна её правая треть. Compose зажимает Modifier.size
шириной родителя, и от снимка оставалась середина — 60,8 % кадра красным. Лечится не
«требуемым размером», а пересчётом: из матрицы обрезки считается общий кроп источника.
Исходники в 4096 px Skia уменьшает не так, как Figma. Картинка заранее приводится к точному размеру ×3, а фильтр пересчёта подобран измерением по рендеру: на уменьшение HAMMING, на увеличение BILINEAR. На кадре «Верификация 3» это сняло 16,88 % пикселей с |Δ|>30 до 1,19 %.
Figma при выравнивании по центру не учитывает пробел в конце строки, Compose учитывает — заголовок уезжал на 4,67 dp. А в одном тексте перенос строки задан символом U+2028: Compose рисует его отдельным глифом-«тофу» и строку не переносит — блок выходил на 35 dp шире. Оба случая закрыты одной нормализацией строки из ноды.
У фотографии селфи в заливке стоят filters: exposure −0,05, contrast +0,09,
highlights −0,37. Повторить формулу нечем, поэтому подбирается поканальная линейная
поправка по самому рендеру устойчивым методом — наложенные сверху элементы из выборки
выбрасываются. Остаток пишется числом, а не словом «похоже».
В шапке ленты число лайков лежит текстом, который в дереве стоит до
navbar-custom и закрыт его белой заливкой. Видимое число даёт барабан
counter-reel — тот его .num-box, что стоит на одной высоте с
самим барабаном. Читать надо барабан, а текст в кадре не виден вовсе.
Индикатор фотографий рисовался кружками. В ките у активной точки контур «O»
4926:17524 10×8, у остальных — шеврон «Union» 4926:17526 8×8 белым
с альфой 0,5. Правка общая для всей ленты: после неё пересняты все 26 замеров экрана
карточки, и геометрия у каждого стала не хуже прежней.
Оверлей лайка — квадрат 635×635 dp в кадре 390. С Modifier.requiredSize
он рисовался 635-м только на бумаге: на снимке выходил 506 dp и вставал не туда.
Проверено отдельным кадром — рамка 200×200 укладывается точно, большая нет. Всё, что
выходит за кадр, кладётся drawImage в заданный прямоугольник.
Задняя карточка стопки рисовалась двумя слоями: прозрачность на родителе 339×581, масштаб на ребёнке 366×627. Ребёнок больше родителя — и содержимое уехало ровно на (627 − 581) / 2 = 22,9 dp вверх. Один слой на элемент: масштаб, прозрачность и точка отсчёта вместе.
Карточка 366×627 под углом 10,42° занимает в колонке 682,87 — по своему повёрнутому габариту, а не по своей высоте. Поэтому описание под ней стоит на 604,87, а не на 549. Без этого всё описание уезжало на 55,87 dp.
Фон экрана «Это по любви!» — три эллипса с размытием слоя 243,3. Compose повторяет такое размытие другим ядром: подгонка по сигме давала |Δ| около 20 единиц канала на всём кадре. Фон положен растром ×3 из рендера — тем же классом, что оверлеи свайпа, где размытие тоже не выражается вектором.
У верхней карточки кадров свайпа поверх фотографии лежит gradient up — 366×80 тёмного с альфой 0,5 → 0,25 → 0,06 → 0. Этого слоя нет ни на одном кадре ленты при той же карточке, и мы его не рисовали: верх карточки выходил светлее рендера в 1,7 раза. Слой нашёлся не глазами: разбор красных пикселей по полосам показал, что три четверти расхождения кадра лежат в верхних 60 dp карточки, а подгонка цвета там дала множитель 1,0 — значит дело не в цвете фотографии, а в отсутствующем слое.
У нижнего градиента карточки blendMode заливки — NORMAL, а у самого узла — MULTIPLY. Читали заливку. Проверено там, где ответ известен точно: фикстура фотографии совпадает с экраном пиксель в пиксель, и почти чёрный низ карточки (13…24 единицы) в рендере уходит в 3, а обычным наложением получалось 24. Правка общая для всей ленты.
absoluteBoundingBox ребёнка повёрнутой карточки — это габарит уже повёрнутой рамки. Имя шириной 137 читалось как 139,08, возраст 29 как 32,86 — и значок проверки, стоящий в ряду после них, уезжал на 5,9 dp. Ровно столько и намерил центр тяжести терракоты значка на рендере против снимка: 5,87. Сторона решается из габарита — и только для узлов внутри самой повёрнутой карточки: колонка описания под ней лежит ровно.
Фонового размытия чипов эмулятор не рисует, поэтому тон чипа снимается медианой с рендера. Там, где под чипом фон резко меняется — «Йога» лежит наполовину на тёмной шторе, наполовину на светлой руке, — медиана мажет половину чипа. Мерой служит среднее отклонение внутри самого чипа на рендере: выше 11 единиц берётся заливка ноды над фотографией, ниже — снятый тон. Порог выбран прогоном обеих веток по всем кадрам, а не на глаз.
Мера
Форма закрывается числом «геометрия». Текст закрывается отдельно: габарит нарисованных глифов из ноды и плотность чернил в блоке. В этой партии выяснилось, что вторая мера годится не везде.
Плотность чернил считалась как «255 − яркость» по прямоугольнику блока. Для тёмного текста на светлой бумаге это верно, а у белого текста поверх фото сумма считает фото. Контроль: те же рамки, сдвинутые с текста на соседний участок фотографии, дали ту же недостачу — −3,80 % против −3,78 %. Теперь замер печатает обе меры — по яркости и по покрытию цветом заливки, — сам сравнивает подложку вокруг блоков и говорит, по какой мере ставить вердикт.
Строки, где методика не сошлась с самим рендером, помечаются и в вердикт не идут: окно поиска чернил захватывает соседний текст того же цвета, и «сдвиг» получается у пары «56» и «%», стоящих в четырёх dp друг от друга. Это правило спеки К-17 — сначала проверить прибор, потом чинить вёрстку.
Вопросы
Здесь не ошибки вёрстки, а места, где макет спорит сам с собой или с собственным рендером. Мы делаем как в рендере и передаём пару дизайнеру.
| Что | Где | Как сейчас |
|---|---|---|
| Заголовок панели | I3041:13063;4300:16385 |
лежит в контейнере 375, а рендер центрирует по кадру 390 — сдвиг +7,33 dp |
| Крестик на экране мэтча | I6139:30944;2858:23480 |
в ноде x=337, в рендере 352 — тот же случай, что с заголовком |
| Счётчик «О себе» | 3258:25396 |
показывает «400 / 2000» при тексте в 96 знаков |
| Число лайков | 4680:23432 и 5411:10557 |
текст под шапкой и барабан рядом — два источника одного числа |
| Ширина «match-rate» | 11508:24391 против 5568:34637 |
87 при «56 %» и 85 при «72 %» — строка чипов от этого едет |
| Градиент внутри повёрнутой карточки | 6119:26783 |
контр-повёрнут на −3° и остаётся горизонтальным, остальные слои повёрнуты |
| Фотография на экране мэтча | imageRef ffae4478… |
картинки нет в контейнере файла — Figma подгружает её извне |
| Кадр «2.1» верификации | 3615:21079 |
в реестре экрана не значится, хотя это его состояние под диалогом |
| Кадр V5 «4» | 16986:55192 |
числится за одним экраном, по содержимому — другой (дерево совпало поимённо) |
| Кадр V5 «1» | 16970:46651 |
значится сразу за двумя экранами |
| Тёмная шапка карточки | 6046:33121 и ещё четыре |
есть на кадрах свайпа и нет ни на одном кадре ленты при той же карточке |
| Шапка кадра «5 like» | 5411:8370 |
без заливки вовсе — сквозь неё видно сердце лайка; на прочих кадрах белая |
Границы
Форма зелёная — это половина работы. Вот честный остаток.
| Что | Почему | Что нужно |
|---|---|---|
| Плотность чернил на четырёх кадрах | у 6014:24550, 6502:21576, 7047:32037 и
7047:32446 мера чернил даёт −3,9…+5,8 % при пределе 2 %: форма
у них закрыта, расходится сам текст |
разобрать метрики шрифта на кадрах анимации — кегли там дробные (17,84 и 22,69) |
| Поведение экранов | трасса запросов против карт Flutter не снята | файловый лог запросов в debug-сборке и инструмент разбора |
| Лента и мэтч в приложении | новые экраны собраны и снимаются фикстурой, но в навигацию не включены | заменить прежние FeedScreen и
MatchOverlay и переснять кадры |
| Сам жест свайпа | кадры — это ключи анимации, а не её воспроизведение | стек карточек, перетаскивание, вылет сердца и возврат анкеты |
| Плотность чернил на семнадцати старых кадрах | по своей мере они выходят за 2 % (−2,08…−3,72 %) | разобрать текст этих экранов отдельно; форма у них закрыта |