Line height в Android TextView: где не сходится с Figma, как мешает pixel-perfect, и как это решить

Всем привет! Сегодня от лица команды Avito Android Design System поговорим о pixel-perfect-верстке, –, а если более точно, о pixel-perfect в Android TextView.

В крупных проектах уделяется большое внимание деталям: дизайнеры, аналитики и продуктовые менеджеры продумывают все до мелочей, чтобы максимально улучшить пользовательский опыт. В дизайне такой важной деталью становится pixel-perfect-верстка: дизайнеры кропотливо и выверено создают визуальный язык продукта, чтобы пользовательский опыт был интуитивно понятным, простым и гармоничным. Разработчики, в свою очередь, всеми силами стараются реализовать все тонкости дизайна, но иногда по разным причинам это невозможно. Одна из таких причин — расхождение платформенного рендеринга Android TextView и Figma Text.

ef6964a129f02bf0f6a219225b291659.jpg

Line height в Figma и Android

Когда дизайнеры рисуют макеты в Figma, они добавляют в них надписи с помощью компонента Text, а мы, Android-разработчики, используем его аналог на нашей платформе — TextView.

Text и TextView обладают идентичными параметрами, как, например, размер текста, его цвет, стилизация (bold, italic, normal) и так далее. Все эти параметры между Figma и Android работают идентично. Но есть одно но: Line height (высота линии) работает по-разному. Именно о проблеме неконсистентности Line height между Figma и Android сегодня и пойдёт речь.

Рассмотрим на примере одного из наших текстовых стилей Дизайн Системы.

d503ea7973e0d8df6450c8dd79041e58.png

В стиле указана высота линии 22. Что примечательно, высота самого компонента Text равна высоте линии. Доверимся значениям и перенесём этот параметр as is из Figma в Android TextView.

Запускаем приложение и смотрим результат через Layout Inspector:

TextView.height = 20

TextView.height = 20

Видим неконсистентность: несмотря на то, что мы указали высоту линии в TextView, по факту TextView имеет другую высоту. Именно эта проблема долгое время досаждала разработчикам Авито: скажем, есть макет, в котором Text находится под Button с отступом в 12 px. Если представить, что Button.bottom — это y = 0, то TextView.bottom, по макету, должен находиться на y = 12 + 22 = 34. Но по факту TextView.bottom находится на y = 12 + 20 = 32. 

Может быть, звучит не так критично, но, во-первых, это уже не pixel-perfect, во-вторых, расхождение с Figma растёт прямо пропорционально количеству элементов Text на экране. Чтобы бороться с этой проблемой, поддерживать консистентность и проходить дизайн-ревью экранов, разработчики Avito добавляют компенсации, увеличивая внешние отступы текста. 

Вроде бы таким образом проблема решается, но, если задуматься, что буквально на каждом экране приложения Avito (да и любого приложения в принципе) есть TextView, то становится понятно, что много лишнего времени тратится на поиск расхождения, вычисление необходимой компенсации и добавление ее в текст. И вот совсем недавно мы, наконец, нашли решение этой проблемы.

Отличия логики параметра Line height в Android и Figma

Спустя много часов дебага и ресёрча, мы выяснили несколько важных фактов:

Высота TextView в Android рассчитывается по формуле textSize * 1.33 (если включены все дефолтные параметры, как, например, includeFontPadding = true)

Параметр lineHeight, который мы указывали до этого, начинает иметь значение только тогда, когда TextView становится многострочной: lineHeight — это просто умная обертка над параметрами lineSpacingExtra и lineSpacingMultiplier.

19087ea46a667b4909b31d9106a30411.png

Проверим?

TextView.height = 42

TextView.height = 42

Кажется, работает: изначальная высота 20 + указанная высота линии 22 = 42.

Но и тут неконсистентность: подобный Text в Figma имел бы высоту 44 (lineHeight * nLines = 22×2 = 44).

Как решить проблему расхождения высоты текста между Android и Figma

Буквально недавно, разбираясь в другой проблеме TextView, случайно наткнулись на статью Hard grids & baselines: How I achieved 1:1 fidelity on Android и указанные в ней примечательные атрибуты: firstBaselineToTopHeight и lastBaselineToBottomHeight. Названия атрибутов, на первый взгляд, выглядят громоздко и наводят страх, но именно они и помогают решить проблему расхождения высоты текста.

baseline — это линия, на которой «лежат» все буквы текста. В основном все буквы уходят вверх над baseline, но есть исключения, как, например, у строчной буквы «у» или «ф», у которых хвостик уходит ниже baseline.

firstBaselineToTopHeight — высота от первого baseline текста до верха TextView.

lastBaselineToBottomHeight — высота от последнего baseline текста до низа TextView.

В случае с однострочным TextView firstBaseline == lastBaseline, т.е. атрибуты имеют ту же силу для таких случаев.

Возникает вопрос: как нам узнать значения этих параметров из Figma, чтобы перенести их в Android? Честно говоря, мы пока не придумали ничего лучше, чем просто глазами посчитать квадратики-пиксели.

b2531734992fdb9089a718cf63c2736a.png

Если хорошенько приблизить стиль нашего текста, то становится видно, что от baseline вниз 5 пикселей, а вверх 17 (верхний еще можно считать не глазами, а посчитать как 22 — 5, где 22 — высота линии, а 5 — отступ снизу).

Устанавливаем данные значения для нашего TextView:

Запускаем и радуемся!

51850254254bd8c4f2eb93fec293a100.png

И для многострочного:

83cc6ade50f9aef43be5090c5ea156ce.png

Ура! Проблема решена!

Подводные камни 

Есть несколько подводных камней, о которых тоже расскажем вкратце. Во-первых, данный функционал не завёлся сразу для русского языка на Android API >= 33.

Мы потратили целый день на дебаг проблемы и поиск решения,   и в процессе наткнулись на интересную ссылку: https://developer.android.com/about/versions/13/features#line-height

ee94819ae47fd42b6790229ee54b2ee4.png

В документации нигде не написано, как это выключить, однако мы нашли параметр TextView, который выключает эту «улучшенную» высоту линии: isFallbackLineSpacing = false. Про fallbackLineSpacing можете почитать отдельно, на нём подробно останавливаться не будем.

Во-вторых, данный функционал не работает для больших размеров текста, близких к 30. Как решить проблему — пока не разобрались, но и не критично, т.к. в мобильном приложении Avito такие тексты используются крайне редко.

В-третьих, если посмотреть подкапотную логику атрибутов firstBaselineToTopHeight и lastBaselineToBottomHeight, можно заметить, что они работают за счет выставления paddingTop и paddingBottom для TextView. Это в свою очередь сбрасывает указанный вами padding для TextView. Эту проблему можно решить за счет указания margins вместо paddings.

В-четвёртых, пока не разбирались, присутствует ли подобная разница рендеринга высоты линии между Jetpack Compose Text и Figma Text, — и если да, то как решить данную проблему в Compose.

Исходя из того, что функционал меняет как саму высоту TextView, так и её paddings, мы в Avito раскатывали данный функционал под feature-flag«ом, чтобы все разработчики смогли оценить на своих экранах влияние этих атрибутов, убрать лишние добавленные ранее компенсации и заменить paddings на margins.

© Habrahabr.ru