使用
文本編輯器在APP中太常見(jiàn)了临扮,但如何實(shí)現(xiàn)的呢?不知大家有沒(méi)有跟我一個(gè)疑問(wèn)教翩?下面我將用Span來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的文本編輯器杆勇。國(guó)際慣例,先上效果圖迂曲。
怎樣靶橱,效果是不是還行,使用也很簡(jiǎn)單,只要一行代碼就能改變文本的樣式关霸!
1.添加依賴
compile 'com.leo.extendedittext:library:0.1.1'
2.布局中配置
<com.leo.extendedittext.ExtendEditText
android:id="@+id/extend_edit_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:textSize="@dimen/normal_text_size"
android:scrollbars="none"
android:background="@android:color/transparent"
app:bulletColor="@color/colorPrimary" // 著重號(hào)顏色
app:bulletRadius="@dimen/bullet_radius" // 著重號(hào)半徑
app:bulletGapWidth="@dimen/bullet_gap_width" // 著重號(hào)與文本的寬度
app:quoteColor="@color/colorPrimary" // 引用顏色
app:quoteStripeWidth="@dimen/quote_stripe_width" // 引用寬度
app:quoteGapWidth="@dimen/quote_gap_width" // 引用與文本的寬度
app:linkColor="@color/colorPrimaryDark" // 鏈接顏色
app:drawUnderLine="true" // 鏈接是否畫(huà)下劃線
app:enableHistory="true" // 是否開(kāi)啟歷史記錄
app:historyCapacity="50" // 歷史記錄容量
app:rule="EXCLUSIVE_EXCLUSIVE"> // 規(guī)則传黄,后面說(shuō)
</com.leo.extendedittext.ExtendEditText>
當(dāng)然,配置項(xiàng)也可以用代碼設(shè)置队寇,如:
mExtendEdt.enableHistory(true); // 開(kāi)啟歷史記錄
3.設(shè)置樣式
配置好了就非常簡(jiǎn)單了膘掰,只要選中文本,調(diào)用相應(yīng)的接口佳遣,所選文本就會(huì)更換樣式识埋。
- mExtendEdt.bold(); // 粗體
- mExtendEdt.italic(); // 斜體
- mExtendEdt.underline(); // 下劃線
- mExtendEdt.strikethrough(); // 刪除線
- mExtendEdt.link(); // 鏈接
- mExtendEdt.bullet(); // 著重號(hào)
- mExtendEdt.quote(); // 引用
細(xì)心的同學(xué)應(yīng)該看到我上面的配置有個(gè)app:rule的配置項(xiàng),這是設(shè)置更換樣式的規(guī)則零渐,也可以代碼設(shè)置窒舟。
mExtendEdt.setRule(Rule.EXCLUSIVE_INCLUSIVE);
有下面四個(gè)規(guī)則作用分別如下:
- Rule.EXCLUSIVE_EXCLUSIVE // 設(shè)置樣式只對(duì)選中文本有影響
- Rule.EXCLUSIVE_INCLUSIVE // 設(shè)置樣式對(duì)選中的文本有影響, 并在其后輸入的文本也會(huì)有該樣式
- Rule.INCLUSIVE_EXCLUSIVE // 設(shè)置樣式對(duì)選中的文本有影響, 并在其前輸入的文本也會(huì)有該樣式
- Rule.INCLUSIVE_INCLUSIVE // 設(shè)置樣式對(duì)選中的文本有影響, 并在其前后輸入的文本都會(huì)有該樣式
是不是還是不太懂什么意思,我舉個(gè)例子诵盼。例如我設(shè)置了EXCLUSIVE_INCLUSIVE的規(guī)則惠豺,當(dāng)我給選中文本設(shè)置為粗體時(shí),在選中文本后繼續(xù)輸入文本风宁,新增的文本也會(huì)為粗體洁墙;而在剛選中的文本前輸入文本呢,就是普通的文本樣式戒财。但經(jīng)我測(cè)試热监,除了EXCLUSIVE_EXCLUSIVE 規(guī)則以外的三種規(guī)則都不好控制...
使用就這么簡(jiǎn)單了!其實(shí)我還實(shí)現(xiàn)了鏈?zhǔn)秸{(diào)用饮寞。但發(fā)現(xiàn)鏈?zhǔn)秸{(diào)用的場(chǎng)景不多孝扛,一般設(shè)置字體都是點(diǎn)擊一個(gè)樣式圖標(biāo)設(shè)置一種樣式,所以鏈?zhǔn)秸{(diào)用就沒(méi)多大用處了骂际,看看就好疗琉。
mExtendEdt.cover()
.bold()
.italic()
.underline()
.strikethrough()
.link()
.bullet()
.quote()
.action();
原理
在講解原理之前,各位同學(xué)需要對(duì)Span有一定的了解歉铝,可以看這篇文章:【譯】Spans盈简,一個(gè)強(qiáng)大的概念/#使用自定義的span。
每種樣式對(duì)應(yīng)一個(gè)Span太示,例如粗體樣式對(duì)應(yīng)StyleSpan(Typeface.BOLD)柠贤、斜體對(duì)應(yīng)StyleSpan(Typeface.ITALIC)、下劃線對(duì)應(yīng)UnderlineSpan类缤,只要獲取到相應(yīng)的樣式臼勉,再調(diào)用Spannable.setSpan接口來(lái)設(shè)置樣式即可。
// what參數(shù)傳Span對(duì)象
// start文本開(kāi)始索引
// end文本結(jié)束索引
// flags有四個(gè)值餐弱,分別為
// Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
// Spanned.SPAN_EXCLUSIVE_INCLUSIVE
// Spanned.SPAN_INCLUSIVE_EXCLUSIVE
// Spanned.SPAN_INCLUSIVE_INCLUSIVE
public void setSpan(Object what, int start, int end, int flags);
聰明的你應(yīng)該猜到了宴霸,flags對(duì)應(yīng)的就是我上面所說(shuō)的Rule囱晴,我只是封裝一層而已。
這里可能大家有疑問(wèn)瓢谢,我怎么獲取到Spannable呢畸写?放心,Editable是繼承于Spannable的氓扛!
public interface Editable extends CharSequence, GetChars, Spannable, Appendable {
...
}
是的枯芬,我們只要繼承EditView來(lái)實(shí)現(xiàn)文本編輯器,就能獲取到Editable采郎,也就能對(duì)文本進(jìn)行樣式修改了千所。說(shuō)到這里捋一捋實(shí)現(xiàn)文本編輯器的思路:
- 創(chuàng)建繼承于EditText的View
- 獲取EditText的Editable對(duì)象
- 調(diào)用Editable的setSpan來(lái)設(shè)置樣式
怎樣?思路是不是非常簡(jiǎn)單明了蒜埋!但只要設(shè)置樣式就夠了嗎淫痰?APP往往點(diǎn)擊一個(gè)按鈕設(shè)置樣式,再點(diǎn)擊一次就清除樣式理茎『诮纾考慮到這里管嬉,設(shè)置樣式和清除樣式應(yīng)該是同一個(gè)接口比較合理皂林。好,基于此再來(lái)捋一捋思路:
- 創(chuàng)建繼承于EditText的View
- 獲取EditText的Editable對(duì)象
- 判斷選中文本是否具有將要設(shè)置樣式的樣式
- 若已設(shè)置蚯撩,清除樣式
- 若沒(méi)設(shè)置础倍,設(shè)置樣式
基于上面的思路,我定義了一個(gè)Style抽象類胎挎,下面是核心代碼:
public abstract class Style {
/**
* 改變選中文本樣式
* @param text 選中的可編輯文本
* @param start 開(kāi)始索引
* @param end 結(jié)束索引
* @param rule 規(guī)則
* @return 若設(shè)置樣式返回true, 清除樣式返回false
*/
public boolean format(Editable text, int start, int end, Rule rule) {
...
boolean result = false;
if (!isSetting(text, start, end)) {
set(text, start, end);
result = true;
} else {
remove(text, start, end);
}
return result;
}
/**
* 設(shè)置樣式
* @param text 可編輯文本
* @param start 開(kāi)始索引
* @param end 結(jié)束索引
*/
public abstract void set(Editable text, int start, int end);
/**
* 移除樣式
* @param text 可編輯文本
* @param start 開(kāi)始索引
* @param end 結(jié)束索引
*/
public abstract void remove(Editable text,int start, int end);
/**
* 選中文本是否已設(shè)置樣式
* @param text 可編輯文本
* @param start 開(kāi)始索引
* @param end 結(jié)束索引
* @return 若選中的全部文本已設(shè)置該樣式, 返回true; 反之, 返回false.
*/
public abstract boolean isSetting(Editable text, int start, int end);
...
然后各種樣式繼承Style抽象類沟启,并實(shí)現(xiàn)isSetting、set和remove方法即可犹菇。下面用粗體Bold類的實(shí)現(xiàn)代碼:
public class Bold extends Style {
@Override
public void set(Editable text, int start, int end) {
if (start >= end) {
return;
}
text.setSpan(new StyleSpan(Typeface.BOLD), start, end, mRule);
}
@Override
public void remove(Editable text, int start, int end) {
if (start >= end) {
return;
}
StyleSpan[] spans = text.getSpans(start, end, StyleSpan.class);
List<TypeBean> list = new ArrayList<>(spans.length);
for (StyleSpan span : spans) {
if (span.getStyle() == Typeface.BOLD) {
list.add(new TypeBean(text.getSpanStart(span), text.getSpanEnd(span)));
text.removeSpan(span); // remove
}
}
// 恢復(fù)未選上但與移除文本具有相同樣式的文本
for (TypeBean bean : list) {
if (bean.isValid()) {
if (bean.getStart() < start) {
set(text, bean.getStart(), start);
}
if (bean.getEnd() > end) {
set(text, end, bean.getEnd());
}
}
}
}
@Override
public boolean isSetting(Editable text, int start, int end) {
if (start >= end) {
return false;
}
// 思路: 遍歷可編輯文本, 若選中文本存在未設(shè)置該樣式的, 返回false; 反之, 返回true
StringBuilder builder = new StringBuilder();
for (int i = start; i < end; i++) {
// 獲取每個(gè)字符的樣式, 可能有重復(fù), 只需獲取判斷一次
StyleSpan[] spans = text.getSpans(i, i + 1, StyleSpan.class);
for (StyleSpan span : spans) {
if (span.getStyle() == Typeface.BOLD) {
builder.append(text.subSequence(i, i + 1).toString());
break;
}
}
}
return text.subSequence(start, end).toString().equals(builder.toString());
}
}
代碼注釋說(shuō)得很清楚了德迹,這里不多說(shuō),但有一點(diǎn)需要提醒下揭芍,清除樣式的接口是:
public void removeSpan(Object what);
可以看到, 沒(méi)有指定開(kāi)始和結(jié)束索引的胳搞,它會(huì)清除具有該樣式的所有相鄰的文本的樣式。即如果HelloWorld整個(gè)單詞是粗體称杨,如果你選中“ello”肌毅,調(diào)用removeSpan來(lái)清除粗體樣式,會(huì)把HelloWorld整個(gè)單詞的粗體樣式都清除掉姑原。所以要想只清除選中的“ello”悬而,就要先把整個(gè)單詞的粗體樣式清除,再對(duì)非選中的文本進(jìn)行樣式恢復(fù)锭汛。
另外一點(diǎn)需要注意的是笨奠,對(duì)于“著重號(hào)”袭蝗、“鏈接”等樣式,Android自帶的不能滿足我們的需求般婆,所以需要自己改下呻袭,具體不說(shuō)了,看源碼吧腺兴!
結(jié)論
目前支持樣式:
- 粗體
- 斜體
- 下劃線
- 刪除線
- 鏈接
- 著重號(hào)
- 引用
未來(lái)更新支持樣式:
- 圖片
- 背景色
參考:
- Spans左电,一個(gè)強(qiáng)大的概念。
- Spanned | Android Developers
- Spannable | Android Developers
- Knife | github
寫(xiě)篇文章不容易~ 記得幫我點(diǎn)個(gè)喜歡或者Star哈