Ich mochte die Idee nicht, dies jedes Mal per Code zu tun, wenn ich Teile des Textes einfärben möchte, was ich in all meinen Apps oft getan habe (und da in einigen Fällen der Text zur Laufzeit mit verschiedenen Inline- eingestellt wird). definierte Farben) also habe ich meine eigenen erstellt MarkableTextView
.
Die Idee war:
- Erkennen Sie XML-Tags anhand von Zeichenfolgen
- Identifizieren und Abgleichen des Tag-Namens
- Extrahieren und speichern Sie Attribute und Position von Text
- Tag entfernen und Inhalt behalten
- Durch Attribute iterieren und Stile anwenden
Hier ist der Prozess Schritt für Schritt:
Zuerst brauchte ich eine Möglichkeit, XML-Tags in einer bestimmten Zeichenfolge zu finden, und Regex
habe den Trick gemacht.
<([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)(?:\s+([^>]*))?>([^>][^<]*)</\1\s*>
Damit das oben Genannte mit einem XML-Tag übereinstimmt, müssen die folgenden Kriterien erfüllt sein:
- Gültiger Tag-Name wie
<a>
<a >
<a-a>
<a ..attrs..>
aber nicht< a>
<1>
- Schließen eines Tags mit einem passenden Namen wie,
<a></a>
aber nicht<a></b>
- Beliebiger Inhalt, da keine Notwendigkeit besteht, "nichts" zu stylen.
Nun zu den Attributen, die wir verwenden werden.
([a-zA-Z]+)\s*=\s*(['"])\s*([^'"]+?)\s*\2
Es hat das gleiche Konzept und im Allgemeinen musste ich für beide nicht weit gehen, da der Compiler sich um den Rest kümmert, wenn etwas aus dem Format gerät.
Jetzt brauchen wir eine Klasse, die die extrahierten Daten enthalten kann:
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;
}
}
Vor allem werden wir diesen coolen Iterator hinzufügen, den ich lange benutzt habe, um Übereinstimmungen zu durchlaufen ( ich kann mich nicht an den Autor erinnern) :
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;
}
Zum Schluss noch das Styling. Hier ist ein sehr einfacher Styler, den ich für diese Antwort gemacht habe:
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);
}
}
}
Und so sieht die Markable
Klasse mit den Definitionen aus:
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";
}
}
Jetzt müssen wir nur noch auf eine Zeichenfolge verweisen, und im Grunde sollte sie so aussehen:
<string name="markable_string">
<![CDATA[Hello <markable textStyle=\"underline\" textColor=\"interaction\">world</markable>!]]>
</string>
Achten Sie darauf , die Tags mit einer einzuwickeln CDATA Section
und die Flucht "
mit\
.
Ich habe dies als modulare Lösung entwickelt, um Teile des Textes auf alle möglichen Arten zu verarbeiten, ohne unnötigen Code dahinter stecken zu müssen.