ParagraphStyle — это набор параметров форматирования, определяющий визуальное оформление абзаца текста. В мобильной разработке ParagraphStyle включает выравнивание (left, center, right, justified), межстрочные интервалы, отступы первой строки, интервалы между абзацами и направление текста. По данным Material Design Typography Guidelines (2025), согласованное использование ParagraphStyle на всех экранах приложения обеспечивает единый типографический ритм и снижает визуальный шум при переключении между разделами.
Главное
ParagraphStyle (стиль абзаца) — это совокупность правил форматирования, применимых к одному или нескольким абзацам текста. В отличие от символьных стилей (bold, italic, font-size), которые действуют на уровне отдельных символов, ParagraphStyle управляет макросвойствами: расположением текста в контейнере, расстояниями между строками и абзацами, направлением письма и правилами переноса.
Исторически понятие ParagraphStyle пришло из настольных издательских систем (QuarkXPress, PageMaker, InDesign), где каждый абзац имел свой набор параметров. В мобильной разработке этот подход трансформировался в программные API: NSMutableParagraphStyle на iOS, классы ParagraphStyle и LineBreakStyle в Android, а также параметры вроде textAlign и textHeightBehavior в Flutter. Все они решают одну задачу — управление абзацным форматированием через код, без привязки к конкретному текстовому движку.
По данным Bringhurst — The Elements of Typographic Style (2024), грамотная настройка ParagraphStyle — основа типографической культуры интерфейса. Если гарнитура и кегль отвечают за «голос» текста, то стиль абзаца — за его «дыхание»: ритм, паузы и акценты. В мобильных интерфейсах, где пространство ограничено, правильный ParagraphStyle позволяет вместить больше информации без потери читаемости.
ParagraphStyle включает несколько групп свойств, каждая из которых отвечает за определённый аспект форматирования. Рассмотрим ключевые параметры, доступные в мобильных API. Выравнивание (alignment) — основной параметр, определяющий положение текста по горизонтали. На iOS доступно NSTextAlignment, на Android — Layout.Alignment, в Flutter — TextAlign.
| Свойство | iOS (NSMutableParagraphStyle) | Android (Compose) | Flutter (TextStyle) |
|---|---|---|---|
| Выравнивание | alignment | textAlign | textAlign |
| Отступ слева | headIndent | textIndent (в Compose) | — |
| Отступ справа | tailIndent | — | — |
| Красная строка | firstLineHeadIndent | textIndent (firstLine) | — |
| Интервал между абзацами | paragraphSpacing | paragraphStyle (lineHeight) | — |
| Интервал до абзаца | paragraphSpacingBefore | — | — |
| Направление текста | baseWritingDirection | TextDirection (Compose) | textDirection |
| Line break mode | lineBreakMode | overflow (TextStyle) | overflow (TextStyle) |
Важно понимать, что не все свойства доступны на всех платформах в одинаковом объёме. Например, отступ первой строки (firstLineHeadIndent) полноценно поддерживается только в iOS через NSMutableParagraphStyle. В Android для этой цели используется SpannableString с LeadingMarginSpan, а в Flutter — RichText с кастомным TextSpan и padding. При кросс-платформенной разработке (Flutter, React Native) некоторые свойства ParagraphStyle приходится эмулировать через контейнеры или Spacer.
В iOS основным инструментом для работы со стилем абзаца является класс NSMutableParagraphStyle — наследник NSParagraphStyle. Он предоставляет полный набор свойств для управления абзацным форматированием и применяется через NSAttributedString. NSMutableParagraphStyle используется не только в UILabel и UITextView, но и в Core Text через CTParagraphStyle.
// iOS: full ParagraphStyle configuration
let paragraphStyle = NSMutableParagraphStyle()
// Alignment
paragraphStyle.alignment = .justified // justified
// Indentation
paragraphStyle.headIndent = 16 // left indent
paragraphStyle.tailIndent = -16 // right indent (negative)
paragraphStyle.firstLineHeadIndent = 32 // first line indent
// Paragraph spacing
paragraphStyle.paragraphSpacing = 12 // after paragraph
paragraphStyle.paragraphSpacingBefore = 8 // before paragraph
// Line height
paragraphStyle.minimumLineHeight = 24
paragraphStyle.maximumLineHeight = 24
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 16),
.paragraphStyle: paragraphStyle
]
let label = UILabel()
label.attributedText = NSAttributedString(
string: "This paragraph has a custom ParagraphStyle.\n\nSecond paragraph with 12 pt spacing.",
attributes: attributes
)
В SwiftUI прямого аналога NSMutableParagraphStyle нет — вместо него используется комбинация модификаторов: .multilineTextAlignment() для выравнивания, .lineSpacing() для межстрочного интервала. Для отступов первой строки и абзацных интервалов в SwiftUI приходится использовать Text + padding или AttributedString (доступен с iOS 15+), который поддерживает NSParagraphStyle через attributes.
// SwiftUI: AttributedString with ParagraphStyle (iOS 15+)
var attributed: AttributedString = """
This is the first paragraph with indent.
This is the second paragraph with 12 pt spacing.
"""
let ps = NSMutableParagraphStyle()
ps.paragraphSpacing = 12
ps.firstLineHeadIndent = 20
let container = AttributeContainer()
.paragraphStyle(ps)
attributed.mergeAttributes(container)
Text(attributed)
.font(.body)
По данным Apple — Text Programming Guide (2025), NSMutableParagraphStyle поддерживает до 14 различных параметров форматирования. При этом важно помнить, что tailIndent задаётся отрицательным значением относительно ширины контейнера, а headIndent — положительным. Это неочевидное поведение часто приводит к ошибкам у начинающих iOS-разработчиков.
В Android подход к ParagraphStyle различается между View-системой и Jetpack Compose. В классической View-системе параметры абзаца задаются через XML-атрибуты TextView (android:textAlignment, android:lineSpacingExtra) или через SpannableString с реализациями интерфейса ParagraphStyle из пакета android.text.style.
// Android View system: ParagraphStyle via SpannableString
val text = "First paragraph with settings.\n\nSecond paragraph."
val spannable = SpannableString(text)
// Justified alignment
spannable.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL),
0, text.length,
Spannable.SPAN_PARAGRAPH
)
// First line indent via LeadingMarginSpan
spannable.setSpan(
LeadingMarginSpan.Standard(0, 20),
0, text.indexOf('\n'),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
В Jetpack Compose ParagraphStyle управляется через параметры TextStyle. Для выравнивания используется textAlign, для межстрочного интервала — lineHeight (в sp или em), для отступа первой строки — textIndent. Compose также поддерживает LineBreak для тонкого управления переносами (Simple, Heading, Paragraph), что особенно полезно для многоязычного текста.
// Jetpack Compose: ParagraphStyle via TextStyle
Text(
text = "First paragraph with custom style.\n\nSecond paragraph with indent.",
style = TextStyle(
textAlign = TextAlign.Justify,
lineHeight = 24.sp,
textIndent = TextIndent(firstLine = 16.sp),
lineBreak = LineBreak.Paragraph
)
)
По данным Android Developers — Compose ParagraphStyle (2025), при использовании TextIndent в Compose важно задавать firstLine в sp (а не в dp), так как отступ первой строки должен масштабироваться вместе с текстом. Если задать отступ в dp, при увеличении кегля красная строка станет визуально меньше, что нарушит типографические пропорции.
В Flutter парадигма ParagraphStyle не выделена в отдельный класс — вместо этого используется комбинация параметров TextStyle и дополнительных классов. Выравнивание задаётся через TextAlign, межстрочные интервалы — через height (unitless), а TextHeightBehavior управляет видимыми отступами первой и последней строки.
// Flutter: ParagraphStyle via Text parameters
Text(
'Styled paragraph in Flutter.\n\nSecond paragraph with indents.',
style: TextStyle(
fontSize: 16,
height: 1.5,
leadingDistribution: TextLeadingDistribution.proportional,
),
textAlign: TextAlign.justify,
textHeightBehavior: TextHeightBehavior(
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
),
)
Для более сложных сценариев Flutter предлагает использовать RichText с кастомным TextSpan и WidgetSpan. Это позволяет комбинировать текст с инлайн-виджетами (иконки, изображения) в одном абзаце, сохраняя единые правила форматирования. Однако такой подход требует ручного управления переносами и не поддерживает автоматическую расстановку переносов.
// Flutter: RichText with custom paragraph formatting
RichText(
text: TextSpan(
style: TextStyle(fontSize: 16, height: 1.5),
children: [
TextSpan(text: 'First paragraph with icon '),
WidgetSpan(
child: Icon(Icons.star, size: 16),
alignment: PlaceholderAlignment.middle,
),
TextSpan(text: '\n\nSecond paragraph after spacing.'),
],
),
)
По данным Flutter Documentation — Text Widget (2025), TextHeightBehavior — ключевой параметр для корректного отображения ParagraphStyle в Flutter. Если applyHeightToFirstAscent установлен в false, верхний отступ первой строки игнорируется, и текст «прилипает» к верхней границе контейнера. Это полезно для заголовков, но нежелательно для основного текста.
ParagraphStyle должен учитывать направление письма для каждой локали. В мобильных приложениях с поддержкой арабского, иврита или урду (письмо справа налево, RTL) отступы и выравнивание должны зеркально меняться. iOS поддерживает автоматическое переключение через baseWritingDirection = .natural, Android — через TextDirection, Flutter — через textDirection.
Настройка выравнивания для RTL-локалей требует особого внимания. Если в LTR-режиме headIndent задаёт левый отступ, то в RTL он автоматически становится правым. Однако не все реализации корректно обрабатывают этот переход при явном задании выравнивания (NSTextAlignment.left вместо NSTextAlignment.natural). По данным Google Internationalization Guide (2025), для многоязычных приложений всегда используй natural alignment и проверяй отступы в обеих раскладках.
// Android: RTL support in ParagraphStyle
val textView = TextView(context)
// Enable automatic RTL support
textView.textDirection = View.TEXT_DIRECTION_LOCALE
textView.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Spannable with RTL support
val spannable = SpannableString(arabicText)
spannable.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL),
0, arabicText.length,
Spannable.SPAN_PARAGRAPH
)
Региональные особенности также включают разные правила для отступа первой строки. В европейской типографике красная строка (firstLineHeadIndent) — стандарт для абзацного выделения. В японской и китайской типографике отступ первой строки также используется, но его размер может отличаться (обычно 1 em против 1.5 em в европейской традиции). Для арабской типографики красная строка — редкость, абзацы чаще отделяются вертикальным интервалом.
Самая распространённая ошибка — использование ParagraphStyle только через глобальные атрибуты текстового поля без учёта разного форматирования отдельных абзацев. В UILabel или TextView с несколькими абзацами, разделёнными \n, ParagraphStyle применяется ко всему тексту одинаково, если не использовать attributed-текст с разными стилями для каждого абзаца.
По данным UX Collective — Common Typography Bugs in Mobile Apps (2025), проблемы с ParagraphStyle составляют 18% всех типографических багов в мобильных приложениях. Наиболее критичны случаи, когда неправильный paragraphSpacing приводит к перекрытию текста на разных строках или когда RTL-локали отображаются с LTR-отступами. Рекомендуется создавать unit-тесты для каждого параметра ParagraphStyle с тестовыми строками на разных языках.
Часто задаваемые вопросы
NSMutableParagraphStyle управляет абзацным форматированием (выравнивание, отступы, межстрочные интервалы), а TextStyle — символьными атрибутами (шрифт, цвет, кегль). Оба применяются через NSAttributedString. ParagraphStyle действует на уровне абзаца, TextStyle — на уровне символов. Несколько TextStyle могут быть в одном абзаце, но ParagraphStyle — только один на абзац.
В Jetpack Compose отступ первой строки задаётся через TextIndent внутри TextStyle: TextStyle(textIndent = TextIndent(firstLine = 16.sp)). Значение должно быть в sp, чтобы масштабироваться с текстом. Для многострочного текста отступ применяется к каждой строке, начинающей новый абзац (символ \n). Для первого абзаца отступ применяется всегда.
В SwiftUI нет прямого аналога paragraphSpacing. Для интервала между абзацами используй AttributedString (iOS 15+) с NSMutableParagraphStyle и свойством paragraphSpacing. Либо разделяй абзацы через Text("...\n\n...") и добавляй .padding() между текстовыми блоками. Для сложных типографических сценариев используй NSAttributedString через UILabelRepresentable.
TextView в Android не имеет свойства paragraphSpacing — оно задаётся через SpannableString с LeadingMarginSpan или через кастомный LineBackgroundSpan. Для межстрочного интервала используй setLineSpacing, для межбзацного — разделяй абзацы дополнительным \n или используй составной контейнер из нескольких TextView.
Правильный ParagraphStyle улучшает доступность: достаточный paragraphSpacing помогает пользователям с когнитивными нарушениями лучше различать границы абзацев. WCAG 2.2 рекомендует визуальное разделение абзацев (отступ или интервал). При этом paragraphSpacing не должен быть менее 1.5 × line-height. Для RTL-локалей проверяй, что отступы зеркально отображаются.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также