ParagraphStyle là một tập hợp các tham số định dạng xác định giao diện trực quan của một đoạn văn bản. Trong phát triển di động, ParagraphStyle bao gồm căn chỉnh (left, center, right, justified), khoảng cách dòng, thụt lề dòng đầu tiên, khoảng cách giữa các đoạn và hướng văn bản. Theo Material Design Typography Guidelines (2025), việc sử dụng nhất quán ParagraphStyle trên tất cả các màn hình của ứng dụng đảm bảo nhịp điệu kiểu chữ thống nhất và giảm nhiễu thị giác khi chuyển đổi giữa các phần.
Những điểm chính
ParagraphStyle (kiểu đoạn văn) là một tập hợp các quy tắc định dạng có thể áp dụng cho một hoặc nhiều đoạn văn bản. Không giống như kiểu ký tự (bold, italic, font-size) hoạt động ở cấp độ ký tự riêng lẻ, ParagraphStyle quản lý các thuộc tính vĩ mô: vị trí văn bản trong vùng chứa, khoảng cách giữa các dòng và đoạn, hướng viết và quy tắc ngắt từ.
Về mặt lịch sử, khái niệm ParagraphStyle đến từ các hệ thống xuất bản để bàn (QuarkXPress, PageMaker, InDesign), nơi mỗi đoạn văn có bộ tham số riêng. Trong phát triển di động, cách tiếp cận này đã chuyển thành các API phần mềm: NSMutableParagraphStyle trên iOS, các lớp ParagraphStyle và LineBreakStyle trong Android, và các tham số như textAlign và textHeightBehavior trong Flutter. Tất cả đều phục vụ một mục đích — quản lý định dạng đoạn văn thông qua mã, mà không bị ràng buộc với một công cụ văn bản cụ thể.
Theo Bringhurst — The Elements of Typographic Style (2024), cấu hình ParagraphStyle phù hợp là nền tảng của văn hóa kiểu chữ giao diện. Nếu kiểu chữ và cỡ chữ xác định «giọng nói» của văn bản, thì kiểu đoạn văn xác định «hơi thở» của nó: nhịp điệu, khoảng dừng và điểm nhấn. Trong giao diện di động nơi không gian bị hạn chế, ParagraphStyle đúng đắn cho phép chứa nhiều thông tin hơn mà không làm giảm khả năng đọc.
ParagraphStyle bao gồm một số nhóm thuộc tính, mỗi nhóm chịu trách nhiệm cho một khía cạnh cụ thể của định dạng. Hãy xem xét các tham số chính có sẵn trong API di động. Căn chỉnh là tham số chính xác định vị trí ngang của văn bản. Trên iOS có NSTextAlignment, trên Android — Layout.Alignment, trong Flutter — TextAlign.
| Thuộc tính | iOS (NSMutableParagraphStyle) | Android (Compose) | Flutter (TextStyle) |
|---|---|---|---|
| Căn chỉnh | alignment | textAlign | textAlign |
| Thụt lề trái | headIndent | textIndent (trong Compose) | — |
| Thụt lề phải | tailIndent | — | — |
| Thụt lề dòng đầu tiên | firstLineHeadIndent | textIndent (firstLine) | — |
| Khoảng cách giữa các đoạn | paragraphSpacing | paragraphStyle (lineHeight) | — |
| Khoảng cách trước đoạn | paragraphSpacingBefore | — | — |
| Hướng văn bản | baseWritingDirection | TextDirection (Compose) | textDirection |
| Chế độ ngắt dòng | lineBreakMode | overflow (TextStyle) | overflow (TextStyle) |
Điều quan trọng là phải hiểu rằng không phải tất cả các thuộc tính đều có sẵn trên tất cả các nền tảng ở cùng một mức độ. Ví dụ, thụt lề dòng đầu tiên (firstLineHeadIndent) chỉ được hỗ trợ đầy đủ trên iOS thông qua NSMutableParagraphStyle. Trong Android, SpannableString với LeadingMarginSpan được sử dụng cho mục đích này, và trong Flutter — RichText với TextSpan tùy chỉnh và padding. Trong phát triển đa nền tảng (Flutter, React Native), một số thuộc tính ParagraphStyle phải được mô phỏng thông qua các vùng chứa hoặc Spacer.
Trên iOS, công cụ chính để làm việc với kiểu đoạn văn là lớp NSMutableParagraphStyle — một lớp con của NSParagraphStyle. Nó cung cấp một bộ thuộc tính hoàn chỉnh để quản lý định dạng đoạn văn và được áp dụng thông qua NSAttributedString. NSMutableParagraphStyle được sử dụng không chỉ trong UILabel và UITextView, mà còn trong Core Text thông qua CTParagraphStyle.
// iOS: cấu hình ParagraphStyle đầy đủ
let paragraphStyle = NSMutableParagraphStyle()
// Căn chỉnh
paragraphStyle.alignment = .justified // căn đều
// Thụt lề
paragraphStyle.headIndent = 16 // thụt lề trái
paragraphStyle.tailIndent = -16 // thụt lề phải (âm)
paragraphStyle.firstLineHeadIndent = 32 // thụt lề dòng đầu tiên
// Khoảng cách đoạn
paragraphStyle.paragraphSpacing = 12 // sau đoạn
paragraphStyle.paragraphSpacingBefore = 8 // trước đoạn
// Chiều cao dòng
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
)
Trong SwiftUI, không có đối tượng tương đương trực tiếp nào với NSMutableParagraphStyle — thay vào đó, một tổ hợp các bổ nghĩa được sử dụng: .multilineTextAlignment() cho căn chỉnh, .lineSpacing() cho khoảng cách dòng. Để thụt lề dòng đầu tiên và khoảng cách đoạn trong SwiftUI, phải sử dụng Text + padding hoặc AttributedString (có sẵn từ iOS 15+), hỗ trợ NSParagraphStyle thông qua các thuộc tính.
// SwiftUI: AttributedString với 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)
Theo Apple — Text Programming Guide (2025), NSMutableParagraphStyle hỗ trợ tới 14 tham số định dạng khác nhau. Điều quan trọng cần nhớ là tailIndent được đặt dưới dạng giá trị âm so với chiều rộng của vùng chứa, trong khi headIndent là dương. Hành vi không rõ ràng này thường gây ra lỗi cho các nhà phát triển iOS mới bắt đầu.
Trên Android, cách tiếp cận ParagraphStyle khác nhau giữa hệ thống View và Jetpack Compose. Trong hệ thống View cổ điển, các tham số đoạn văn được đặt thông qua thuộc tính XML của TextView (android:textAlignment, android:lineSpacingExtra) hoặc thông qua SpannableString với các triển khai của giao diện ParagraphStyle từ gói android.text.style.
// Hệ thống Android View: ParagraphStyle qua SpannableString
val text = "First paragraph with settings.\n\nSecond paragraph."
val spannable = SpannableString(text)
// Căn đều
spannable.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL),
0, text.length,
Spannable.SPAN_PARAGRAPH
)
// Thụt lề dòng đầu tiên qua LeadingMarginSpan
spannable.setSpan(
LeadingMarginSpan.Standard(0, 20),
0, text.indexOf('\n'),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
Trong Jetpack Compose, ParagraphStyle được quản lý thông qua các tham số TextStyle. textAlign được sử dụng cho căn chỉnh, lineHeight (bằng sp hoặc em) cho khoảng cách dòng, và textIndent cho thụt lề dòng đầu tiên. Compose cũng hỗ trợ LineBreak để kiểm soát chi tiết ngắt dòng (Simple, Heading, Paragraph), điều này đặc biệt hữu ích cho văn bản đa ngôn ngữ.
// Jetpack Compose: ParagraphStyle qua TextStyle
Text(
text = "Đoạn đầu tiên với kiểu tùy chỉnh.
Đoạn thứ hai có thụt lề.",
style = TextStyle(
textAlign = TextAlign.Justify,
lineHeight = 24.sp,
textIndent = TextIndent(firstLine = 16.sp),
lineBreak = LineBreak.Paragraph
)
)
Theo Android Developers — Compose ParagraphStyle (2025), khi sử dụng TextIndent trong Compose, điều quan trọng là đặt firstLine bằng sp (không phải dp), vì thụt lề dòng đầu tiên phải co giãn theo văn bản. Nếu thụt lề được đặt bằng dp, khi kích thước phông chữ tăng lên, thụt lề dòng đầu tiên sẽ trở nên nhỏ hơn về mặt thị giác, phá vỡ tỷ lệ kiểu chữ.
Trong Flutter, mô hình ParagraphStyle không được tách riêng thành một lớp chuyên biệt — thay vào đó, một tổ hợp các tham số TextStyle và các lớp bổ sung được sử dụng. Căn chỉnh được đặt thông qua TextAlign, khoảng cách dòng thông qua height (không đơn vị), và TextHeightBehavior kiểm soát các thụt lề hiển thị của dòng đầu tiên và dòng cuối cùng.
// Flutter: ParagraphStyle qua tham số Text
Text(
'Đoạn được tạo kiểu trong Flutter.
Đoạn thứ hai có thụt lề.',
style: TextStyle(
fontSize: 16,
height: 1.5,
leadingDistribution: TextLeadingDistribution.proportional,
),
textAlign: TextAlign.justify,
textHeightBehavior: TextHeightBehavior(
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
),
)
Đối với các kịch bản phức tạp hơn, Flutter cung cấp việc sử dụng RichText với TextSpan và WidgetSpan tùy chỉnh. Điều này cho phép kết hợp văn bản với các widget nội dòng (biểu tượng, hình ảnh) trong một đoạn duy nhất, duy trì các quy tắc định dạng thống nhất. Tuy nhiên, cách tiếp cận này yêu cầu kiểm soát thủ công các ngắt dòng và không hỗ trợ ngắt từ tự động.
// Flutter: RichText với định dạng đoạn tùy chỉnh
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.'),
],
),
)
Theo Flutter Documentation — Text Widget (2025), TextHeightBehavior là một tham số chính để hiển thị ParagraphStyle chính xác trong Flutter. Nếu applyHeightToFirstAscent được đặt thành false, thụt lề trên cùng của dòng đầu tiên bị bỏ qua và văn bản «dính» vào đường viền trên cùng của vùng chứa. Điều này hữu ích cho các tiêu đề nhưng không mong muốn cho văn bản chính.
ParagraphStyle phải tính đến hướng viết cho từng ngôn ngữ. Trong các ứng dụng di động hỗ trợ tiếng Ả Rập, tiếng Do Thái hoặc tiếng Urdu (viết từ phải sang trái, RTL), thụt lề và căn chỉnh phải được phản chiếu. iOS hỗ trợ chuyển đổi tự động thông qua baseWritingDirection = .natural, Android thông qua TextDirection, Flutter thông qua textDirection.
Cấu hình căn chỉnh cho ngôn ngữ RTL đòi hỏi sự chú ý đặc biệt. Nếu ở chế độ LTR, headIndent đặt thụt lề trái, thì trong RTL, nó tự động trở thành thụt lề phải. Tuy nhiên, không phải tất cả các triển khai đều xử lý chính xác quá trình chuyển đổi này khi căn chỉnh được chỉ định rõ ràng (NSTextAlignment.left thay vì NSTextAlignment.natural). Theo Google Internationalization Guide (2025), đối với các ứng dụng đa ngôn ngữ, luôn sử dụng căn chỉnh tự nhiên và kiểm tra thụt lề ở cả hai bố cục.
// Android: hỗ trợ RTL trong ParagraphStyle
val textView = TextView(context)
// Bật hỗ trợ RTL tự động
textView.textDirection = View.TEXT_DIRECTION_LOCALE
textView.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Spannable với hỗ trợ RTL
val spannable = SpannableString(arabicText)
spannable.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL),
0, arabicText.length,
Spannable.SPAN_PARAGRAPH
)
Các đặc điểm khu vực cũng bao gồm các quy tắc khác nhau cho thụt lề dòng đầu tiên. Trong kiểu chữ châu Âu, thụt lề dòng đầu tiên (firstLineHeadIndent) là tiêu chuẩn để phân cách đoạn văn. Trong kiểu chữ Nhật Bản và Trung Quốc, thụt lề dòng đầu tiên cũng được sử dụng, nhưng kích thước của nó có thể khác nhau (thường là 1 em so với 1,5 em trong truyền thống châu Âu). Đối với kiểu chữ Ả Rập, thụt lề dòng đầu tiên hiếm khi được sử dụng — các đoạn văn thường được phân cách bằng khoảng cách dọc.
Lỗi phổ biến nhất là chỉ sử dụng ParagraphStyle thông qua các thuộc tính toàn cục của trường văn bản mà không xem xét định dạng khác nhau của các đoạn riêng lẻ. Trong UILabel hoặc TextView với nhiều đoạn được phân cách bằng , ParagraphStyle được áp dụng đồng nhất cho toàn bộ văn bản, trừ khi sử dụng văn bản có thuộc tính với các kiểu khác nhau cho mỗi đoạn.
Theo UX Collective — Common Typography Bugs in Mobile Apps (2025), các vấn đề với ParagraphStyle chiếm 18% tổng số lỗi kiểu chữ trong ứng dụng di động. Các trường hợp nghiêm trọng nhất là khi paragraphSpacing không chính xác dẫn đến văn bản chồng lên nhau trên các dòng khác nhau hoặc khi ngôn ngữ RTL hiển thị với thụt lề LTR. Nên tạo các bài kiểm tra đơn vị cho từng tham số ParagraphStyle với các chuỗi kiểm tra bằng các ngôn ngữ khác nhau.
Các câu hỏi thường gặp
NSMutableParagraphStyle quản lý định dạng đoạn văn (căn chỉnh, thụt lề, khoảng cách dòng), trong khi TextStyle quản lý các thuộc tính ký tự (phông chữ, màu sắc, kích thước). Cả hai đều được áp dụng thông qua NSAttributedString. ParagraphStyle hoạt động ở cấp độ đoạn văn, TextStyle ở cấp độ ký tự. Nhiều phiên bản TextStyle có thể tồn tại trong một đoạn văn, nhưng chỉ có một ParagraphStyle cho mỗi đoạn.
Trong Jetpack Compose, thụt lề dòng đầu tiên được đặt thông qua TextIndent bên trong TextStyle: TextStyle(textIndent = TextIndent(firstLine = 16.sp)). Giá trị phải ở dạng sp để co giãn theo văn bản. Đối với văn bản nhiều dòng, thụt lề được áp dụng cho mỗi dòng bắt đầu một đoạn mới (ký tự ). Đối với đoạn đầu tiên, thụt lề luôn được áp dụng.
Trong SwiftUI, không có đối tượng tương đương trực tiếp nào với paragraphSpacing. Để có khoảng cách giữa các đoạn, hãy sử dụng AttributedString (iOS 15+) với NSMutableParagraphStyle và thuộc tính paragraphSpacing. Hoặc phân tách các đoạn thông qua Text("...\n\n...") và thêm .padding() giữa các khối văn bản. Đối với các kịch bản kiểu chữ phức tạp, hãy sử dụng NSAttributedString thông qua UILabelRepresentable.
TextView trong Android không có thuộc tính paragraphSpacing — nó được đặt thông qua SpannableString với LeadingMarginSpan hoặc thông qua LineBackgroundSpan tùy chỉnh. Để có khoảng cách dòng, hãy sử dụng setLineSpacing; để có khoảng cách giữa các đoạn, hãy phân tách các đoạn bằng bổ sung hoặc sử dụng vùng chứa tổng hợp gồm nhiều phiên bản TextView.
ParagraphStyle phù hợp cải thiện khả năng tiếp cận: paragraphSpacing đủ giúp người dùng khiếm khuyết về nhận thức phân biệt ranh giới đoạn văn tốt hơn. WCAG 2.2 khuyến nghị phân tách trực quan các đoạn văn (thụt lề hoặc khoảng cách). ParagraphSpacing không được nhỏ hơn 1,5 × line-height. Đối với ngôn ngữ RTL, hãy xác minh rằng thụt lề được phản chiếu chính xác.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm