ParagraphStyle — to zestaw parametrów formatowania, który określa wizualny wygląd akapitu tekstu. W rozwoju mobilnym ParagraphStyle obejmuje wyrównanie (left, center, right, justified), odstępy między wierszami, wcięcia pierwszego wiersza, odstępy między akapitami i kierunek tekstu. Według Material Design Typography Guidelines (2025), spójne stosowanie ParagraphStyle na wszystkich ekranach aplikacji zapewnia jednolity rytm typograficzny i zmniejsza szum wizualny przy przełączaniu między sekcjami.
Najważniejsze
ParagraphStyle (styl akapitu) — to zbiór zasad formatowania stosowanych do jednego lub kilku akapitów tekstu. W przeciwieństwie do stylów znakowych (bold, italic, font-size), które działają na poziomie pojedynczych znaków, ParagraphStyle zarządza makrowłaściwościami: rozmieszczeniem tekstu w kontenerze, odległościami między wierszami i akapitami, kierunkiem pisma i zasadami przenoszenia.
Historycznie pojęcie ParagraphStyle pochodzi z desktopowych systemów DTP (QuarkXPress, PageMaker, InDesign), gdzie każdy akapit miał swój zestaw parametrów. W rozwoju mobilnym to podejście przekształciło się w programowe API: NSMutableParagraphStyle na iOS, klasy ParagraphStyle i LineBreakStyle w Android, a także parametry takie jak textAlign i textHeightBehavior w Flutter. Wszystkie rozwiązują to samo zadanie — zarządzanie formatowaniem akapitów za pomocą kodu, bez przywiązania do konkretnego silnika tekstowego.
Według Bringhurst — The Elements of Typographic Style (2024), poprawne skonfigurowanie ParagraphStyle to podstawa kultury typograficznej interfejsu. Jeśli krój pisma i stopień odpowiadają za „głos" tekstu, to styl akapitu — za jego „oddech": rytm, pauzy i akcenty. W interfejsach mobilnych, gdzie przestrzeń jest ograniczona, odpowiedni ParagraphStyle pozwala zmieścić więcej informacji bez utraty czytelności.
ParagraphStyle obejmuje kilka grup właściwości, z których każda odpowiada za określony aspekt formatowania. Omówmy kluczowe parametry dostępne w mobilnych API. Wyrównanie (alignment) — podstawowy parametr określający położenie tekstu w poziomie. W iOS dostępne jest NSTextAlignment, w Android — Layout.Alignment, w Flutter — TextAlign.
| Właściwość | iOS (NSMutableParagraphStyle) | Android (Compose) | Flutter (TextStyle) |
|---|---|---|---|
| Wyrównanie | alignment | textAlign | textAlign |
| Wcięcie lewe | headIndent | textIndent (w Compose) | — |
| Wcięcie prawe | tailIndent | — | — |
| Wcięcie akapitowe | firstLineHeadIndent | textIndent (firstLine) | — |
| Odstęp między akapitami | paragraphSpacing | paragraphStyle (lineHeight) | — |
| Odstęp przed akapitem | paragraphSpacingBefore | — | — |
| Kierunek tekstu | baseWritingDirection | TextDirection (Compose) | textDirection |
| Tryb łamania wierszy | lineBreakMode | overflow (TextStyle) | overflow (TextStyle) |
Ważne jest, aby zrozumieć, że nie wszystkie właściwości są dostępne na wszystkich platformach w jednakowym zakresie. Na przykład wcięcie pierwszego wiersza (firstLineHeadIndent) jest w pełni obsługiwane tylko w iOS przez NSMutableParagraphStyle. W Android do tego celu używa się SpannableString z LeadingMarginSpan, a w Flutter — RichText z niestandardowym TextSpan i paddingiem. W rozwoju międzyplatformowym (Flutter, React Native) niektóre właściwości ParagraphStyle trzeba emulować przez kontenery lub Spacer.
W iOS głównym narzędziem do pracy ze stylem akapitu jest klasa NSMutableParagraphStyle — dziedzicząca po NSParagraphStyle. Oferuje ona pełny zestaw właściwości do zarządzania formatowaniem akapitów i jest stosowana przez NSAttributedString. NSMutableParagraphStyle jest używany nie tylko w UILabel i UITextView, ale także w Core Text przez CTParagraphStyle.
// iOS: pełna konfiguracja ParagraphStyle
let paragraphStyle = NSMutableParagraphStyle()
// Wyrównanie
paragraphStyle.alignment = .justified // justified
// Wcięcia
paragraphStyle.headIndent = 16 // wcięcie lewe
paragraphStyle.tailIndent = -16 // wcięcie prawe (ujemne)
paragraphStyle.firstLineHeadIndent = 32 // wcięcie pierwszego wiersza
// Odstęp między akapitami
paragraphStyle.paragraphSpacing = 12 // po akapicie
paragraphStyle.paragraphSpacingBefore = 8 // przed akapitem
// Wysokość wiersza
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
)
W SwiftUI nie ma bezpośredniego odpowiednika NSMutableParagraphStyle — zamiast tego używa się kombinacji modyfikatorów: .multilineTextAlignment() do wyrównania, .lineSpacing() do odstępu między wierszami. Dla wcięć pierwszego wiersza i odstępów między akapitami w SwiftUI trzeba używać Text + padding lub AttributedString (dostępny od iOS 15+), który obsługuje NSParagraphStyle przez attributes.
// SwiftUI: AttributedString z 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)
Według Apple — Text Programming Guide (2025), NSMutableParagraphStyle obsługuje do 14 różnych parametrów formatowania. Przy czym ważne jest, aby pamiętać, że tailIndent jest ustawiany jako wartość ujemna względem szerokości kontenera, a headIndent — jako dodatnia. To nieoczywiste zachowanie często prowadzi do błędów u początkujących iOS-developerów.
W Android podejście do ParagraphStyle różni się między View-system a Jetpack Compose. W klasycznym View-system parametry akapitu są ustawiane przez atrybuty XML TextView (android:textAlignment, android:lineSpacingExtra) lub przez SpannableString z implementacjami interfejsu ParagraphStyle z pakietu android.text.style.
// Android View system: ParagraphStyle przez SpannableString
val text = "First paragraph with settings.\n\nSecond paragraph."
val spannable = SpannableString(text)
// Wyrównanie justowane
spannable.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL),
0, text.length,
Spannable.SPAN_PARAGRAPH
)
// Wcięcie pierwszego wiersza przez LeadingMarginSpan
spannable.setSpan(
LeadingMarginSpan.Standard(0, 20),
0, text.indexOf('\n'),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
W Jetpack Compose ParagraphStyle jest zarządzany przez parametry TextStyle. Do wyrównania używa się textAlign, do odstępu między wierszami — lineHeight (w sp lub em), do wcięcia pierwszego wiersza — textIndent. Compose obsługuje również LineBreak do precyzyjnego zarządzania przenoszeniem (Simple, Heading, Paragraph), co jest szczególnie przydatne dla tekstu wielojęzycznego.
// Jetpack Compose: ParagraphStyle przez TextStyle
Text(
text = "Pierwszy akapit z niestandardowym stylem.\n\nDrugi akapit z wcięciem.",
style = TextStyle(
textAlign = TextAlign.Justify,
lineHeight = 24.sp,
textIndent = TextIndent(firstLine = 16.sp),
lineBreak = LineBreak.Paragraph
)
)
Według Android Developers — Compose ParagraphStyle (2025), podczas używania TextIndent w Compose ważne jest ustawienie firstLine w sp (a nie w dp), ponieważ wcięcie pierwszego wiersza powinno skalować się wraz z tekstem. Jeśli wcięcie zostanie ustawione w dp, przy zwiększeniu stopnia pisma wcięcie akapitowe stanie się wizualnie mniejsze, co naruszy proporcje typograficzne.
W Flutter paradygmat ParagraphStyle nie jest wydzielony w osobną klasę — zamiast tego używa się kombinacji parametrów TextStyle i dodatkowych klas. Wyrównanie jest ustawiane przez TextAlign, odstępy między wierszami — przez height (unitless), a TextHeightBehavior zarządza widocznymi wcięciami pierwszego i ostatniego wiersza.
// Flutter: ParagraphStyle przez parametry Text
Text(
'Stylowany akapit w Flutter.\n\nDrugi akapit z wcięciami.',
style: TextStyle(
fontSize: 16,
height: 1.5,
leadingDistribution: TextLeadingDistribution.proportional,
),
textAlign: TextAlign.justify,
textHeightBehavior: TextHeightBehavior(
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
),
)
W bardziej złożonych scenariuszach Flutter oferuje użycie RichText z niestandardowym TextSpan i WidgetSpan. Pozwala to łączyć tekst z widżetami inline (ikony, obrazy) w jednym akapicie, zachowując jednolite reguły formatowania. Jednak takie podejście wymaga ręcznego zarządzania przenoszeniem i nie obsługuje automatycznego ustawiania przeniesień.
// Flutter: RichText z niestandardowym formatowaniem akapitu
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.'),
],
),
)
Według Flutter Documentation — Text Widget (2025), TextHeightBehavior — kluczowy parametr do poprawnego wyświetlania ParagraphStyle w Flutter. Jeśli applyHeightToFirstAscent jest ustawiony na false, górne wcięcie pierwszego wiersza jest ignorowane, a tekst „przykleja się" do górnej krawędzi kontenera. Jest to przydatne w nagłówkach, ale niepożądane w tekście głównym.
ParagraphStyle musi uwzględniać kierunek pisma dla każdej lokalizacji. W aplikacjach mobilnych obsługujących arabski, hebrajski lub urdu (pismo od prawej do lewej, RTL) wcięcia i wyrównanie powinny zmieniać się lustrzanie. iOS obsługuje automatyczne przełączanie przez baseWritingDirection = .natural, Android — przez TextDirection, Flutter — przez textDirection.
Konfiguracja wyrównania dla lokalizacji RTL wymaga szczególnej uwagi. Jeśli w trybie LTR headIndent ustawia lewe wcięcie, to w RTL automatycznie staje się prawym. Jednak nie wszystkie implementacje poprawnie obsługują to przejście przy jawnym ustawieniu wyrównania (NSTextAlignment.left zamiast NSTextAlignment.natural). Według Google Internationalization Guide (2025), dla aplikacji wielojęzycznych zawsze używaj natural alignment i sprawdzaj wcięcia w obu układach.
// Android: obsługa RTL w ParagraphStyle
val textView = TextView(context)
// Włącz automatyczną obsługę RTL
textView.textDirection = View.TEXT_DIRECTION_LOCALE
textView.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Spannable z obsługą RTL
val spannable = SpannableString(arabicText)
spannable.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL),
0, arabicText.length,
Spannable.SPAN_PARAGRAPH
)
Różnice regionalne obejmują również odmienne reguły dotyczące wcięcia pierwszego wiersza. W typografii europejskiej wcięcie akapitowe (firstLineHeadIndent) to standard wyróżniania akapitów. W typografii japońskiej i chińskiej wcięcie pierwszego wiersza również jest używane, ale jego rozmiar może się różnić (zwykle 1 em wobec 1.5 em w tradycji europejskiej). W typografii arabskiej wcięcie akapitowe jest rzadkością, akapity częściej oddziela się odstępem pionowym.
Najczęstszym błędem jest używanie ParagraphStyle tylko przez globalne atrybuty pola tekstowego bez uwzględnienia różnego formatowania poszczególnych akapitów. W UILabel lub TextView z wieloma akapitami rozdzielonymi \n, ParagraphStyle jest stosowany do całego tekstu jednakowo, jeśli nie użyje się attributed-text z różnymi stylami dla każdego akapitu.
Według UX Collective — Common Typography Bugs in Mobile Apps (2025), problemy z ParagraphStyle stanowią 18% wszystkich błędów typograficznych w aplikacjach mobilnych. Najbardziej krytyczne są przypadki, gdy nieprawidłowy paragraphSpacing prowadzi do nakładania się tekstu w różnych wierszach lub gdy lokalizacje RTL wyświetlają się z wcięciami LTR. Zaleca się tworzenie testów jednostkowych dla każdego parametru ParagraphStyle z przykładowymi ciągami w różnych językach.
Często zadawane pytania
NSMutableParagraphStyle zarządza formatowaniem akapitów (wyrównanie, wcięcia, odstępy między wierszami), a TextStyle — atrybutami znakowymi (czcionka, kolor, stopień). Oba są stosowane przez NSAttributedString. ParagraphStyle działa na poziomie akapitu, TextStyle — na poziomie znaków. Kilka TextStyle może być w jednym akapicie, ale ParagraphStyle — tylko jeden na akapit.
W Jetpack Compose wcięcie pierwszego wiersza ustawia się przez TextIndent wewnątrz TextStyle: TextStyle(textIndent = TextIndent(firstLine = 16.sp)). Wartość powinna być w sp, aby skalować się z tekstem. Dla tekstu wielowierszowego wcięcie jest stosowane do każdego wiersza rozpoczynającego nowy akapit (znak \n). Dla pierwszego akapitu wcięcie jest stosowane zawsze.
W SwiftUI nie ma bezpośredniego odpowiednika paragraphSpacing. Do odstępu między akapitami użyj AttributedString (iOS 15+) z NSMutableParagraphStyle i właściwością paragraphSpacing. Albo rozdzielaj akapity przez Text("...\n\n...") i dodawaj .padding() między blokami tekstu. W złożonych scenariuszach typograficznych używaj NSAttributedString przez UILabelRepresentable.
TextView w Android nie ma właściwości paragraphSpacing — jest ona ustawiana przez SpannableString z LeadingMarginSpan lub przez niestandardowy LineBackgroundSpan. Do odstępu między wierszami używaj setLineSpacing, do odstępu między akapitami — rozdzielaj akapity dodatkowym \n lub używaj złożonego kontenera z kilku TextView.
Prawidłowy ParagraphStyle poprawia dostępność: odpowiedni paragraphSpacing pomaga użytkownikom z zaburzeniami kognitywnymi lepiej rozróżniać granice akapitów. WCAG 2.2 zaleca wizualne oddzielanie akapitów (wcięcie lub odstęp). Przy czym paragraphSpacing nie powinien być mniejszy niż 1.5 × line-height. Dla lokalizacji RTL sprawdzaj, czy wcięcia są wyświetlane lustrzanie.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również