Однажды я установил приложение по текущему проекту на свой смартфон и заметил, что на некоторых экранах изменилась вёрстка: «поехало» отображение текста, хотя при работе с эмулятором всё было нормально.
Меня зовут Даниль Галимзянов, я начинающий Flutter-разработчик в компании Surf. Разобрался, в чём причина проблемы с вёрсткой текста, и хочу поделиться с вами.
Проблема наглядно
Давайте посмотрим на код виджета MyWidget из этого gist в DartPad.
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { double _deviceTsf = 1.0; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, builder: (context, child) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: _deviceTsf), child: child ?? const SizedBox.shrink(), ); }, home: Scaffold( appBar: AppBar( title: Text( 'Text Scale Factor на устройстве = $_deviceTsf', textScaleFactor: 1.0, ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 40), const Text( 'Типа настройки размера шрифта в Accessibility', textScaleFactor: 1.0, ), Slider( value: _deviceTsf, min: 0.85, max: 1.3, divisions: 3, onChanged: (value) { setState(() { _deviceTsf = value; }); }, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: const [ Text('0.85', textScaleFactor: 1.0), Text('1.00', textScaleFactor: 1.0), Text('1.15', textScaleFactor: 1.0), Text('1.30', textScaleFactor: 1.0), ], ), ), Flexible(child: MyWidget(deviceTsf: _deviceTsf)), ], ), ), ), ); } } class MyWidget extends StatelessWidget { final double deviceTsf; const MyWidget({required this.deviceTsf, super.key}); @override Widget build(BuildContext context) { const defaultTextSize = 25; final defaultTextStyle = TextStyle( fontSize: defaultTextSize.toDouble(), color: Colors.black, ); const additionalTextSpans = <TextSpan>[ TextSpan( text: ' widget ', style: TextStyle(fontStyle: FontStyle.italic), ), TextSpan( text: '- size $defaultTextSize', style: TextStyle(fontWeight: FontWeight.bold), ) ]; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Text widget - size $defaultTextSize', textAlign: TextAlign.center, style: defaultTextStyle, // Заданный стиль текста высотой 25 ), const SizedBox(height: 20), Text.rich( textAlign: TextAlign.center, TextSpan( text: 'Text.rich', style: defaultTextStyle, // Заданный стиль текста высотой 25 children: additionalTextSpans, ), ), const SizedBox(height: 20), RichText( textAlign: TextAlign.center, text: TextSpan( text: 'RichText', style: defaultTextStyle, // Заданный стиль текста высотой 25 children: additionalTextSpans, ), ), const SizedBox(height: 20), Text( deviceTsf != 1 ? '?' : '?', style: const TextStyle(fontSize: 40), ), ], ); } }
Вроде бы размер текста задан везде одинаковый: он равен 25. Но когда меняется масштабирование текста, отображение получается разное. Хм…
В примере выше мы для наглядности реализовали эмуляцию масштабирования текста. Если же задавать значение в настройках телефона, а не подменять в MaterialApp, то картина будет аналогичная:

Почему так получилось
Дело было в настройках масштабирования текста на моём смартфоне, а если точнее — в свойстве textScaleFactor, которое есть у виджетов Text и RichText. Для упрощения будем называть его TSF.
TSF — один из Accessibility-параметров, который задается пользователем. Это количество отображаемых пикселей на экране для каждого логического пикселя. У пользователя эти настройки находятся в соответствующем разделе настроек смартфона.
Например, если TSF равен 1,5, текст на экране будет на 50% больше указанного размера шрифта fontSize.
Как приложение работает с TSF
Приложение получает значение TSF из системы. Доступ к нему можно получить при помощи класса MediaQuery, который хранит в себе MediaQueryData с нужным нам параметром.
MaterialApp, CupertinoApp и WidgetsApp по умолчанию добавляют MediaQuery в дерево. Статический метод of получает по контексту ближайший InheritedWidget типа MediaQuery и возвращает из него свойство data типа MediaQueryData:
final textScaleFactor = MediaQuery.of(context).textScaleFactor;
Также можно воспользоваться статическим методом textScaleFactorOf, который либо вернет текущий TSF на устройстве, либо 1.0, если MediaQuery не был определен выше по дереву.
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
Как текстовые виджеты работают с TSF
Для виджета Text всё просто
Есть три варианта, откуда виджет Text получает значения:
-
Значение из
MediaQuery— то есть заданное пользователем на смартфоне. -
Значение, заданное разработчиком.
-
Значение по умолчанию.
А вот у RichText поле textScaleFactor ведёт себя иначе
Если его не передали, оно по умолчанию становится равным 1.0 и не изменится в зависимости от настроек смартфона, поскольку не подписано на изменения MediaQuery.
Эту проблему решает использование именного конструктора Text.rich, который предоставляет возможности RichText.
Как подготовиться к тому, что пользователь может изменить масштаб текста
Значения TSF зависят от операционной системы и модели телефона. Они могут варьироваться в диапазоне примерно от 0.8 до 3.0: разброс серьёзный и надо быть к этому готовым.
Что можно сделать:
-
Договориться с заказчиком и дизайнерами: определить, какие значения масштабирования текста могут быть в приложении, зафиксировать их в ТЗ и учитывать при разработке. Диапазон значений зависит в первую очередь от назначения приложения и его аудитории.
-
Если диапазон определён, можно добавить на дебаг-экран возможность менять значение TSF в заданных пределах, чтоб отслеживать изменения в вёрстке во время отладки и тестирования.
Код реализации установки ограничений для TSF
const maxPossibleTsf = 1.1; return MaterialApp( builder: (context, child) { final data = MediaQuery.of(context); final newTextScaleFactor = min(maxPossibleTsf, data.textScaleFactor); // можно так, если нам надо задать минимальное и // максимальное значение: // data.textScaleFactor.clamp(minPossibleTsf, maxPossibleTsf); return MediaQuery( data: data.copyWith( textScaleFactor: newTextScaleFactor, ), child: child ?? const SizedBox.shrink(), ); }, home: const Text('We are finally ready for any TSF ?'), );
Больше полезностей о работе с Flutter, а также вакансии, новости, кейсы из практики Surf — в нашем телеграм-канале.
ссылка на оригинал статьи https://habr.com/ru/company/surfstudio/blog/720098/
Добавить комментарий