Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android SpannableString淺析

Android SpannableString淺析

編輯:關於Android編程

引言

在應用程序開發過程經常需要對文本進行處理,比如說對一段描述文字的其中一段加入點擊事件,或者對其設置不一樣的前景色,有什麼方法可以實現要求的功能吶?

需求樣例

比如我們需要實現如下圖所示的功能,將文本:#重磅消息#近日谷歌放出Android N的第二個開發者預覽版(Developer Preview) 處理成第二種或者第三種的形式。

這裡寫圖片描述

實現方案

根據上圖,我們可以采用如下的方法來實現上訴要求的效果。

方案1

比如顯示效果二你可以能會說,我們可以采用三個TextView來實現,第一個TextView設置不一樣的顏色,第二個正常顯示內容,第三個處理點擊事件。該方式對圖二可能是能夠實現的,但是如果第二行裡面就有部分內容需要進行點擊處理,就比較難以實現了。

對於圖三的效果上述的方式就很難實現了。必須要對TextView的內容進行處理了!!

方案2

如果文案的處理只是簡單的對齊,顏色,大小的變換,我們還可以采用自定義view來實現,在前面的文章中我們就采用了自定義view來顯示了一個文字的排版效果,具體實現可以查看Android文本排版實現;

方案3

除了上面的方案,我們還可以采用另外一個種方式來實現,采用html來顯示,可以將要顯示的內容轉換成html的格式,用TextView來進行加載。說了這麼多,我們來看看代碼吧!

private void setText() {
    String originText = "#重磅消息#近日谷歌放出Android N的第二個開發者預覽版(Developer Preview)";

    String effect1 = "#重磅消息# 
 近日谷歌放出Android " +
            "N的第二個開發者預覽版(Developer Preview)";

    String effect2 = "#重磅消息# 近日谷歌放出Android " +
            "N的第二個開發者預覽版(Developer Preview)";
    StringBuilder sb = new StringBuilder(originText);
    sb.append("



");
    sb.append(effect1);
    sb.append("



");
    sb.append(effect2);
    textView.setText(Html.fromHtml(sb.toString()));
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}

寫到這,突然發現要跑題,僅僅是Html的實現就可以分析出很多的知識點,不過這裡還是先契合主題,先這裡挖一個坑,後續對html進行分析,查看鏈接,現在還未實現

方案4

終於回到我們的主題了,這裡我們采用SpannableString來實現上述的效果。代碼如下:


private void setSpan() {
    String originText = "#重磅消息#近日谷歌放出Android N的第二個開發者預覽版(Developer Preview)";

    SpannableStringBuilder sb = new SpannableStringBuilder(originText);
    sb.append("\r\n").append("\r\n").append("\r\n");
    getEffect1Span(sb);
    sb.append("\r\n").append("\r\n").append("\r\n");
    getEffect2Span(sb);
    textView.setText(sb);
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}

private void getEffect1Span(SpannableStringBuilder sb) {
    String source1 = "#重磅消息#";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(span);
    sb.append("\n");
    String source2 = "近日谷歌放出Android N的第二個開發者預覽版";
    sb.append(source2);

    final String source3 = "(Developer Preview)";
    SpannableString clickSpan = new SpannableString(source3);
    clickSpan.setSpan(new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            ToastUtil.showLong(source3);
        }
    }, 0, source3.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(clickSpan);
}

private void getEffect2Span(SpannableStringBuilder sb) {
    String source1 = "#重磅消息#近日谷歌放出Android N的第二個開發者預覽版";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorPrimaryDark)), 0, 6, Spanned
            .SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(span);

    final String source2 = "(Developer Preview)";
    SpannableString clickSpan = new SpannableString(source2);
    clickSpan.setSpan(new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            ToastUtil.showLong(source2);
        }
    }, 0, source2.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(clickSpan);
}

上述代碼采用了硬編碼方式實現,正常實現,需要根據需求進行設置。記得要添加textView.setMovementMethod(LinkMovementMethod.getInstance());來接受點擊事件。

SpnnableString詳解

SpannableString繼承了SpannableStringInternal,同時實現了CharSequence, GetChars, Spannable三個接口,正常處理文本的函數為setSpan函數:

public void setSpan(Object what, int start, int end, int flags) {
    super.setSpan(what, start, end, flags);
}

該函數有四個參數,第一個為一個span類型,第二個參數為開始位置,第三個位置為span的結束位置,最後一個為flag參數。
what可以設置如下類型:

1, AbsoluteSizeSpan 設置文字字體的絕對大小, 有兩個參數,第一個是字體大小,第二個是單位是否是dip

public AbsoluteSizeSpan(int size, boolean dip) {
        mSize = size;
        mDip = dip;
    }

2,AlignmentSpan 主要設置文本的對齊方式,有三種方式正常,居中,相反的方式對齊,默認實現為Standard

   public Standard(Layout.Alignment align) {
        mAlignment = align;
    }

3,BackgroundColorSpan 設置文字的背景色

private void setfCS(){
    String source1 = "#重磅消息#";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new BackgroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(span);
}

4,BulletSpan 給文本的開始處加上項目符號。比如前面加一個 .

private void setBSpan() {
    final String source3 = "近日谷歌放出Android N的第二個開發者預覽版";
    SpannableString bSpan = new SpannableString(source3);
    bSpan.setSpan(new BulletSpan(), 0, source3.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(bSpan);
}

5, ClickableSpan 設置文本的點擊事件,要實現onClick函數,可以復寫updateDrawState,設置下劃線,或者取消下劃線,還可以設置下劃線顏色

private void setCS(){
    final String source2 = "(Developer Preview)";
    SpannableString clickSpan = new SpannableString(source2);
    clickSpan.setSpan(new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            ToastUtil.showLong(source2);
        }
    }, 0, source2.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(clickSpan);
}

6,DrawableMarginSpan 可以設置一個圖標,並且可以設置與文字的寬度

private void setDMSpan() {
    final String source3 = "(Developer Preview)";
    SpannableString dmSpan = new SpannableString(source3);
    dmSpan.setSpan(new DrawableMarginSpan(getResources().getDrawable(R.mipmap.ic_launcher), 30), 0, source3
            .length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(dmSpan);
}

7,DynamicDrawableSpan 設置某段文字被圖標替換,需要返回一個drawable

8,EasyEditSpan 當文本改變或者刪除時調用, 例如入下長按可以很容易刪除一行

private void setEdit() {
    editText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    editText.setSingleLine(false);
    editText.setText("近日\n谷歌放出Android N的\n第二個開發者預覽版");
    editText.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            final Layout layout = editText.getLayout();
            final int line = layout.getLineForOffset(editText.getSelectionStart());
            final int start = layout.getLineStart(line);
            final int end = layout.getLineEnd(line);
            editText.getEditableText().setSpan(new EasyEditSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            return true;
        }
    });
}

9,ForegroundColorSpan 設置文字前景色

private void setfCS(){
    String source1 = "#重磅消息#";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(span);
}

寫到這裡我停下來了。天啦噜,30多個span,可以去系統代碼package android.text.style包下查看,這麼多,整個人都不好了。

因此先就針對上面的做了部分樣例,之後會專門實現一下每個span的效果。仔細理解一個就行,其他的都是類似的,我們繼續看看後面的參數。

第二參數start和第三個參數end,表示當時設置的span作用效果的范圍,start表示開始位置,end表示結束位置,第四個參數是一個flag標簽。這裡主要設置以下的值:

/**
 * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
 * to include text inserted at their starting point but not at their
 * ending point.  When 0-length, they behave like marks.
 */
public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;

/**
 * Spans of type SPAN_INCLUSIVE_INCLUSIVE expand
 * to include text inserted at either their starting or ending point.
 */
public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;

/**
 * Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand
 * to include text inserted at either their starting or ending point.
 * They can never have a length of 0 and are automatically removed
 * from the buffer if all the text they cover is removed.
 */
public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;

/**
 * Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand
 * to include text inserted at their ending point but not at their
 * starting point.  When 0-length, they behave like points.
 */
public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;

常用的就是上述的四個值,這裡我們來分別解釋以下:
1. SPAN_INCLUSIVE_EXCLUSIVE表示左閉右開區間 “[ )”
2. SPAN_INCLUSIVE_INCLUSIVE表示左右都是閉區間 ‘( )’
3. SPAN_EXCLUSIVE_EXCLUSIVE表示左右都是閉區間 ‘[ ]’
4. SPAN_EXCLUSIVE_INCLUSIVE表示左右都是閉區間 ‘( ]’

我們繼續來看代碼,SpannableString的setSpan又繼續調用了SpannableStringInternal的setSpan函數。

/* package */ void setSpan(Object what, int start, int end, int flags) {
    int nstart = start;
    int nend = end;

    checkRange("setSpan", start, end);

    if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
        if (start != 0 && start != length()) {
            char c = charAt(start - 1);

            if (c != '\n')
                throw new RuntimeException(
                        "PARAGRAPH span must start at paragraph boundary" +
                        " (" + start + " follows " + c + ")");
        }

        if (end != 0 && end != length()) {
            char c = charAt(end - 1);

            if (c != '\n')
                throw new RuntimeException(
                        "PARAGRAPH span must end at paragraph boundary" +
                        " (" + end + " follows " + c + ")");
        }
    }

    int count = mSpanCount;
    Object[] spans = mSpans;
    int[] data = mSpanData;

    for (int i = 0; i < count; i++) {
        if (spans[i] == what) {
            int ostart = data[i * COLUMNS + START];
            int oend = data[i * COLUMNS + END];

            data[i * COLUMNS + START] = start;
            data[i * COLUMNS + END] = end;
            data[i * COLUMNS + FLAGS] = flags;

            sendSpanChanged(what, ostart, oend, nstart, nend);
            return;
        }
    }

    if (mSpanCount + 1 >= mSpans.length) {
        Object[] newtags = ArrayUtils.newUnpaddedObjectArray(
                GrowingArrayUtils.growSize(mSpanCount));
        int[] newdata = new int[newtags.length * 3];

        System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
        System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);

        mSpans = newtags;
        mSpanData = newdata;
    }

    mSpans[mSpanCount] = what;
    mSpanData[mSpanCount * COLUMNS + START] = start;
    mSpanData[mSpanCount * COLUMNS + END] = end;
    mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
    mSpanCount++;

    if (this instanceof Spannable)
        sendSpanAdded(what, nstart, nend);
}

/* package */ void removeSpan(Object what) {
    int count = mSpanCount;
    Object[] spans = mSpans;
    int[] data = mSpanData;

    for (int i = count - 1; i >= 0; i--) {
        if (spans[i] == what) {
            int ostart = data[i * COLUMNS + START];
            int oend = data[i * COLUMNS + END];

            int c = count - (i + 1);

            System.arraycopy(spans, i + 1, spans, i, c);
            System.arraycopy(data, (i + 1) * COLUMNS,
                             data, i * COLUMNS, c * COLUMNS);

            mSpanCount--;

            sendSpanRemoved(what, ostart, oend);
            return;
        }
    }
}

首先調用了checkRange,判斷了位置的合法性,如果start小於end,或者位置下標越界都會拋出IndexOutOfBoundsException異常。

之後判斷了(flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH是否相等,這裡如果設置的是上述四個值,這裡是不等的,所以不會進入該判斷。

設置了count,第一次count為0,設置了spans數組與data,第一次設置的值是在構造函數中初始化的值。

因為count為0,因此for循環也不會進入

之後判斷了mSpanCount + 1 >= mSpans.length,這裡前面為1,後面為0,因此會進入if判斷,首先申請了一個3個長度的newtags數組,一個9個長度的int數組, 之後進行了兩次數據拷貝,將已有的span拷貝到新申請的數組中,將其他參數拷貝到新的int數組中。

之後將改成設置的span設置到mSpans數組中,將其他的參數設置到mSpanData,三個參數是連續設置的。

最後調用了sendSpanAdded,代碼如下:

private void sendSpanAdded(Object what, int start, int end) {
    SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
    int n = recip.length;

    for (int i = 0; i < n; i++) {
        recip[i].onSpanAdded((Spannable) this, what, start, end);
    }
}

這個調用了getSpans,返回了一個SpanWatcher數組,SpanWatcher是一個接口,MultiTapKeyListener, TextKeyListener實現了該類,因此當調用了TextKeyListener或者MultiTapKeyListener會對相應的span進行處理。

總結

這裡只是大致的解析了SpannableString,他還需要結合TextView進行分析,看看在界面繪制的時候是怎樣解析顯示的。後續有時間會陸續進行解析的。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved