Tôi không thích ý tưởng làm điều này bằng mã mỗi khi tôi muốn tô màu các phần của văn bản mà tôi đã làm rất nhiều trong tất cả các ứng dụng của mình (và vì trong một số trường hợp, văn bản đang được đặt trong thời gian chạy với nội tuyến khác- màu xác định) vì vậy tôi đã tạo ra màu của riêng mình MarkableTextView
.
Ý tưởng là:
- Phát hiện các thẻ XML từ chuỗi
- Xác định và đối sánh tên thẻ
- Trích xuất và lưu các thuộc tính và vị trí của văn bản
- Xóa thẻ và giữ lại nội dung
- Lặp lại các thuộc tính và áp dụng các kiểu
Đây là quy trình từng bước:
Đầu tiên, tôi cần một cách để tìm các thẻ XML trong một chuỗi nhất định và Regex
đã thực hiện thủ thuật ..
<([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)(?:\s+([^>]*))?>([^>][^<]*)</\1\s*>
Để đối sánh ở trên với thẻ XML, nó phải có các tiêu chí sau:
- Tên thẻ hợp lệ như
<a>
<a >
<a-a>
<a ..attrs..>
nhưng không< a>
<1>
- Thẻ đóng có tên phù hợp như
<a></a>
nhưng không<a></b>
- Bất kỳ nội dung nào, vì không cần phải tạo kiểu "không có gì"
Bây giờ đối với các thuộc tính, chúng tôi sẽ sử dụng cái này ..
([a-zA-Z]+)\s*=\s*(['"])\s*([^'"]+?)\s*\2
Nó có cùng một khái niệm và nói chung tôi không cần phải đi xa cho cả hai vì trình biên dịch sẽ xử lý phần còn lại nếu bất cứ điều gì xảy ra ngoài định dạng.
Bây giờ chúng ta cần một lớp có thể chứa dữ liệu được trích xuất:
public class MarkableSheet {
private String attributes;
private String content;
private int outset;
private int ending;
private int offset;
private int contentLength;
public MarkableSheet(String attributes, String content, int outset, int ending, int offset, int contentLength) {
this.attributes = attributes;
this.content = content;
this.outset = outset;
this.ending = ending;
this.offset = offset;
this.contentLength = contentLength;
}
public String getAttributes() {
return attributes;
}
public String getContent() {
return content;
}
public int getOutset() {
return outset;
}
public int getContentLength() {
return contentLength;
}
public int getEnding() {
return ending;
}
public int getOffset() {
return offset;
}
}
Trước bất kỳ điều gì khác, chúng tôi sẽ thêm trình lặp thú vị này mà tôi đã sử dụng từ lâu để lặp qua các trận đấu (không thể nhớ tác giả) :
public static Iterable<MatchResult> matches(final Pattern p, final CharSequence input) {
return new Iterable<MatchResult>() {
public Iterator<MatchResult> iterator() {
return new Iterator<MatchResult>() {
// Use a matcher internally.
final Matcher matcher = p.matcher(input);
// Keep a match around that supports any interleaving of hasNext/next calls.
MatchResult pending;
public boolean hasNext() {
// Lazily fill pending, and avoid calling find() multiple times if the
// clients call hasNext() repeatedly before sampling via next().
if (pending == null && matcher.find()) {
pending = matcher.toMatchResult();
}
return pending != null;
}
public MatchResult next() {
// Fill pending if necessary (as when clients call next() without
// checking hasNext()), throw if not possible.
if (!hasNext()) { throw new NoSuchElementException(); }
// Consume pending so next call to hasNext() does a find().
MatchResult next = pending;
pending = null;
return next;
}
/** Required to satisfy the interface, but unsupported. */
public void remove() { throw new UnsupportedOperationException(); }
};
}
};
}
MarkableTextView:
public class MarkableTextView extends AppCompatTextView {
public MarkableTextView(Context context) {
super(context);
}
public MarkableTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MarkableTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void setText(CharSequence text, BufferType type) {
// Intercept and process text
text = prepareText(text.toString());
super.setText(text, type);
}
public Spannable Markable;
private Spannable prepareText(String text) {
String parcel = text;
Multimap<String, MarkableSheet> markableSheets = ArrayListMultimap.create();
// Used to correct content position after tossing tags
int totalOffset = 0;
// Iterate through text
for (MatchResult match : matches(Markable.Patterns.XML, parcel)) {
// Get tag name
String tag = match.group(1);
// Match with a defined tag name "case-sensitive"
if (!tag.equals(Markable.Tags.MARKABLE)) {
// Break if no match
break;
}
// Extract data
String attributes = match.group(2);
String content = match.group(3);
int outset = match.start(0);
int ending = match.end(0);
int offset = totalOffset; // offset=0 since no preceded changes happened
int contentLength = match.group(3).length();
// Calculate offset for the next element
totalOffset = (ending - outset) - contentLength;
// Add to markable sheets
MarkableSheet sheet =
new MarkableSheet(attributes, content, outset, ending, offset, contentLength);
markableSheets.put(tag, sheet);
// Toss the tag and keep content
Matcher reMatcher = Markable.Patterns.XML.matcher(parcel);
parcel = reMatcher.replaceFirst(content);
}
// Initialize spannable with the modified text
Markable = new SpannableString(parcel);
// Iterate through markable sheets
for (MarkableSheet sheet : markableSheets.values()) {
// Iterate through attributes
for (MatchResult match : matches(Markable.Patterns.ATTRIBUTES, sheet.getAttributes())) {
String attribute = match.group(1);
String value = match.group(3);
// Apply styles
stylate(attribute,
value,
sheet.getOutset(),
sheet.getOffset(),
sheet.getContentLength());
}
}
return Markable;
}
Cuối cùng là tạo kiểu, vì vậy đây là một trình tạo kiểu rất đơn giản mà tôi đã tạo cho câu trả lời này:
public void stylate(String attribute, String value, int outset, int offset, int length) {
// Correct position
outset -= offset;
length += outset;
if (attribute.equals(Markable.Tags.TEXT_STYLE)) {
if (value.contains(Markable.Tags.BOLD) && value.contains(Markable.Tags.ITALIC)) {
Markable.setSpan(
new StyleSpan(Typeface.BOLD_ITALIC),
outset,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
else if (value.contains(Markable.Tags.BOLD)) {
Markable.setSpan(
new StyleSpan(Typeface.BOLD),
outset,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
else if (value.contains(Markable.Tags.ITALIC)) {
Markable.setSpan(
new StyleSpan(Typeface.ITALIC),
outset,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (value.contains(Markable.Tags.UNDERLINE)) {
Markable.setSpan(
new UnderlineSpan(),
outset,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
if (attribute.equals(Markable.Tags.TEXT_COLOR)) {
if (value.equals(Markable.Tags.ATTENTION)) {
Markable.setSpan(
new ForegroundColorSpan(ContextCompat.getColor(
getContext(),
R.color.colorAttention)),
outset,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
else if (value.equals(Markable.Tags.INTERACTION)) {
Markable.setSpan(
new ForegroundColorSpan(ContextCompat.getColor(
getContext(),
R.color.colorInteraction)),
outset,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
Và đây là cách Markable
lớp chứa các định nghĩa trông như thế nào:
public class Markable {
public static class Patterns {
public static final Pattern XML =
Pattern.compile("<([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)(?:\\s+([^>]*))?>([^>][^<]*)</\\1\\s*>");
public static final Pattern ATTRIBUTES =
Pattern.compile("(\\S+)\\s*=\\s*(['\"])\\s*(.+?)\\s*\\2");
}
public static class Tags {
public static final String MARKABLE = "markable";
public static final String TEXT_STYLE = "textStyle";
public static final String BOLD = "bold";
public static final String ITALIC = "italic";
public static final String UNDERLINE = "underline";
public static final String TEXT_COLOR = "textColor";
public static final String ATTENTION = "attention";
public static final String INTERACTION = "interaction";
}
}
Tất cả những gì chúng ta cần bây giờ là tham chiếu đến một chuỗi và về cơ bản nó sẽ giống như sau:
<string name="markable_string">
<![CDATA[Hello <markable textStyle=\"underline\" textColor=\"interaction\">world</markable>!]]>
</string>
Đảm bảo bọc các thẻ bằng a CDATA Section
và thoát "
bằng \
.
Tôi đã thực hiện điều này như một giải pháp mô-đun để xử lý các phần của văn bản theo tất cả các cách khác nhau mà không cần phải nhồi các mã không cần thiết phía sau.