最近在研究android中emoji的顯示問題,突然對(duì)spannable,特別好奇.
查了一圈資料發(fā)現(xiàn)spannable是Android中的一大殺器啊
本文基本上是類似直播的形式,全文大量的粘貼注釋...
Spannable是一個(gè)接口,我們先來看看它的繼承樹.
然后是Spannable,這個(gè)接口里面居然還有個(gè)類,是一個(gè)單例類.
仔細(xì)看了一下,原來Spannable也不是頂層接口...CharSequence才是...
我們先看一下CharSequence,居然是java.lang包下的,String是它的實(shí)現(xiàn)類...以前學(xué)java的時(shí)候都沒見過...注釋上說這個(gè)接口代表了一個(gè)有序的字符集合,而且定義了幾個(gè)方法來操作這個(gè)字符集合.
package java.lang;
/**
* This interface represents an ordered set of characters and defines the
* methods to probe them.
*/
public interface CharSequence
里面有這幾個(gè)方法.
接下來我們看一下Spanned接口,發(fā)現(xiàn)有一大堆MARK,POINT之類的常量.
看一下注釋
/**
* 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.
*/
再看一下關(guān)于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上找到一個(gè)答案,認(rèn)認(rèn)真真的看了兩個(gè)答案好幾遍,總算是有點(diǎn)明白了.
原文地址:
注釋上說,MARK和POINT在兩個(gè)相鄰的字符間,MARK貼在前面一個(gè)字符后面,POINT貼在后面一個(gè)字符前面,然后我們常用的光標(biāo)就是存在于MARK和POINT之間.
StackOverFlow的答案上用]
來表示MARK,用[
來表示POINT,我覺得是很形象的,開口的方向代表了他依附字符的方向.也就是說]
依附在前面一個(gè)字符上,[
依附在后面的字符上.
最后我推測,Spanned這個(gè)接口提出了SPAN這個(gè)概念,并且定義了許多類型的SPAN,每個(gè)SPAN都由MARK或SPAN包圍,也就是Spanned接口中得那些靜態(tài)常量:SPAN_MARK_MARK
,SPAN_MARK_POINT
,SPAN_POINT_MARK
,SPAN_POINT_POINT
我再舉幾個(gè)例子,首先SPAN是有長度的,
然后SPAN是不可見的,
然后Spanned這個(gè)接口還提出了Start和End的概念,不同類型的Span,Start和End的位置也不同.
一個(gè)長度為0的SPAN_MARK_MARK
相當(dāng)于一個(gè)標(biāo)簽,
//一個(gè)SPAN_MARK_MARK的span
//這個(gè)span的Start和End的位置是這樣的 ]]Start,End
"]]我是例句"
//無論我們是在Start還是在End出輸入內(nèi)容,span店鋪不會(huì)變化
"]]hello,我是例句"
//一個(gè)SPAN_POINT_POINT的span
//這個(gè)span的Start和End的位置是這樣的 Start,End[[
"[[我是例句"
//無論我們是在Start還是在End出輸入內(nèi)容,span店鋪不會(huì)變化
"hello,[[我是例句"
//一個(gè)SPAN_MARK_POINT的span
//這個(gè)span的Start和End的位置是這樣的 ]Start,End[
"][我是例句"
//SPAN_MARK_POINT又叫做SPAN_INCLUSIVE_INCLUSIVE,意思是在Start處和在End處添加內(nèi)容都會(huì)包括在Span中
在Start處輸入 "]hello,[我是例句"
在End處輸入 "]hello,[我是例句"
//一個(gè)SPAN_POINT_MARK的span
//這個(gè)span的Start和End的位置是這樣的 Start[]End
"[]我是例句"
//SPAN_MARK_POINT又叫做SPAN_EXCLUSIVE_EXCLUSIVE,意思是在Start處和在End處添加內(nèi)容都不會(huì)包括在Span中
"hello,[]我是例句"
"[]hello,我是例句"
"he[]llo,我是例句"
當(dāng)然,目前為止,我剛剛說的都是猜測.
后來發(fā)現(xiàn)SPAN_MARK_MARK
,SPAN_MARK_POINT
,SPAN_POINT_MARK
,SPAN_POINT_POINT
這些常量是作為flag來應(yīng)用的,每個(gè)flag都可以設(shè)置成其中任意一種.
接下來我們回來看 Spannable
這個(gè)類,最常用的就是這個(gè)setSpan
了
接著貼注釋...注釋里說這個(gè)方法會(huì)把指定的標(biāo)記對(duì)象貼到start
和end
之間,其中的第四個(gè)參數(shù)flag
我前文已經(jīng)解釋的很清楚了.
所以我們接著看第一個(gè)參數(shù)Object what
,注釋里說這個(gè)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} ...
于是找到了這個(gè)類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
能夠進(jìn)行字符級(jí)別的格式化的類都集成自這個(gè)類.我們看下繼承樹,可以發(fā)現(xiàn)很多熟悉的Span,比如URLSpan
,StyleSpan
,ImageSpan
,我猜這些就是可選的what
吧.
種類繁多,我們先看些碼一些代碼試試效果,那個(gè)鏈接還不能點(diǎn)擊,我們慢慢解決.
代碼如下,試了BackgroundColorSpan
,StyleSpan
,URLSpan
,ImageSpan
四種,效果還可以
tvSpan = (TextView) findViewById(R.id.tv_span);
SpannableStringBuilder ssb= new SpannableStringBuilder("public static void main \n www.reibang.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.reibang.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這個(gè)類,看看他的setSpan方法是如何實(shí)現(xiàn)的.注釋上說The flags determine how the span will behave when text is inserted at the start or end of the span's range.說明上面的內(nèi)容還是靠譜的.這個(gè)方法調(diào)用了另外一個(gè)更多參數(shù)的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);
這個(gè)方法100多行,除了吧把span存到mSpans
,并沒有找到更多線索,找了一圈,發(fā)現(xiàn)也許和SpanWatcher
及他的子類有關(guān)系,看的仔細(xì)的同學(xué)應(yīng)該發(fā)現(xiàn)前面的注釋里有提到過....
唔,這個(gè)接口看起來很像是span的處理者,第二個(gè)參數(shù)就是what
,不過,這個(gè)接口是觀察者也說不定...
/**
* 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. */
果然是搞錯(cuò)了,這個(gè)是觀察者...也許如何操縱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相關(guān)的類,看來相應(yīng)的邏輯應(yīng)該在這里了.
but...TextView的源碼有10194行...怎么看啊...
找了半天,在第8061
行找到了一個(gè)看起來很關(guān)鍵的方法,但是沒注釋...
void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd)
看完發(fā)現(xiàn)這個(gè)方法也不是我要找的....
既然自己找不到,還是去Google查一查
于是找到了這篇文章
開篇第一句
When you set text on a TextView, it uses the base class Layout to manage text rendering.
接下來看到這個(gè)類,不過這個(gè)類不在SDK里,而是在android源碼里,也就是AOSP的代碼里,
這個(gè)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
已經(jīng)下到源碼了= =\這個(gè)貌似已經(jīng)超出了我的實(shí)力范圍...接著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.
總結(jié)
最后也沒能把每個(gè)過程都找清楚,這篇文章別看沒什么質(zhì)量,可使卻也是花了我三天,15個(gè)小時(shí)左右的時(shí)間才寫出來的,寫完之后覺得并沒有什么收獲...
但是看到了許多酷酷的東西還是覺得很開心,感覺打開了一扇新的大門,以前認(rèn)為不可能實(shí)現(xiàn)的功能真實(shí)的出現(xiàn)在眼前,感覺好爽.
blog還是要寫的,越是怕文章沒內(nèi)容越是要寫,因?yàn)榕Φ南胍獙懗銎鸫a差強(qiáng)人意的文章,也是多努力了好多,多堅(jiān)持了好久.所以也看到了許多實(shí)戰(zhàn)幾個(gè)月都接觸不到的東西.
最后推薦這篇文章,剛剛沒點(diǎn)開的同學(xué)一定不要錯(cuò)過啊!
你看,效果酷不酷!
還可以這樣
還有這樣