Мне не нравилась идея делать это с помощью кода каждый раз, когда я хочу раскрасить части текста, которые я много делал во всех своих приложениях (и поскольку в некоторых случаях текст устанавливается во время выполнения с разными встроенными определенные цвета), поэтому я создал свой собственный MarkableTextView
.
Идея заключалась в следующем:
- Обнаруживать теги XML из строки
- Определить и сопоставить имя тега
- Извлечь и сохранить атрибуты и положение текста
- Удалить тег и сохранить содержимое
- Перебирать атрибуты и применять стили
Вот пошаговый процесс:
Сначала мне нужен был способ найти теги XML в заданной строке, и я Regex
сделал свое дело ..
<([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)(?:\s+([^>]*))?>([^>][^<]*)</\1\s*>
Чтобы указанное выше совпадало с тегом XML, он должен соответствовать следующим критериям:
- Допустимое имя тега, например,
<a>
<a >
<a-a>
<a ..attrs..>
но не< a>
<1>
- Закрывающий тег с таким же именем, как,
<a></a>
но не<a></b>
- Любой контент, так как не нужно стилизовать «ничего»
Теперь что касается атрибутов, которые мы собираемся использовать ...
([a-zA-Z]+)\s*=\s*(['"])\s*([^'"]+?)\s*\2
Он имеет ту же концепцию, и, как правило, мне не нужно было далеко ходить за обоими, поскольку компилятор позаботится обо всем остальном, если что-то выйдет из формата.
Теперь нам нужен класс, который может хранить извлеченные данные:
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;
}
}
Прежде всего, мы собираемся добавить этот крутой итератор, который я давно использую для перебора совпадений (не могу вспомнить автора) :
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;
}
Наконец, стилизация, вот очень простой стилист, который я сделал для этого ответа:
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);
}
}
}
А вот как Markable
выглядит класс, содержащий определения:
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";
}
}
Все, что нам сейчас нужно, это ссылка на строку, и в основном она должна выглядеть так:
<string name="markable_string">
<![CDATA[Hello <markable textStyle=\"underline\" textColor=\"interaction\">world</markable>!]]>
</string>
Убедитесь в том , чтобы обернуть тег с CDATA Section
и бежать "
с\
.
Я сделал это как модульное решение, позволяющее обрабатывать части текста различными способами без необходимости вставлять ненужный код.