Span là đối tượng đánh dấu hữu ích mà bạn có thể dùng để tạo kiểu cho văn bản
ký tự hoặc đoạn văn bản. Bằng cách đính kèm span vào đối tượng văn bản, bạn có thể thay đổi
văn bản theo nhiều cách khác nhau, chẳng hạn như thêm màu sắc, làm cho văn bản có thể nhấp vào được,
điều chỉnh tỷ lệ cỡ chữ và vẽ văn bản theo cách tuỳ chỉnh. Span cũng có thể
thay đổi thuộc tính TextPaint
, vẽ trên
Canvas
và thay đổi bố cục văn bản.
Android cung cấp nhiều loại span cho phép tạo nhiều kiểu văn bản phổ biến. Bạn cũng có thể tạo các span của riêng mình để tạo kiểu theo ý thích.
Tạo và áp dụng span
Để tạo span, bạn có thể dùng một trong các lớp được liệt kê trong bảng sau. Các lớp này sẽ khác nhau tuỳ thuộc vào việc văn bản có thể thay đổi hay không, văn bản đánh dấu có thể thay đổi và cấu trúc dữ liệu cơ bản nào chứa dữ liệu span.
Lớp | Văn bản có thể thay đổi | Mã đánh dấu có thể thay đổi | Cấu trúc dữ liệu |
---|---|---|---|
SpannedString |
Không | Không | Mảng tuyến tính |
SpannableString |
Không | Có | Mảng tuyến tính |
SpannableStringBuilder |
Có | Có | Cây khoảng (interval tree) |
Cả 3 lớp đều mở rộng Spanned
. SpannableString
và SpannableStringBuilder
cũng mở rộng giao diện Spannable
.
Dưới đây là cách quyết định nên sử dụng loại nào:
- Nếu không cần sửa đổi văn bản hoặc mã đánh dấu sau khi tạo, hãy sử dụng
SpannedString
. - Nếu bạn cần đính kèm ít span vào một đối tượng văn bản và
văn bản ở chế độ chỉ có thể đọc, hãy sử dụng
SpannableString
. - Nếu cần sửa đổi văn bản sau khi tạo và cần đính kèm span vào
văn bản, sử dụng
SpannableStringBuilder
. - Nếu bạn cần đính kèm nhiều span vào một đối tượng văn bản, bất kể
về việc liệu văn bản đó có phải là văn bản chỉ đọc hay không, hãy sử dụng
SpannableStringBuilder
.
Để áp dụng span, gọi setSpan(Object _what_, int _start_, int _end_, int
_flags_)
trên đối tượng Spannable
. Tham số what ám chỉ độ dài hiện tại của bạn
áp dụng cho văn bản, còn các thông số start (bắt đầu) và end (kết thúc) cho biết phần
của văn bản mà bạn sẽ áp dụng độ dài.
Nếu bạn chèn văn bản vào bên trong đường viền của span, span đó sẽ tự động mở rộng thành
bao gồm văn bản được chèn. Khi chèn văn bản tại khoảng
ranh giới – tức là tại chỉ mục bắt đầu hoặc kết thúc – cờ
xác định liệu span có mở rộng để bao gồm văn bản được chèn hay không. Sử dụng
thời gian
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
gắn cờ để bao gồm văn bản được chèn và sử dụng
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
để loại trừ văn bản được chèn.
Ví dụ sau đây trình bày cách đính kèm
ForegroundColorSpan
thành một
chuỗi:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE )
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE );
Vì span được đặt bằng Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, nên span
mở rộng để bao gồm văn bản được chèn tại ranh giới span, như được thể hiện trong
ví dụ sau:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) spannable.insert(12, "(& fon)")
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ); spannable.insert(12, "(& fon)");
Bạn có thể đính kèm nhiều span cho cùng một văn bản. Ví dụ sau đây minh hoạ cách để tạo văn bản được in đậm và có màu đỏ:
Kotlin
val spannable = SpannableString("Text is spantastic!") spannable.setSpan(ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan( StyleSpan(Typeface.BOLD), 8, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE )
Java
SpannableString spannable = new SpannableString("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); spannable.setSpan( new StyleSpan(Typeface.BOLD), 8, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
Loại span trên Android
Android cung cấp hơn 20 loại span trong gói android.text.style. Android phân loại span theo hai cách chính:
- Cách span ảnh hưởng đến văn bản: span có thể ảnh hưởng đến giao diện hoặc văn bản chỉ số.
- Phạm vi span: có thể áp dụng một số span cho từng ký tự trong khi các span khác phải được áp dụng cho toàn bộ đoạn.
Các phần sau đây sẽ mô tả chi tiết hơn về các danh mục này.
Các span ảnh hưởng đến giao diện văn bản
Một số span áp dụng ở cấp ký tự sẽ ảnh hưởng đến hình thức văn bản, chẳng hạn như
thay đổi văn bản hoặc màu nền và thêm dấu gạch dưới hoặc gạch ngang chữ. Các
span mở rộng
Lớp CharacterStyle
.
Ví dụ về mã sau đây cho biết cách áp dụng UnderlineSpan
cho dấu gạch dưới
văn bản:
Kotlin
val string = SpannableString("Text with underline span") string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with underline span"); string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Các span chỉ ảnh hưởng đến giao diện văn bản sẽ kích hoạt thao tác vẽ lại văn bản mà không kích hoạt việc tính toán lại bố cục. Các span này sẽ triển khai UpdateAppearance
và mở rộng CharacterStyle
.
Các lớp con CharacterStyle
xác định cách vẽ văn bản bằng cách cấp quyền truy cập vào
hãy cập nhật TextPaint
.
Các span ảnh hưởng đến chỉ số văn bản
Các span khác áp dụng ở cấp ký tự sẽ ảnh hưởng đến chỉ số văn bản, chẳng hạn như dòng
chiều cao và kích thước văn bản. Các span này mở rộng
MetricAffectingSpan
.
Ví dụ về mã sau đây sẽ tạo một
RelativeSizeSpan
đó
tăng kích thước văn bản lên 50%:
Kotlin
val string = SpannableString("Text with relative size span") string.setSpan(RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with relative size span"); string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Việc áp dụng span ảnh hưởng đến các chỉ số văn bản khiến đối tượng quan sát đo lường lại văn bản để điều chỉnh bố cục và kết xuất hình ảnh (ví dụ: thay đổi) cỡ chữ có thể khiến từ xuất hiện trên các dòng khác nhau. Đang áp dụng quy tắc trước span kích hoạt việc đo lường lại, tính toán lại bố cục văn bản và vẽ lại văn bản.
Các span ảnh hưởng đến chỉ số văn bản sẽ mở rộng lớp MetricAffectingSpan
, một
lớp trừu tượng cho phép các lớp con xác định cách span ảnh hưởng đến việc đo lường văn bản
bằng cách cấp quyền truy cập vào TextPaint
. Vì MetricAffectingSpan
mở rộng
CharacterSpan
, các lớp con ảnh hưởng đến hình thức văn bản tại ký tự
cấp độ.
Các span ảnh hưởng đến đoạn văn bản
Span cũng có thể ảnh hưởng đến văn bản ở cấp đoạn, chẳng hạn như thay đổi
căn chỉnh hoặc lề của khối văn bản. Các span ảnh hưởng đến toàn bộ đoạn văn sẽ mở rộng giao diện ParagraphStyle
. Người nhận
sử dụng các span này, bạn đính kèm chúng vào toàn bộ đoạn văn, ngoại trừ phần cuối
ký tự dòng mới. Nếu bạn cố áp dụng span span cho văn bản nào đó không phải là
toàn bộ đoạn văn, Android hoàn toàn không áp dụng span.
Hình 8 thể hiện cách Android phân tách các đoạn văn bản.
Ví dụ về mã sau đây áp dụng một
QuoteSpan
đến một đoạn. Lưu ý rằng
nếu bạn đính kèm span vào bất kỳ vị trí nào khác ngoài phần đầu hoặc phần cuối của
, Android hoàn toàn không áp dụng kiểu này.
Kotlin
spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Tạo span tuỳ chỉnh
Nếu cần thêm chức năng khác với chức năng được cung cấp trong các span Android hiện tại, bạn có thể triển khai một span tuỳ chỉnh. Khi triển khai span của riêng bạn, hãy quyết định liệu span ảnh hưởng đến văn bản ở cấp ký tự hay cấp đoạn và cũng như liệu nó có ảnh hưởng đến bố cục hay hình thức của văn bản hay không. Việc này giúp bạn xác định những lớp cơ sở mà bạn có thể mở rộng và những giao diện bạn có thể cần để triển khai. Hãy tham khảo bảng sau:
Trường hợp | Lớp hoặc giao diện |
---|---|
Span ảnh hưởng đến văn bản ở cấp ký tự. | CharacterStyle |
Span ảnh hưởng đến giao diện văn bản. | UpdateAppearance |
Span ảnh hưởng đến các chỉ số văn bản. | UpdateLayout |
Span ảnh hưởng đến văn bản ở cấp đoạn văn bản. | ParagraphStyle |
Ví dụ: nếu bạn cần triển khai một span tuỳ chỉnh có chức năng sửa đổi kích thước văn bản và
màu, mở rộng RelativeSizeSpan
. Thông qua tính kế thừa, RelativeSizeSpan
mở rộng CharacterStyle
và triển khai 2 giao diện Update
. Từ ngày này
đã cung cấp các lệnh gọi lại cho updateDrawState
và updateMeasureState
,
bạn có thể ghi đè các lệnh gọi lại này để triển khai hành vi tuỳ chỉnh. Chiến lược phát hành đĩa đơn
đoạn mã sau đây sẽ tạo một span tuỳ chỉnh mở rộng RelativeSizeSpan
và
ghi đè lệnh gọi lại updateDrawState
để đặt màu của TextPaint
:
Kotlin
class RelativeSizeColorSpan( size: Float, @ColorInt private val color: Int ) : RelativeSizeSpan(size) { override fun updateDrawState(textPaint: TextPaint) { super.updateDrawState(textPaint) textPaint.color = color } }
Java
public class RelativeSizeColorSpan extends RelativeSizeSpan { private int color; public RelativeSizeColorSpan(float spanSize, int spanColor) { super(spanSize); color = spanColor; } @Override public void updateDrawState(TextPaint textPaint) { super.updateDrawState(textPaint); textPaint.setColor(color); } }
Ví dụ này minh hoạ cách tạo span tuỳ chỉnh. Bạn có thể đạt được cùng một
bằng cách áp dụng RelativeSizeSpan
và ForegroundColorSpan
cho văn bản.
Kiểm thử sử dụng span
Giao diện Spanned
cho phép bạn đặt span và truy xuất span từ
. Khi kiểm thử, hãy triển khai Android JUnit
kiểm thử để xác minh rằng bạn đã thêm đúng span
tại những vị trí chính xác. Mẫu Kiểu văn bản
ứng dụng
chứa ký tự span áp dụng mã đánh dấu cho các dấu đầu dòng bằng cách đính kèm
BulletPointSpan
vào văn bản. Ví dụ về mã sau đây cho thấy cách kiểm thử
liệu các dấu đầu dòng có xuất hiện như mong đợi hay không:
Kotlin
@Test fun textWithBulletPoints() { val result = builder.markdownToSpans("Points\n* one\n+ two") // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()) // Get all the spans attached to the SpannedString. val spans = result.getSpans<Any>(0, result.length, Any::class.java) // Check whether the correct number of spans are created. assertEquals(2, spans.size.toLong()) // Check whether the spans are instances of BulletPointSpan. val bulletSpan1 = spans[0] as BulletPointSpan val bulletSpan2 = spans[1] as BulletPointSpan // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1).toLong()) assertEquals(11, result.getSpanEnd(bulletSpan1).toLong()) assertEquals(11, result.getSpanStart(bulletSpan2).toLong()) assertEquals(14, result.getSpanEnd(bulletSpan2).toLong()) }
Java
@Test public void textWithBulletPoints() { SpannedString result = builder.markdownToSpans("Points\n* one\n+ two"); // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()); // Get all the spans attached to the SpannedString. Object[] spans = result.getSpans(0, result.length(), Object.class); // Check whether the correct number of spans are created. assertEquals(2, spans.length); // Check whether the spans are instances of BulletPointSpan. BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0]; BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1]; // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1)); assertEquals(11, result.getSpanEnd(bulletSpan1)); assertEquals(11, result.getSpanStart(bulletSpan2)); assertEquals(14, result.getSpanEnd(bulletSpan2)); }
Để biết thêm các ví dụ kiểm thử, hãy xem MarkdownBuilderTest trên GitHub.
Kiểm thử span tuỳ chỉnh
Khi kiểm thử span, hãy xác minh rằng TextPaint
chứa
sửa đổi và các phần tử chính xác xuất hiện trên Canvas
. Ví dụ: hãy cân nhắc việc triển khai một span tuỳ chỉnh để thêm dấu đầu dòng vào một số văn bản. Dấu đầu dòng có kích thước và màu sắc cụ thể và có một khoảng trống
giữa lề trái của vùng có thể vẽ và dấu đầu dòng.
Bạn có thể kiểm thử hành vi của lớp này bằng cách triển khai kiểm thử AndroidJUnit để kiểm tra những việc sau:
- Nếu bạn áp dụng span đúng cách, hãy dùng một dấu đầu dòng có kích thước được chỉ định và sẽ xuất hiện trên canvas và khoảng trống thích hợp nằm ở giữa bên trái lề và dấu đầu dòng.
- Nếu bạn không áp dụng span thì sẽ không có hành vi tuỳ chỉnh nào xuất hiện.
Bạn có thể xem cách triển khai các bài kiểm thử này trong lớp TextStyle mẫu trên GitHub.
Bạn có thể kiểm thử hoạt động tương tác với Canvas bằng cách mô phỏng canvas này, truyền mô phỏng
đối tượng cho thuộc tính
drawLeadingMargin()
và xác minh rằng các phương thức đúng được gọi bằng đúng phương thức
tham số.
Bạn có thể tìm thêm các mẫu kiểm thử span trong BulletPointSpanTest.
Các phương pháp hay nhất để sử dụng span
Có một số cách tiết kiệm bộ nhớ để đặt văn bản trong TextView
, tuỳ thuộc vào
theo nhu cầu của bạn.
Đính kèm hoặc tách span mà không thay đổi văn bản cơ bản
TextView.setText()
chứa nhiều phương thức nạp chồng xử lý nhiều span. Ví dụ: bạn có thể
đặt đối tượng văn bản Spannable
bằng mã sau:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
Khi gọi phương thức nạp chồng setText()
này, TextView
sẽ tạo một bản sao của Spannable
dưới dạng SpannedString
và lưu bản sao đó trong bộ nhớ dưới dạng CharSequence
.
Tức là văn bản và span là không thể thay đổi, vì vậy khi cần
cập nhật văn bản hoặc span, tạo một đối tượng Spannable
mới rồi gọi
setText()
. Thao tác này cũng kích hoạt việc đo lường và vẽ lại
của bạn.
Để cho biết rằng các span phải có thể thay đổi, bạn có thể sử dụng
setText(CharSequence text, TextView.BufferType
type)
!
như trong ví dụ sau:
Kotlin
textView.setText(spannable, BufferType.SPANNABLE) val spannableText = textView.text as Spannable spannableText.setSpan( ForegroundColorSpan(color), 8, spannableText.length, SPAN_INCLUSIVE_INCLUSIVE )
Java
textView.setText(spannable, BufferType.SPANNABLE); Spannable spannableText = (Spannable) textView.getText(); spannableText.setSpan( new ForegroundColorSpan(color), 8, spannableText.getLength(), SPAN_INCLUSIVE_INCLUSIVE);
Trong ví dụ này, phương thức
BufferType.SPANNABLE
khiến TextView
tạo SpannableString
và
Đối tượng CharSequence
do TextView
giữ lại hiện có mã đánh dấu có thể thay đổi và
văn bản bất biến. Để cập nhật span, hãy truy xuất văn bản dưới dạng Spannable
rồi sau đó
cập nhật các span nếu cần.
Khi bạn đính kèm, tách hoặc đặt lại vị trí span, TextView
sẽ tự động cập nhật để phản ánh thay đổi trong văn bản. Nếu bạn thay đổi thuộc tính nội bộ
của span hiện tại, hãy gọi invalidate()
để thực hiện các thay đổi liên quan đến giao diện hoặc
requestLayout()
để thực hiện các thay đổi liên quan đến chỉ số.
Thiết lập văn bản nhiều lần trong TextView
Trong một số trường hợp, chẳng hạn như khi sử dụng RecyclerView.ViewHolder
, bạn có thể muốn sử dụng lại TextView
và thiết lập văn bản nhiều lần. Theo
bất kể bạn có đặt BufferType
hay không, TextView
sẽ tạo
một bản sao của đối tượng CharSequence
và lưu giữ đối tượng này trong bộ nhớ. Điều này khiến cho tất cả
TextView
có chủ ý cập nhật. Bạn không thể cập nhật phiên bản gốc
Đối tượng CharSequence
để cập nhật văn bản. Điều này có nghĩa là mỗi khi bạn đặt
thì TextView
sẽ tạo một đối tượng mới.
Nếu muốn kiểm soát quá trình này nhiều hơn và tránh đối tượng thừa
của riêng mình, bạn có thể tự triển khai
Spannable.Factory
và ghi đè
newSpannable()
.
Thay vì tạo một đối tượng văn bản mới, bạn có thể truyền và trả về đối tượng hiện có
CharSequence
làm Spannable
, như được minh hoạ trong ví dụ sau:
Kotlin
val spannableFactory = object : Spannable.Factory() { override fun newSpannable(source: CharSequence?): Spannable { return source as Spannable } }
Java
Spannable.Factory spannableFactory = new Spannable.Factory(){ @Override public Spannable newSpannable(CharSequence source) { return (Spannable) source; } };
Bạn phải sử dụng textView.setText(spannableObject, BufferType.SPANNABLE)
khi
thiết lập văn bản. Nếu không, nguồn CharSequence
sẽ được tạo dưới dạng Spanned
và không thể truyền tới Spannable
, khiến newSpannable()
sẽ gửi một
ClassCastException
.
Sau khi ghi đè newSpannable()
, hãy yêu cầu TextView
sử dụng Factory
mới:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
Đặt đối tượng Spannable.Factory
một lần ngay sau khi bạn lấy thông tin tham chiếu đến
TextView
Nếu bạn đang sử dụng RecyclerView
, hãy đặt đối tượng Factory
khi bạn
tăng cường chế độ xem. Điều này giúp tránh việc tạo thêm đối tượng khi
RecyclerView
liên kết một mục mới với ViewHolder
.
Thay đổi các thuộc tính của span nội bộ
Nếu bạn chỉ cần thay đổi thuộc tính nội bộ của span có thể thay đổi, chẳng hạn như
màu của dấu đầu dòng trong một dấu đầu dòng tuỳ chỉnh, bạn có thể tránh hao tổn tài nguyên khi gọi
setText()
nhiều lần bằng cách giữ lại tham chiếu đến span khi nó được tạo.
Khi cần sửa đổi span, bạn có thể sửa đổi tệp tham chiếu, sau đó gọi
invalidate()
hoặc requestLayout()
trên TextView
, tuỳ thuộc vào loại
mà bạn đã thay đổi.
Trong ví dụ về mã sau, cách triển khai dấu đầu dòng tuỳ chỉnh có màu mặc định của màu đỏ sẽ chuyển thành màu xám khi bạn nhấn vào một nút:
Kotlin
class MainActivity : AppCompatActivity() { // Keeping the span as a field. val bulletSpan = BulletPointSpan(color = Color.RED) override fun onCreate(savedInstanceState: Bundle?) { ... val spannable = SpannableString("Text is spantastic") // Setting the span to the bulletSpan field. spannable.setSpan( bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) styledText.setText(spannable) button.setOnClickListener { // Change the color of the mutable span. bulletSpan.color = Color.GRAY // Color doesn't change until invalidate is called. styledText.invalidate() } } }
Java
public class MainActivity extends AppCompatActivity { private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED); @Override protected void onCreate(Bundle savedInstanceState) { ... SpannableString spannable = new SpannableString("Text is spantastic"); // Setting the span to the bulletSpan field. spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE); styledText.setText(spannable); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Change the color of the mutable span. bulletSpan.setColor(Color.GRAY); // Color doesn't change until invalidate is called. styledText.invalidate(); } }); } }
Sử dụng các hàm tiện ích Android KTX
Android KTX cũng chứa các hàm tiện ích giúp làm việc với span dễ dàng hơn. Để tìm hiểu thêm, hãy xem tài liệu về gói androidx.core.text.