Android Spannable

最近在研究android中emoji的顯示問題,突然對spannable,特別好奇.
查了一圈資料發現spannable是Android中的一大殺器啊
本文基本上是類似直播的形式,全文大量的粘貼注釋...

Spannable是一個接口,我們先來看看它的繼承樹.

Spannable繼承樹.png

然后是Spannable,這個接口里面居然還有個類,是一個單例類.


Spannable.png

仔細看了一下,原來Spannable也不是頂層接口...CharSequence才是...

CharSequence.png

我們先看一下CharSequence,居然是java.lang包下的,String是它的實現類...以前學java的時候都沒見過...注釋上說這個接口代表了一個有序的字符集合,而且定義了幾個方法來操作這個字符集合.

package java.lang;

/**
 * This interface represents an ordered set of characters and defines the
 * methods to probe them.
 */
public interface CharSequence

里面有這幾個方法.


CharSequence.png

接下來我們看一下Spanned接口,發現有一大堆MARK,POINT之類的常量.

Spanned.png

看一下注釋

/**
 * This is the interface for text that has markup objects attached to
 * ranges of it.  Not all text classes have mutable markup or text;
 * see {@link Spannable} for mutable markup and {@link Editable} for
 * mutable text.
 */

再看一下關于MARK和POINT的注釋

     * MARK and POINT are conceptually located <i>between</i> two adjacent characters.
     * A MARK is "attached" to the character before, while a POINT will stick to the character
     * after. The insertion cursor is conceptually located between the MARK and the POINT.
     *
     * As a result, inserting a new character between a MARK and a POINT will leave the MARK
     * unchanged, while the POINT will be shifted, now located after the inserted character and
     * still glued to the same character after it.
     *
     * Depending on whether the insertion happens at the beginning or the end of a span, the span
     * will hence be expanded to <i>include</i> the new character (when the span is using a MARK at
     * its beginning or a POINT at its end) or it will be <i>excluded</i>.
     *
     * Note that <i>before</i> and <i>after</i> here refer to offsets in the String, which are
     * independent from the visual representation of the text (left-to-right or right-to-left).

不知道大家看懂了沒,反正我是沒看懂.
不過還好,最后在StackOverFlow上找到一個答案,認認真真的看了兩個答案好幾遍,總算是有點明白了.

原文地址:

http://stackoverflow.com/questions/16531555/what-is-the-difference-between-span-point-mark-and-span-mark-point/17846413#17846413

注釋上說,MARK和POINT在兩個相鄰的字符間,MARK貼在前面一個字符后面,POINT貼在后面一個字符前面,然后我們常用的光標就是存在于MARK和POINT之間.

StackOverFlow的答案上用]來表示MARK,用[來表示POINT,我覺得是很形象的,開口的方向代表了他依附字符的方向.也就是說]依附在前面一個字符上,[依附在后面的字符上.

最后我推測,Spanned這個接口提出了SPAN這個概念,并且定義了許多類型的SPAN,每個SPAN都由MARK或SPAN包圍,也就是Spanned接口中得那些靜態常量:SPAN_MARK_MARK,SPAN_MARK_POINT,SPAN_POINT_MARK,SPAN_POINT_POINT

我再舉幾個例子,首先SPAN是有長度的,
然后SPAN是不可見的,
然后Spanned這個接口還提出了Start和End的概念,不同類型的Span,Start和End的位置也不同.
一個長度為0的SPAN_MARK_MARK相當于一個標簽,

//一個SPAN_MARK_MARK的span
//這個span的Start和End的位置是這樣的 ]]Start,End
"]]我是例句"
//無論我們是在Start還是在End出輸入內容,span店鋪不會變化
"]]hello,我是例句"

//一個SPAN_POINT_POINT的span
//這個span的Start和End的位置是這樣的 Start,End[[
"[[我是例句"
//無論我們是在Start還是在End出輸入內容,span店鋪不會變化
"hello,[[我是例句"

//一個SPAN_MARK_POINT的span
//這個span的Start和End的位置是這樣的 ]Start,End[
"][我是例句"
//SPAN_MARK_POINT又叫做SPAN_INCLUSIVE_INCLUSIVE,意思是在Start處和在End處添加內容都會包括在Span中
在Start處輸入 "]hello,[我是例句"
在End處輸入 "]hello,[我是例句" 

//一個SPAN_POINT_MARK的span
//這個span的Start和End的位置是這樣的 Start[]End
"[]我是例句"
//SPAN_MARK_POINT又叫做SPAN_EXCLUSIVE_EXCLUSIVE,意思是在Start處和在End處添加內容都不會包括在Span中
"hello,[]我是例句"
"[]hello,我是例句"
"he[]llo,我是例句"

當然,目前為止,我剛剛說的都是猜測.

后來發現SPAN_MARK_MARK,SPAN_MARK_POINT,SPAN_POINT_MARK,SPAN_POINT_POINT這些常量是作為flag來應用的,每個flag都可以設置成其中任意一種.

接下來我們回來看 Spannable 這個類,最常用的就是這個setSpan

Spannable

接著貼注釋...注釋里說這個方法會把指定的標記對象貼到startend之間,其中的第四個參數flag我前文已經解釋的很清楚了.
所以我們接著看第一個參數Object what,注釋里說這個what可以給文字加特技,加功能.

/**
     * Attach the specified markup object to the range <code>start…end</code>
     * of the text, or move the object to that range if it was already
     * attached elsewhere.  See {@link Spanned} for an explanation of
     * what the flags mean.  The object can be one that has meaning only
     * within your application, or it can be one that the text system will
     * use to affect text display or behavior.  Some noteworthy ones are
     * the subclasses of {@link android.text.style.CharacterStyle} and
     * {@link android.text.style.ParagraphStyle}, and
     * {@link android.text.TextWatcher} and
     * {@link android.text.SpanWatcher}.
     */
    public void setSpan(Object what, int start, int end, int flags);

注釋里還說Some noteworthy ones are the subclasses of {@link android.text.style.CharacterStyle} ...于是找到了這個類CharacterStyle,我們接著看注釋...

/**
 * The classes that affect character-level text formatting extend this
 * class.  Most extend its subclass {@link MetricAffectingSpan}, but simple
 * ones may just implement {@link UpdateAppearance}.
 */

CharacterStyle能夠進行字符級別的格式化的類都集成自這個類.我們看下繼承樹,可以發現很多熟悉的Span,比如URLSpan,StyleSpan,ImageSpan,我猜這些就是可選的what吧.

CharacterStyle繼承樹.png

種類繁多,我們先看些碼一些代碼試試效果,那個鏈接還不能點擊,我們慢慢解決.

span效果圖.png

代碼如下,試了BackgroundColorSpan,StyleSpan,URLSpan,ImageSpan四種,效果還可以

tvSpan = (TextView) findViewById(R.id.tv_span);
SpannableStringBuilder ssb= new SpannableStringBuilder("public static void main \n www.lxweimin.com");

ssb.setSpan(new BackgroundColorSpan(0x88ff0000),0,6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new StyleSpan(Typeface.BOLD_ITALIC),0,6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new URLSpan("www.lxweimin.com"),26,41,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

Drawable drawable = getResources().getDrawable(R.drawable.p1);
drawable.setBounds(0,0,tvSpan.getLineHeight(),tvSpan.getLineHeight());
ssb.setSpan(new ImageSpan(drawable,1),7,13,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

tvSpan.setText(ssb);

我們先看BackgroundColorSpan這個類,看看他的setSpan方法是如何實現的.注釋上說The flags determine how the span will behave when text is inserted at the start or end of the span's range.說明上面的內容還是靠譜的.這個方法調用了另外一個更多參數的setSpan.

    /**
     * Mark the specified range of text with the specified object.
     * The flags determine how the span will behave when text is
     * inserted at the start or end of the span's range.
     */
    public void setSpan(Object what, int start, int end, int flags) {
        setSpan(true, what, start, end, flags);
    }

setSpan(true, what, start, end, flags);這個方法100多行,除了吧把span存到mSpans,并沒有找到更多線索,找了一圈,發現也許和SpanWatcher及他的子類有關系,看的仔細的同學應該發現前面的注釋里有提到過....

唔,這個接口看起來很像是span的處理者,第二個參數就是what,不過,這個接口是觀察者也說不定...

SpanWatcher.png
/** 
    * When an object of this type is attached to a Spannable, its methods 
    * will be called to notify it that other markup objects have been * added, changed, or removed. */

果然是搞錯了,這個是觀察者...也許如何操縱Span的代碼在TextView里?

打開TextView的源碼,先看看import信息

import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;

好多span相關的類,看來相應的邏輯應該在這里了.
but...TextView的源碼有10194行...怎么看啊...

找了半天,在第8061行找到了一個看起來很關鍵的方法,但是沒注釋...

 void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd)

看完發現這個方法也不是我要找的....

既然自己找不到,還是去Google查一查
于是找到了這篇文章

http://flavienlaurent.com/blog/2014/01/31/spans/

開篇第一句

When you set text on a TextView, it uses the base class Layout to manage text rendering.

接下來看到這個類,不過這個類不在SDK里,而是在android源碼里,也就是AOSP的代碼里,
這個java文件的路徑是frameworks/base/core/java/android/text/TextLine.java

/**
 * Represents a line of styled text, for measuring in visual order and
 * for rendering.
 *
 * <p>Get a new instance using obtain(), and when finished with it, return it
 * to the pool using recycle().
 *
 * <p>Call set to prepare the instance for use, then either draw, measure,
 * metrics, or caretToLeftRightOf.
 *
 * @hide
 */
class TextLine

已經下到源碼了= =\這個貌似已經超出了我的實力范圍...接著ctrl+V


android.text.TextLine documentation says: Represents a line of styled text, for measuring in visual order and for rendering.

TextLine class contains 3 sets of Spans:

MetricAffectingSpan set
CharacterStyle set
ReplacementSpan set
The interesting method is TextLine#handleRun. It’s where all Spans are used to render the text. Relative to the type of Span, TextLine calls:

CharacterStyle#updateDrawState to change the TextPaint configuration for MetricAffectingSpan and CharacterStyle Spans.
TextLine#handleReplacement for ReplacementSpan. It calls Replacement#getSize to get the replacement width, update the font metrics if it’s needed and finally call Replacement#draw.


更多的Span.png

總結

最后也沒能把每個過程都找清楚,這篇文章別看沒什么質量,可使卻也是花了我三天,15個小時左右的時間才寫出來的,寫完之后覺得并沒有什么收獲...
但是看到了許多酷酷的東西還是覺得很開心,感覺打開了一扇新的大門,以前認為不可能實現的功能真實的出現在眼前,感覺好爽.

blog還是要寫的,越是怕文章沒內容越是要寫,因為努力的想要寫出起碼差強人意的文章,也是多努力了好多,多堅持了好久.所以也看到了許多實戰幾個月都接觸不到的東西.

最后推薦這篇文章,剛剛沒點開的同學一定不要錯過啊!

http://flavienlaurent.com/blog/2014/01/31/spans/

你看,效果酷不酷!

Fireworks

還可以這樣


animateblur

還有這樣


animatetypewriter
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 在項目中很常用到Spannable,既豐富了文本又精簡了布局。SpannableString、Spanna...
    尋味Android閱讀 1,416評論 0 2
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,497評論 0 17
  • 前言 工作找完了,已經干了兩個星期。雖然經常加班,不過相比之前的工作,現在過得更加充實、更有意義。現在有點空閑時間...
    帶心情去旅行閱讀 73,055評論 42 237
  • 【向前一步】 女性獲取成功一般會比男性更艱難, 我們應該從下面三點去突破。 1.勇敢點 女性和男性相比總是膽子不夠...
    K王之姐閱讀 108評論 0 0