先上效果圖
一豌研、初識TextView富文本
接觸Android以來契讲,都不知道TextView能完成的事情原來不止于顯示文字這么簡單芯勘。一個TextView能完成的事情意想不到。這次接到導(dǎo)師布置的任務(wù)需求一句話概括就是完成類似美拍社區(qū)中装蓬,我的關(guān)注模塊中列表里面酷酷的文本顯示著拭。
Span的基本使用
XxxSpan span = new XxxSpan();
SpannableString spannableString = new SpannableString("hello world");
spannableString.setSpan(span,0,spannableString.length/2,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
上面的四行代碼是Span最基本的使用,可以解決一些簡單的需求牍帚。例如要設(shè)置某些文字為某種顏色儡遮,區(qū)別于其他文字就是new一個ForegroundSpan并且在構(gòu)造函數(shù)中傳入相應(yīng)顏色。
具體需求
要求對一段服務(wù)器下發(fā)的文本以及附加的文本信息對文本的顯示進(jìn)行處理暗赶。
- 以#開頭#結(jié)尾的標(biāo)記著是一個話題鄙币。
- 以@開頭空格結(jié)尾的為@某個人的ID。
- 以http://開頭的url根據(jù)服務(wù)器下發(fā)的附加文本信息將其替換為自定義的樣式蹂随。
- 以上三點要求點擊的時候要實現(xiàn)二態(tài)的效果十嘿。
思路分析
理清思路,要實現(xiàn)功能岳锁,大致可以分為如下的兩個個大的方面:
- 一是處理View層面绩衷。對于文本樣式、以及點擊的時候的兩種狀態(tài)的變化屬于界面上的變化激率,在TextView上面要做成酷炫的界面效果咳燕,就得依賴于Span。
- 二是數(shù)據(jù)的處理乒躺,處理后在界面呈現(xiàn)迟郎。
二、面向接口的嘗試聪蘸。
在處理界面的時候,樣式的實現(xiàn)就像上面Span的基本使用一樣,比較簡單健爬。對于我媒至,難到我的地方在于點擊的時候谴蔑,兩種狀態(tài)的變換 ,嘗試了兩種方法。
方法一:多個Span的疊加處理
因為Span不會有排他性崇呵,所以在開始自己的思路很容易就想到了,在原本已經(jīng)設(shè)置好的各個樣式的基礎(chǔ)之上添加一個ClickableSpan蛮瞄,繼承LinkMovementMethod并重寫onTouchEvent浴井。在onTouchEvent中處理事件,當(dāng)ACTION_DOWN的事件中添加一個背景樣式纳胧,ACTION_UP和ACTION_CANCEL的事件中移除添加的背景樣式镰吆。這樣就可以實現(xiàn)點擊時兩種狀態(tài)效果的切換。
上述的方法跑慕,可以達(dá)到相應(yīng)的效果万皿。但是考慮代碼的擴(kuò)展性,這樣做是很不好的核行。每當(dāng)增加擴(kuò)展一種樣式都要去變動onTouchEvent方法中代碼牢硅,無形中可能會造成后期更改導(dǎo)致之前已經(jīng)通過測試的代碼出現(xiàn)bug。違反了開閉原則芝雪。
方法二:定義接口减余,讓Span完成一些該完成的事情
在接觸自定義View的時候,知道自定義View最重要的三點就是:繪制惩系、布局位岔、事件邏輯處理。而這里要完成的富文本不會涉及到布局蛆挫。所以從繪制和事件處理兩方面考慮赃承,會得到一些啟發(fā)。當(dāng)ACTION_DOWN事件的時候悴侵,改變一下Span的背景顏色瞧剖,并通知重繪。ACTION_UP和ACTION_CANCEL將Span的背景顏色更改回原來的顏色可免,并通知重繪抓于。這種做法就能夠達(dá)到兩種狀態(tài)的切換。
進(jìn)而可以抽象出一個可點擊Span都要遵循的規(guī)范的接口:
public interface ITouchSpan {
boolean onTouchDown(TextView widget);
boolean onTouchUp(TextView widget);
boolean onTouchCancel(TextView widget);
}
自定義的Span實現(xiàn)改接口之后浇借,override上面三個方法捉撮,增加一些自己的邏輯,在調(diào)用TextView的invalidate()方法就能達(dá)到相應(yīng)的點擊效果妇垢。
在LinkMovementMethod的onTouchEvent方法中可以這樣處理
ITouchSpan[] link = buffer.getSpans(off, off, ITouchSpan.class);
if (action == MotionEvent.ACTION_DOWN) {
if (link.length != 0) {
mTouchSpan = link[0];
mTouchSpan.onTouchDown(widget);
}
} else if (action == MotionEvent.ACTION_UP) {
if (mTouchSpan != null) {
mTouchSpan.onTouchUp(widget);
mTouchSpan = null;
}
} else if (action == MotionEvent.ACTION_CANCEL) {
if (mTouchSpan != null) {
mTouchSpan.onTouchCancel(widget);
mTouchSpan = null;
}
}
在onTouchEvent()方法中只需要對接口的引用進(jìn)行處理巾遭,而不涉及具體的實現(xiàn)肉康。當(dāng)我們需要擴(kuò)展一個新的樣式,并要求該樣式可點擊灼舍,點擊的時候要有某種特定的效果吼和。我們只擴(kuò)展一個類實現(xiàn)ITouchSpan,在相應(yīng)的方法中實現(xiàn)即可擴(kuò)展骑素。好處在于增加一種新的樣式并不需要太多的更改onTouchEvent()的方法炫乓,只需要增加一個樣式類并實現(xiàn)ITouchSpan接口就可以達(dá)到效果。
三献丑、數(shù)據(jù)處理的抽離
數(shù)據(jù)邏輯的處理大致分為三個部分:
- 正則匹配服務(wù)器下發(fā)的一段文本信息末捣。記錄下三種樣式的起始位置。因為每種樣式的個數(shù)是不確定的创橄,所以要用一個列表進(jìn)行存儲樣式的起始位置箩做。
- 根據(jù)位置列表,設(shè)置相應(yīng)的Span
- 最后一個是一個小細(xì)節(jié)筐摘。根據(jù)icon圖標(biāo)的url地址去下載相應(yīng)的icon圖標(biāo)卒茬。
剛開始處理數(shù)據(jù)主要是在Adapter中定義三個方法,onBindViewHolder調(diào)用這三個方法分別處理三種樣式咖熟。類似第二點圃酵,每當(dāng)新增一個樣式的時候,都需要在Adapter中增加一個方法馍管,這樣做就會使得Adapter這個類要做的事情越來越多郭赐,功能也越來越復(fù)雜。不符合單一職責(zé)原則确沸。
換一種方式思考捌锭,樣式數(shù)據(jù)邏輯處理交由某個類幫忙處理,Adapter只需要調(diào)用該類對象的方法即可 這個思路就可以做到較好的將adapter里面復(fù)雜且冗余的數(shù)據(jù)處理進(jìn)行抽離罗捎。
具體的解決方法:
定義一個接口观谦,規(guī)定了數(shù)據(jù)綁定的方法。每當(dāng)新增加一個樣式桨菜,要處理該樣式的數(shù)據(jù)豁状,只需要實現(xiàn)改接口,就能夠處理數(shù)據(jù)邏輯倒得。
public interface IBindStyleListStrategy {
void bindStyleList(TextView textView, Spannable text, Comment comment, Context context);
}
Adapter中根據(jù)相應(yīng)的策略的具體實現(xiàn)類去綁定具體的樣式泻红。
//根據(jù)樣式的類型設(shè)置樣式
for (int i = 0; i < BindStyleListStrategyFactory.STYLE_COUNT; i++) {
iBindStyleListStrategy = BindStyleListStrategyFactory.getBindStyleList(i);
if (iBindStyleListStrategy != null) {
//綁定樣式
iBindStyleListStrategy.bindStyleList(holder.textView, text, comment, holder.itemView.getContext());
}
}
//顯示數(shù)據(jù)
holder.textView.setText(text);
BindStyleListStrategyFactory的具體實現(xiàn)
public class BindStyleListStrategyFactory {
//樣式種類個數(shù)
public static final int STYLE_COUNT = 3;
//話題類型
public static final int TOPIC_TYPE = 0;
//URL鏈接類型
public static final int URL_TYPE = 1;
//AtUser類型
public static final int AT_USER_TYPE = 2;
//返回具體實現(xiàn)類
public static IBindStyleListStrategy getBindStyleList(int type) {
switch (type) {
case TOPIC_TYPE:
return new BindTopicListStrategy();
case AT_USER_TYPE:
return new BindAtUserListStrategy();
case URL_TYPE:
return new BindLinkUrlListStrategy();
}
return null;
}
}
上述的解決辦法,在增加一個樣式類型的時候霞掺。只需要定義一個類實現(xiàn)IBindStyleListStrategy接口并實現(xiàn)對應(yīng)的方法谊路,在BindStyleListStrategyFactory中修改樣式種類個數(shù),以及增加返回具體的類型的對象即可擴(kuò)展樣式菩彬,而不需要每增加一個樣式類型改動adapter中的代碼缠劝。
四潮梯、代碼的迭代與收獲總結(jié)
細(xì)節(jié)處理總結(jié)
- 在某些細(xì)節(jié)處理,可能剛開始做的沒有很到位剩彬。例如剛開始的點擊事件處理的代碼
SpanUtils.setForegroundColorSpan(text, color, atUser.getStart(), atUser.getEnd());
SpanUtils.setClickSpan(text, atUser.getStart(), atUser.getEnd(), atUser,
new SpanUtils.SpanClickListener() {
@Override
public void onClick(Object o) {
AtUser atUser = (AtUser) o;
Intent intent = new Intent(mActivity, SecondActivity.class);
intent.putExtra("text", atUser.getUrl());
mActivity.startActivity(intent);
}
});
}
上面的代碼每當(dāng)設(shè)置一個Span就要new一個回調(diào)接口的對象酷麦,而其實new出來的對象做的事情都是類似的,權(quán)衡之下喉恋,如果將事件的處理放到Span內(nèi)部并結(jié)合上面的onTouchUp(TextView widget);方法中處理,可以少寫很多重復(fù)的代碼母廷。
-
圖片的加載
在處理鏈接樣式的時候轻黑,需要根據(jù)url地址去加載網(wǎng)絡(luò)上面的圖標(biāo)。我學(xué)習(xí)了Glide的基本使用的方法琴昆,在Glide提供的回調(diào)接口中獲取圖片的Bitmap對象氓鄙。得到Bitmap之后,將其設(shè)置給相應(yīng)的Span并通知TextView重繪业舍。剛開始處理的時候抖拦,每加載一張圖片就重繪一次。如果采取這種方式舷暮,會導(dǎo)致TextView被重繪多次态罪,性能不夠好。更好的方式是全部的圖片加載完成之后下面,再進(jìn)行一次重繪复颈。解決方法如下:
/** * 檢查一個TextView中所有的鏈接的Icon圖標(biāo)是否全部加載完成 * * @param linkUrlSpanList Span列表 * @return 返回是否全部加載完成 */ private boolean checkAllLoadSuccess(List<LinkUrlSpan> linkUrlSpanList) { boolean loadAll = true; for (LinkUrlSpan linkUrlSpan : linkUrlSpanList) { loadAll = linkUrlSpan.isLoadSuccess() && loadAll; } return loadAll; }
總結(jié)
通過這次Demo的練習(xí),對接口的使用沥割,設(shè)計模式兩個基本原則:開閉原則耗啦、單一職責(zé)原則,簡單工廠模式机杜、策略模式有了更深的理解帜讲。其次是學(xué)習(xí)了Glide的基本使用。在自定義Span實現(xiàn)鏈接的樣式的那一塊椒拗,在學(xué)習(xí)自定義view的時候有一定的了解似将,所以直接在draw方法中進(jìn)行繪制,完成較快陡叠。