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 |
| Режим перенесення рядка | 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: повна конфігурація ParagraphStyle
let paragraphStyle = NSMutableParagraphStyle()
// Вирівнювання
paragraphStyle.alignment = .justified // вирівняний
// Відступ
paragraphStyle.headIndent = 16 // лівий відступ
paragraphStyle.tailIndent = -16 // правий відступ (негативний)
paragraphStyle.firstLineHeadIndent = 32 // відступ першого рядка
// Інтервал між абзацами
paragraphStyle.paragraphSpacing = 12 // після абзацу
paragraphStyle.paragraphSpacingBefore = 8 // перед абзацом
// Висота рядка
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 через атрибути.
// SwiftUI: AttributedString з 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: ParagraphStyle через SpannableString
val text = "First paragraph with settings.\n\nSecond paragraph."
val spannable = SpannableString(text)
// Вирівнювання по ширині
spannable.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL),
0, text.length,
Spannable.SPAN_PARAGRAPH
)
// Відступ першого рядка через 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 через TextStyle
Text(
text = "Перший абзац з кастомним стилем.
Другий абзац з відступом.",
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 через параметри Text
Text(
'Стилізований абзац у Flutter.
Другий абзац з відступами.',
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 з кастомним форматуванням абзацу
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 у ParagraphStyle
val textView = TextView(context)
// Увімкнути автоматичну підтримку RTL
textView.textDirection = View.TEXT_DIRECTION_LOCALE
textView.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Spannable з підтримкою RTL
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 з кількома абзацами, розділеними , 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, щоб масштабуватися з текстом. Для багаторядкового тексту відступ застосовується до кожного рядка, що починає новий абзац (символ ). Для першого абзацу відступ застосовується завжди.
У SwiftUI немає прямого аналога paragraphSpacing. Для інтервалу між абзацами використовуй AttributedString (iOS 15+) з NSMutableParagraphStyle і властивістю paragraphSpacing. Або розділяй абзаци через Text("...\n\n...") та додавай .padding() між текстовими блоками. Для складних типографічних сценаріїв використовуй NSAttributedString через UILabelRepresentable.
TextView в Android не має властивості paragraphSpacing — вона задається через SpannableString з LeadingMarginSpan або через кастомний LineBackgroundSpan. Для міжрядкового інтервалу використовуй setLineSpacing, для міжабзацного — розділяй абзаци додатковим або використовуй складовий контейнер з кількох TextView.
Правильний ParagraphStyle покращує доступність: достатній paragraphSpacing допомагає користувачам з когнітивними порушеннями краще розрізняти межі абзаців. WCAG 2.2 рекомендує візуальне розділення абзаців (відступ або інтервал). При цьому paragraphSpacing не повинен бути меншим за 1.5 × line-height. Для RTL-локалей перевіряй, що відступи дзеркально відображаються.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також