《Styling Android》的忠實(shí)讀者應(yīng)該知道我是一個(gè)Spans的骨灰級(jí)粉絲,知道我認(rèn)為掌握好Span對(duì)于發(fā)揮TextView的最大功效來說至關(guān)重要。人們說Span有時(shí)只做一些簡(jiǎn)單的工作,比如僅僅改變文本的顏色,這樣看起來有一點(diǎn)尷尬。這本文中,我們將展望如何利用我們自己的Span,并且看一下使用自定義Span可進(jìn)行如何的簡(jiǎn)化。
在我們開始前,我要先簡(jiǎn)短地介紹一下。編寫本文的靈感來自于一些代碼,這些代碼使用一個(gè)我最不喜歡的類別即android.text.Html中執(zhí)行的文本樣式。在這個(gè)類別在一個(gè)文本字符串中解析html標(biāo)記并生成相關(guān)的Span后,其本身就不是問題了。然而,我碰到的問題時(shí)是這個(gè)類別支持難以維護(hù)代碼。使用Html的代碼常常需要進(jìn)行重寫才可以直接利用使用Span的功能以滿足做出更改的需要。
一個(gè)示例可能會(huì)幫助闡明這個(gè)問題。想一想TextView文本的一部分需要在剩下的字符串中采用不同顏色的情況。這個(gè)任務(wù)可以使用Html類別來完成:
Spanned text = Html.fromHtml("One word should be <font color='16711680'>red</font>");
textView.setText(text);
我首先遇到的麻煩是它看起來很不臃腫。Java代碼中字符串的嵌入式HTML標(biāo)記是一個(gè)看起來簡(jiǎn)單的代碼,這是因?yàn)橛泻芏嗫尚械墓ぞ弑绕涓行?。雖然字符串可以移入一個(gè)字符串資源中,但如果在運(yùn)行時(shí)需要對(duì)實(shí)際顏色值進(jìn)行確定,這種情況則通常不會(huì)發(fā)生。 我們最后必須執(zhí)行甚至更為臃腫的字符串連接,或我們需要使用格式化的字符串資源來生成適當(dāng)?shù)腍TML字符串,之后我們使用Html類別對(duì)其進(jìn)行解析。還有一個(gè)更大的麻煩就是Html的fromHtml()方法可使我們?cè)跇?biāo)記內(nèi)指定顏色資源,包括顏色狀態(tài)列表。我們只能在‘a(chǎn)ndroid’包中找到它,所以我們無法介入系統(tǒng)資源,并不能使用我們自己的資源。 如果我們?cè)谏蠈覶extView更改的情況下需要改變文本顏色,這就會(huì)給我們帶來問題。
我已經(jīng)對(duì)一個(gè)Html為什么不好的用例進(jìn)行了闡述,那么應(yīng)
如何解決這個(gè)問題呢? 使用Span來更改文本的通常的方法是使用TextAppearanceSpan,但是這常常需要我們指定其他的因素,比如文本外觀或字體等。 在上述列子中,我們僅關(guān)注了改變一部分字符串字體顏色的問題,所以大可不必使用TextAppearanceSpan。其他的選擇還有 ForegroundColorSpan,但這個(gè)方法同樣存在著不支持顏色狀態(tài)列表的問題。
但是我們可以輕松地創(chuàng)建我們自己的自定義Span,它可以準(zhǔn)確地完成我們需要做的工作(而且我們可以在此之后在整個(gè)應(yīng)用程序中再次使用這個(gè)自定義Span)。
讓我們首先建立一個(gè)簡(jiǎn)單的自定義Span,其將僅僅改變文本的顏色。我們?cè)谶@里可以改用ForegroundColorSpan(而且我肯定會(huì)使用生產(chǎn)代碼),但是我選擇創(chuàng)建一個(gè)自定義的代碼,僅此用來幫助解釋如何編寫我們自己的Span:
class StaticColourSpan extends CharacterStyle {
private final int colour;
public StaticColourSpan(int colour) {
super();
this.colour = colour;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setColor(colour);
}
}
很簡(jiǎn)單吧? 構(gòu)造函數(shù)使用了一個(gè)顏色值,因?yàn)槲覀償U(kuò)展了CharacterStyle,所以我們需要使用updateDrawState()。這種方法將在文本的onDraw()前調(diào)用,并允許我們調(diào)整Paint繪畫對(duì)象,以此給文本上色。所以,我們所有需要做的是設(shè)置繪畫對(duì)象的顏色,之后就全部搞定了。
現(xiàn)在一些人可能已經(jīng)意識(shí)到這種方法并不能解決TextView狀態(tài)變化下文本更改顏色用例的問題,但我們可以創(chuàng)建另一種可以精確完成這個(gè)工作的Span。
class ColourStateListSpan extends CharacterStyle {
private final ColorStateList colorStateList;
public ColourStateListSpan(ColorStateList colorStateList) {
super();
this.colorStateList = colorStateList;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setColor(colorStateList.getColorForState(tp.drawableState, 0));
}
}
這是非常相似的,差異是構(gòu)造函數(shù)采用了ColorStateList而不是原始的顏色值,而且在updateDrawSate(),我們?cè)贑olorStateList(顏色狀態(tài)列表)中查找了合適的顏色,這些顏色依賴我們從TextPaint對(duì)象獲得的狀態(tài)。updateDrawState() 將在TextView 刷新后調(diào)出,所以我們?cè)诖藭r(shí)可獲知該控制狀態(tài)。
下一個(gè)考慮的事情就是我們真的不希望加載ColorStateList對(duì)象將其調(diào)出。 但是我們可以輕松地創(chuàng)建一個(gè)工廠方法,這個(gè)方法將采取資源標(biāo)識(shí)符并依靠資源類型加載適合的Span。
public abstract class TextColourSpan extends CharacterStyle {
public static TextColourSpan newInstance(Context context, int resourceId) {
Resources resources = context.getResources();
ColorStateList colorStateList = resources.getColorStateList(resourceId);
if (colorStateList != null) {
return new ColourStateListSpan(colorStateList);
}
int colour = resources.getColor(resourceId);
if (colour >= 0) {
return new StaticColourSpan(colour);
}
return null;
}
}
如果我們改變StaticColourSpan和 ColourStateListSpan類別,從而擴(kuò)展基礎(chǔ)類別,而不是直接擴(kuò)展CharacterStyle,那么我們有了一個(gè)多態(tài)的newInstance()方法,這個(gè)方法將依靠加載資源的類型而返回合適的對(duì)象。
最后一件值得考慮的事就是如何確定Span應(yīng)用在字符串的范圍。字符串模式匹配的一個(gè)顯而易見的選擇是使用正則表達(dá)式,研究一個(gè)實(shí)用類別是如何為我們完成這個(gè)工作的。
public final class SpanUtils {
private SpanUtils() {
}
public static CharSequence createSpannable(Context context, int stringId, Pattern pattern, CharacterStyle... styles) {
String string = context.getString(stringId);
return createSpannable(string, pattern, styles);
}
public static CharSequence createSpannable(CharSequence source, Pattern pattern, CharacterStyle... styles) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(source);
Matcher matcher = pattern.matcher(source);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
applyStylesToSpannable(spannableStringBuilder, start, end, styles);
}
return spannableStringBuilder;
}
private static SpannableStringBuilder applyStylesToSpannable(SpannableStringBuilder source, int start, int end, CharacterStyle... styles) {
for (CharacterStyle style : styles) {
source.setSpan(CharacterStyle.wrap(style), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
return source;
}
}
我們現(xiàn)在可以使用字符串將其調(diào)出,在出現(xiàn)匹配時(shí)應(yīng)用我們希望匹配的正則表達(dá)式和Span對(duì)象列表。 這種方法可輕松地隨時(shí)進(jìn)行應(yīng)用,而且比Html實(shí)施機(jī)制更為簡(jiǎn)潔。然而,可以肯定的是其在理解和維護(hù)上更為輕松。而且這個(gè)方法用起來很有效
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CharacterStyle redText = TextColourSpan.newInstance(this, R.color.bright_red);
CharacterStyle changeText = TextColourSpan.newInstance(this, R.color.pressable_string);
Pattern redPattern = Pattern.compile(getString(R.string.simple_string_pattern));
Pattern changePattern = Pattern.compile(getString(R.string.pressable_string_pattern));
final TextView text2 = (TextView) findViewById(R.id.text2);
final TextView text3 = (TextView) findViewById(R.id.text3);
formatUsingSpans(text2, R.string.simple_string, redPattern, redText);
formatUsingSpans(text3, R.string.pressable_string, redPattern, redText);
formatUsingSpans(text3, changePattern, changeText);
}
private void formatUsingSpans(TextView textView, int stringId, Pattern pattern, CharacterStyle... styles) {
CharSequence text = SpanUtils.createSpannable(this, stringId, pattern, styles);
textView.setText(text);
}
}
附帶的源文件還有一些進(jìn)一步的例子,指出這種技術(shù)是如何應(yīng)用到這幾個(gè)簡(jiǎn)單類別的。
所以,總而言之:如果我承繼了你使用Html走了捷徑編寫的代碼,那么我將把你找出來并報(bào)仇雪恨。 然而,更有可能發(fā)生的事你將必須維護(hù)這個(gè)代碼,而且最后自怨自艾。 不要使用Html類別來創(chuàng)建技術(shù)債務(wù),應(yīng)該多用點(diǎn)心使用Spans來正確地完成任務(wù)。
本文中的源代碼在此可見。