用Span簡(jiǎn)單實(shí)現(xiàn)文本編輯器

使用

文本編輯器在APP中太常見(jiàn)了临扮,但如何實(shí)現(xiàn)的呢?不知大家有沒(méi)有跟我一個(gè)疑問(wèn)教翩?下面我將用Span來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的文本編輯器杆勇。國(guó)際慣例,先上效果圖迂曲。

文本編輯器演示.gif

怎樣靶橱,效果是不是還行,使用也很簡(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)文本編輯器的思路:

  1. 創(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)捋一捋思路:

  1. 創(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)更新支持樣式:

  • 圖片
  • 背景色

參考:

寫(xiě)篇文章不容易~ 記得幫我點(diǎn)個(gè)喜歡或者Star哈


源碼下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末页响,一起剝皮案震驚了整個(gè)濱河市篓足,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌闰蚕,老刑警劉巖栈拖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異没陡,居然都是意外死亡涩哟,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)盼玄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)贴彼,“玉大人,你說(shuō)我怎么就攤上這事埃儿∑髡蹋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵童番,是天一觀的道長(zhǎng)精钮。 經(jīng)常有香客問(wèn)我,道長(zhǎng)剃斧,這世上最難降的妖魔是什么轨香? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮幼东,結(jié)果婚禮上臂容,老公的妹妹穿的比我還像新娘。我一直安慰自己筋粗,他們只是感情好策橘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著娜亿,像睡著了一般丽已。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上买决,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天沛婴,我揣著相機(jī)與錄音吼畏,去河邊找鬼。 笑死嘁灯,一個(gè)胖子當(dāng)著我的面吹牛泻蚊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丑婿,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼性雄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了羹奉?” 一聲冷哼從身側(cè)響起秒旋,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诀拭,沒(méi)想到半個(gè)月后迁筛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耕挨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年细卧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筒占。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贪庙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赋铝,到底是詐尸還是另有隱情插勤,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布革骨,位于F島的核電站,受9級(jí)特大地震影響析恋,放射性物質(zhì)發(fā)生泄漏良哲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一助隧、第九天 我趴在偏房一處隱蔽的房頂上張望筑凫。 院中可真熱鬧,春花似錦并村、人聲如沸巍实。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)棚潦。三九已至,卻和暖如春膝昆,著一層夾襖步出監(jiān)牢的瞬間丸边,已是汗流浹背叠必。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工痛倚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留帖汞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓掀潮,卻偏偏與公主長(zhǎng)得像骄呼,于是被迫代替她去往敵國(guó)和親共苛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容