SpannableString

支援API level 1以上

在android中,需要使用文字,通常第一個想到的都會是Textview
但Textview每個字的文字字體大小、字形、字體顏色和背景基本上都是一致的
如果要達成下圖這種效果,許多人都會使用2個Textview,外面再包上一層layout進行排版

不過這樣會讓我們的UI的Layout Hierarchies(中文好像是 布局的架構層級,不太會翻所以用英文)變得複雜許多

而android中的SpannableString就是為了解決這個問題而誕生的
SpannableString能夠在一個Textview中同時呈現不同大小、字體顏色、背景顏色、文字的字體
甚至還可以在文字中加入圖片

同時他還有一個雙胞胎兄弟,叫做SpannableStringBuilder
他們的差別就像是Java中的StringStringBuilder
SpannableStringBuilder可以串接字串,而SpannableString不能夠串接字串

簡單來說
SpannableString在實體化(new)時,必需給定要顯示的字串,再對該字串的某部分更改樣式
SpannableStringBuilder則實體化(new)時不需給定要顯示的字串,可以透過append()再進行字串串接

SpannableString & SpannableStringBuilder 常用方法

SpannableStringBuilderSpannableString主要是使用
setSpan(Object what, int start, int end, int flags)這個方法更改字串的樣式

參數介紹一下:

  • what(Object ):要設定的Span樣式物件,主要包含下列這幾種:
物件 說明
BackgroundColorSpan 文字背景色
ForegroundColorSpan 文字顏色
MaskFilterSpan 幫文字加上模糊效果和浮雕效果
StrikethroughSpan 在文字加上刪除線
UnderlineSpan 在文字下方加上下底線
AbsoluteSizeSpan 設定文字大小(絕對大小)
RelativeSizeSpan 設定文字大小(相對於現有文字的大小)
ImageSpan 在文字中插入圖片
ScaleXSpan 基於X軸縮放文字,在高度不變的情況下將文字水平放大
StyleSpan 文字樣式(粗體、斜體等等)
SubscriptSpan 文字的下標(右下角)
SuperscriptSpan 文字的上標(右上角)
TypefaceSpan 文字字體
BulletSpan 在文字前加上Unordered list的圓點(•)
QuoteSpan 引用段落文字(文字段落左邊出現一條線)
AlignmentSpan 文字對齊方式
TextAppearanceSpan 文字外型(包括字體、大小、樣式和顏色)
TypefaceSpan 文字字體
URLSpan 文字超連結
ClickableSpan 讓文字變成可以點擊的文字
  • start:指定Span的開始位置,位置從0開始計算,也就是第一個字的位置是0

  • end: 指定Span的在字串中的结束位置,樣式不會套用在這個位置的文字

  • flags:主要常用的值有下面4種,但介紹前要先定義一下名詞

    包含前/後面: 在字串前/後加入新的字串,新字串會套用被設定的樣式
    不包含前/後面:在字串前/後面加入新字串,新字串不會套用被設定的樣式

flag 說明
Spannable.SPAN_INCLUSIVE_EXCLUSIVE 包含前面,不包含後面
Spannable.SPAN_INCLUSIVE_INCLUSIVE 包含前面,包含後面
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 不包含前面,不包含後面 (這個最常用)
Spannable.SPAN_EXCLUSIVE_INCLUSIVE 不包含前面,包含後面

範例程式碼

先上基本的範例程式碼,全部都只有一個TextView,但會更改changeTextAppearance()這個method的內容去demo不同的效果

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="io.northbei.spannablestring.MainActivity">

    <--就只有一個TextView,置中顯示,字體大小預設30sp-->
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"/>

</RelativeLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    TextView tv;
    //周杰倫,七里香歌詞
    String lyrics1 = "窗外的麻雀 在電線桿上多嘴;";
    String lyrics2 = "妳說這一句 很有夏天的感覺;";
    String lyrics3 = "手中的鉛筆 在紙上來來回回;";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = (TextView) findViewById(R.id.textView);
        changeTextAppearance();
    }

    //會用changeTextAppearance()填入textview的文字內容跟測試不同的文字外觀
    private void changeTextAppearance(){

    }
}

Example Demo & Code

  • setSpan()方法中,flags參數內INCLUSIVEEXCLUSIVE的差別

    private void changeTextAppearance(){
    
        SpannableStringBuilder spb = new SpannableStringBuilder();
        //先放入lyrics2
        spb.append(lyrics2);
        //設定lyrics2字串的顏色為藍色,並且指定樣式的套用方式
        spb.setSpan(new ForegroundColorSpan(Color.BLUE), 0, lyrics2.length(), Spanned.下面demo中的Value);
        //在lyrics2後面加入lyrics3
        spb.append(lyrics3);
    
        //在一開始時插入lyrics1
        spb.insert(0,lyrics1);
        tv.setText(spb);
    }
    

Spannable.SPAN_INCLUSIVE_EXCLUSIVE:前面有新加入的字串會套用該樣式,但是後面加入的新字串不會

Spannable.SPAN_INCLUSIVE_INCLUSIVE:前後有新的字串加入的話都會套用該樣式

Spannable.SPAN_EXCLUSIVE_EXCLUSIVE:前後加入的字串都不會套用該樣式

Spannable.SPAN_EXCLUSIVE_INCLUSIVE:前面有新加入的字串不會套用該樣式,而後面加入的新字串會套用


  • BackgroundColorSpan: 文字背景顏色
private void changeTextAppearance(){
  SpannableString ss = new SpannableString(lyrics1);
  //把"麻雀"這兩個字的背景顏色變成黃色,前後插入的文字不會更改
  ss.setSpan(new BackgroundColorSpan(Color.YELLOW), 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
  tv.setText(ss);
}

  • ForegroundColorSpan: 文字顏色
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //把"麻雀"這兩個字的文字顏色變成FB藍色,前後插入的文字不會更改
    ss.setSpan(new ForegroundColorSpan(Color.parseColor("#4267b2")), 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • MaskFilterSpan: 幫文字加上模糊效果和浮雕效果 需要先用MaskFilterSpan(MaskFilter filter)實體化(new)出MaskFilterSpan的物件,再將該物件交給setSpan()設定樣式
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //浮雕效果
    MaskFilterSpan embossMaskFilterSpan = new MaskFilterSpan(new EmbossMaskFilter(new float[] { 1, 1, 1 }, 0.4f, 6, 3.5f));
    //模糊效果
    MaskFilterSpan blurMaskFilterSpan = new MaskFilterSpan(new BlurMaskFilter(3, BlurMaskFilter.Blur.OUTER));

    //可以使用多次setSpan來對一串文字設定多種文字樣式

    //把"窗外的"這三個字加上浮雕效果
    ss.setSpan(embossMaskFilterSpan, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //把"電線杆"加上模糊效果
    ss.setSpan(blurMaskFilterSpan, 7, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

自己用了之後發現...模糊效果反而比較像是浮雕
由於模糊的參數我還不太會調整,文字整個就不見了QQ
但是可以確定是有加上效果的哈哈,這邊就當作錯誤示範吧(!?)

  • StrikethroughSpan:在文字加上刪除線
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //刪除線物件
    StrikethroughSpan strikethroughSpan = new StrikethroughSpan();
    //把"多"加上刪除線
    ss.setSpan(strikethroughSpan, lyrics1.length()-3, lyrics1.length()-2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • UnderlineSpan:在文字下方加上下底線
  private void changeTextAppearance(){
     SpannableString ss = new SpannableString(lyrics1);
     //下底線物件
     UnderlineSpan underlineSpan = new UnderlineSpan();
     //把"窗外的麻雀"加上下底線
     ss.setSpan(underlineSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
     tv.setText(ss);
 }

  • AbsoluteSizeSpan:設定文字大小(絕對大小)。原本文字的大小是30dp,把部分文字設成12dp
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //設定字體大小為12,true表示單位是dp,預設是false,表示單位是px
    AbsoluteSizeSpan absoluteSizeSpan = new AbsoluteSizeSpan(12, true);
    //窗外的麻雀之後的文字全部改成12dp
    ss.setSpan(absoluteSizeSpan, 5, lyrics1.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • RelativeSizeSpan:設定文字大小(相對於現有文字的大小)。原本是30dp,把部分文字設成45dp(1.5倍)
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //設定字體大小為原本的1.5倍(f代表是使用float)
    RelativeSizeSpan relativeSizeSpan = new RelativeSizeSpan(1.5f);
    //窗外的麻雀之後的文字全部變成15倍(45dp)
    ss.setSpan(relativeSizeSpan, 5, lyrics1.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • ImageSpan:在文字中插入圖片 圖片能選擇要對齊文字基線(ImageSpan.ALIGN_BASELINE)或底線(ImageSpan.ALIGN_BOTTOM),預設是對齊底線 如果不會分基線和底線,可以參考下面這張圖
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //將一張圖片載入至Span,預設對齊底線,改成對齊文字基線
    ImageSpan imageSpan = new ImageSpan(this, R.mipmap.ic_launcher,ImageSpan.ALIGN_BASELINE);


    //或是也能這樣寫
    //Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
    //drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
    //ImageSpan imageSpan1 = new ImageSpan(drawable);

    //將index3,4的"麻雀"用圖片取替,也就是說這張圖片佔了index 3,4的位置
    ss.setSpan(imageSpan, 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • ScaleXSpan:基於X軸縮放文字,在高度不變的情況下將文字水平放大
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //在高度不變的情況下將文字水平放大,2.0f表示放大為預設字體寬度的2倍,
    ScaleXSpan scaleXSpan = new ScaleXSpan(2.0f);
    //將"麻雀"這兩個字變胖2倍
    ss.setSpan(scaleXSpan, 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • StyleSpan:文字樣式(粗體、斜體)
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //正常 - "窗外"
    ss.setSpan(new StyleSpan(android.graphics.Typeface.NORMAL), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //粗體 - "的麻雀"
    ss.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //斜體 - "在電線"
    ss.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 6, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //粗斜體 - "桿上多"
    ss.setSpan(new StyleSpan(android.graphics.Typeface.BOLD_ITALIC), 9, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • SubscriptSpan:文字的下標(右下角)

  • SuperscriptSpan:文字的上標(右上角)

private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //"的麻雀"變成下標
    ss.setSpan(new SubscriptSpan(), 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //"線"變成上標
    ss.setSpan(new SuperscriptSpan(), 8, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

TypefaceSpan:文字字體

private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //預設的字體
    ss.setSpan(new TypefaceSpan("default"), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    ss.setSpan(new TypefaceSpan("default-bold"), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //將"在電線桿"設定成monospace字體
    ss.setSpan(new TypefaceSpan("monospace"), 4, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    ss.setSpan(new TypefaceSpan("serif"), 8, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    ss.setSpan(new TypefaceSpan("sans-serif"), 10, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

設定字體的前提是你的手機要手相對應的字體,否則不會有效果
我手機就是沒有任何字體QQ,所以什麼都沒有
這邊就不附上照片了,大家看看語法就好

  • BulletSpan:在句子前面加上一個小圓點 - • (unorder list) 小圓點的大小是固定的,不過可以設定它的顏色跟跟文字的距離(px)
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //第一個數值是•和文字的間距(right margin),第二個是設定•的顏色,也可以不設定,•的顏色就預設是跟文字相同顏色
    //設定unorder list的圓點顏色是藍色,然後設定unorder list的圓點右邊要有20px的margin
    BulletSpan bulletSpan = new BulletSpan(20,Color.BLUE);
    //這邊設定的start(0)和end(1),start必須要是0才會出現圓點,end基本上就沒有什麼用,只要有給值就好
    ss.setSpan(b/ulletSpan,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • QuoteSpan:引用段落文字(文字段落左邊出現一條線)
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //設定引用線條的顏色
    QuoteSpan quoteSpan = new QuoteSpan(Color.RED);
    //這邊設定的start(0)和end(6),start必須要是0才會出現引用線條,end基本上就沒有什麼用,只要有給值就好
    ss.setSpan(quoteSpan,0,6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • AlignmentSpan:文字對齊方式
    對齊方式有一般(Layout.Alignment.ALIGN_NORMAL)、中間(Layout.Alignment.ALIGN_CENTER)、對面(Layout.Alignment.ALIGN_OPPOSITE)
private void changeTextAppearance(){
    SpannableString ss = new SpannableString(lyrics1);
    //設定該段落文字要對齊的方式,這邊是要文字對齊「另外一邊」
    AlignmentSpan.Standard alignmentSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE);
    //這邊設定的start(0)和end(8),start必須要是0才會出現按照AlignmentSpan對齊,end基本上就沒有什麼用,只要有給值就好
    ss.setSpan(alignmentSpan,0,8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    tv.setText(ss);
}

  • TextAppearanceSpan:文字外型,一次能設定文字字體、大小、樣式(粗體、斜體)、文字顏色、背景顏色

由於比較複雜,請參考這裡

  • URLSpan:文字超連結,超連結的種類跟內容主要包含以下這些
private void changeTextAppearance(){
    String intro = "要連繫我的話,可以用電話、Email、簡訊、多媒體簡訊找到我,或是你想看看我的Github,或是可以開地圖看看我在哪裡"
    SpannableString ss = new SpannableString(intro );

    //電話
    ss.setSpan(new URLSpan("tel:123456789"), 10, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //Email
    ss.setSpan(new URLSpan("mailto:[email protected]"), 13, 18, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //網頁-我的Github
    ss.setSpan(new URLSpan("https://github.com/NorthBei"), 39 , 45 , Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //簡訊 - 使用sms:或者smsto:
    ss.setSpan(new URLSpan("sms:10086"), 19, 21, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //多媒體簡訊 - 使用mms:或者mmsto:      
    ss.setSpan(new URLSpan("mms:10086"), 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    //開啟預設地圖
    ss.setSpan(new URLSpan("geo:24.787775, 120.997917"), 51, 53, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

    //在點擊之後如果要執行動作,就必須設定MovementMethod
    tv.setMovementMethod(LinkMovementMethod.getInstance());
    //設定hover時的顏色
    tv.setHighlightColor(getResources().getColor(R.color.colorPrimary));
    tv.setText(ss);
}

長的會是這樣,點擊之後會出現選單讓你選擇要使用應用程式或是跳去預設開啟的應用

  • ClickableSpan:讓某幾個文字變成可以點擊的文字,其他部分的文字則不能點擊
private void changeTextAppearance(){
    SpannableString ss = new SpannableString("我是一串文字,點我點我");

    ClickableSpan clickableSpan = new ClickableSpan() {
        @Override
        public void onClick(View view) {
            Toast.makeText(MainActivity.this, "我被點了", Toast.LENGTH_SHORT).show();
        }
    };
    //"點我點我"變成可以點擊
    ss.setSpan(clickableSpan, 7, 11, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);

    //在點擊之後如果要跳去執行動作,就必須設定MovementMethod
    tv.setMovementMethod(LinkMovementMethod.getInstance());
    //設定hover時的顏色
    tv.setHighlightColor(getResources().getColor(R.color.colorPrimary));
    tv.setText(ss);
}

點擊之後會出現hover的顏色,並且觸發點擊的method

結論

SpannableString的使用方式非常多,呈現的效果也滿不錯的
不過由於效能考亮,比較不適合使用在一直需要更新的TextView上

Reference

49.xxAndroid中各种Span的用法
SpannableString实现富文本样式
【Android】強大的SpannableStringBuilder
原来我之前并不懂:Android 之 Spanned flag
Android Span架构介绍

results matching ""

    No results matching ""