1. 程式人生 > >TextView+SpannableString實現Android中富文字的顯示及點選衝突解決

TextView+SpannableString實現Android中富文字的顯示及點選衝突解決

前言

最近專案中需要實現一個文章跟讀效果的顯示,還要能夠點選文章中的單詞能夠彈出對話方塊顯示單詞的英美髮音,那麼如何實現這樣的需求呢?當然是利用SpannableString啦,下面就結合專案中使用到的和參考其他部落格的成果,整理一下常用的用法吧。
SpannableString其實很多方法和屬性與String類似,只不過它比普通的字串多了能夠帶有一些富文字屬性,比如顯示不同顏色、帶下劃線刪除線等等。有心的讀者一定想到了那是否有和StringBuilder對應的呢?答案是肯定的,那就是SpannableStringBuilder。顯示富文字最重要的一個方法就是
setSpan(Object what, int start, int end, int flags)方法需要使用者輸入四個引數,what表示設定的格式是什麼,可以是前景色、背景色也可以是可點選的文字等等,顯示和互動的效果就由此決定;start表示需要設定格式的子字串的起始下標(包括),同理end表示終了下標(不包括),flags屬性共四種:

Spanned.SPAN_INCLUSIVE_EXCLUSIVE 前面應用,後面不應用(即在文字前插入新的文字會應用該樣式,而在文字後插入新文字不會應用該樣式)
Spanned.SPAN_INCLUSIVE_INCLUSIVE 前面應用,後面也應用
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 前後都不應用
Spanned.SPAN_EXCLUSIVE_INCLUSIVE 前面不應用,後面應用

常見用法

1.ForegroundColorSpan,為文字設定前景色,效果和setTextColor()類似

 SpannableString spannableString = new SpannableString("前景色為藍色"
); spannableString.setSpan(new ForegroundColorSpan(Color.parseColor("#0000FF")), 0 , spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); textView.setText(spannableString);

2.BackgroundColorSpan,為文字設定背景色,效果和TextView的setBackground()類似

new BackgroundColorSpan(Color.parseColor("#0000FF"))

3.StrikethroughSpan,為文字設定中劃線,也就是常說的刪除線

new StrikethroughSpan()

4.UnderlineSpan,為文字設定下劃線,但線的大小和顏色能否設定呢?我沒找到具體的方法,還請知道的小夥伴指點一二。

new UnderlineSpan()   

5.SuperscriptSpan,設定上標,即類似數學公式中或標註的上標效果

new SuperscriptSpan()

但這樣直接設會使得上標的文字大小和正常文字大小一樣,如果要使上標文字稍微小一點怎麼辦呢?這就需要下面即將講到的 RelativeSizeSpan實現相對字型大小。

6.SubscriptSpan,設定下標,功能與設定上標類似

 new SubscriptSpan()

7.StyleSpan, 設定文字的顯示風格(粗體、斜體),和TextView屬性textStyle類似

new StyleSpan(Typeface.ITALIC) //設定斜體
new StyleSpan(Typeface.BOLD) //設定粗體   

8.RelativeSizeSpan,設定文字相對大小,在TextView原有的文字大小的基礎上,相對設定文字大小

new RelativeSizeSpan(1.2f) //相對正常大小的1.2倍

9.ImageSpan,設定文字圖片。沒錯,我們可以利用這個屬性在一段文字中的任何位置插入圖片,是不是想想都激動哈哈

SpannableString spannableString = new SpannableString("在文字中新增表情(表情)");
        Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
        ImageSpan imageSpan = new ImageSpan(drawable);
        spannableString.setSpan(imageSpan, 6, 8, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        tv_text4.setText(spannableString);

10.ClickableSpan,設定可點選的文字,設定這個屬性的文字可以相應使用者點選事件,至於點選事件使用者可以自定義。這樣就可以實現一段文字中,設定某些特定的文字能夠點選,就像一開始說到的點選彈出對話方塊那樣的效果了。

SpannableString spannableString = new SpannableString("為文字設定點選事件");
MyClickableSpan clickableSpan = new MyClickableSpan("點選了");
spannableString.setSpan(clickableSpan, 5, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setMovementMethod(LinkMovementMethod.getInstance()); //點選事件才能起效
textView.setHighlightColor(Color.parseColor("#36969696"));  //點選背景色,預設淡藍色
textView.setText(spannableString);
class MyClickableSpan extends ClickableSpan {

    private String content;

    public MyClickableSpan(String content) {
        this.content = content;
    }

    @Override
    public void updateDrawState(TextPaint ds) { //設定顯示樣式
        ds.setUnderlineText(false);  //不要預設下劃線
    }

    @Override
    public void onClick(View widget) { //點選事件的響應方法
        Toast.makeText(getApplicationContext(),"點選事件"+content,   Toast.LENGTH_SHORT).show();
    }
}

11.URLSpan,設定超連結文字,能夠調起系統自帶的瀏覽器、郵件、電話等。其實ClickableSpan的也能實現超連結文字的效果,重寫onClick點選事件就行,URLSpan內部就是這樣實現的。

SpannableString ss5 = new SpannableString("超連結to百度");
        ss5.setSpan( new URLSpan("http://www.baidu.com"), 0 ,ss5.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv_text5.setMovementMethod(LinkMovementMethod.getInstance());
        tv_text5.setHighlightColor(Color.parseColor("#36969696"));
        tv_text5.setText(ss5);

clickableSpan和TextView的點選事件衝突

1.簡化需求
我們需要實現這樣的功能:一個TextView中,一部分文字設定了富文字顯示,一部分還是普通的文字,點選設定了富文字的文字彈出相應對話方塊,二點選普通文字彈出另外相應的對話方塊。
2.問題
當點選富文字文字時,普通文字的點選事件也得到了相應。如下例子中,點選”富文字文字”時,先後彈出“點選富文字文字”和“點選普通文字”

SpannableString ss2 = new SpannableString("普通文字,富文字文字");
        ss2.setSpan(new MaskFilterSpan(new MaskFilter()), 0 ,2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        ss2.setSpan(new RelativeSizeSpan(2.0f), 5,
                10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        ss2.setSpan(new ClickableSpan() {

            @Override
            public void updateDrawState(TextPaint ds) {
                //super.updateDrawState(ds);
                ds.setUnderlineText(false);
            }

            @Override
            public void onClick(View view) {

                Toast.makeText(getApplicationContext(),"點選富文字文字", Toast.LENGTH_SHORT).show();
            }
        }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv_text2.setMovementMethod(MyTextView.CustomLinkMovementMethod.getInstance());
        tv_text2.setHighlightColor(Color.TRANSPARENT); //背景色透明
        tv_text2.setText(ss2);

tv_text2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getApplicationContext(),"點選普通文字", Toast.LENGTH_SHORT).show();

            }
        });

但我們實際上是要點選富文字文字時,不要彈出“點選普通文字”的響應事件的。那麼這個問題如何解決呢?馬上想到的當然是判斷點選了富文字文字時攔截掉點選事件啦。這就需要我們自定義一個MyTextView繼承TextView重寫performClick()、onTouchEvent(MotionEvent event)還有在LinkMovementMethod的自定義子類中重寫onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event)
如下:

public class MyTextView extends TextView {
    public MyTextView(Context context) {
        super(context);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public boolean linkHit;//內部富文字是否被點選

    @Override
    public boolean performClick() {   //最後響應3
        if (linkHit) {
            return true;    //這樣textView的OnClick事件不會響應
        }
        return super.performClick();  //呼叫監聽的onClick方法
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {   //textView的OnClick事件響應,首先響應1
        linkHit = false;
        return super.onTouchEvent(event);
    }

    public static class CustomLinkMovementMethod extends LinkMovementMethod { //次之2

        static CustomLinkMovementMethod sInstance;

        @Override
        public boolean onTouchEvent(TextView widget, Spannable buffer,
                                    MotionEvent event) {
            int action = event.getAction();

            if (action == MotionEvent.ACTION_UP ||
                    action == MotionEvent.ACTION_DOWN) {
                int x = (int) event.getX();
                int y = (int) event.getY();

                x -= widget.getTotalPaddingLeft();
                y -= widget.getTotalPaddingTop();

                x += widget.getScrollX();
                y += widget.getScrollY();

                Layout layout = widget.getLayout();
                int line = layout.getLineForVertical(y);  //第幾行
                int off = layout.getOffsetForHorizontal(line, x); //水平偏移,第幾個字

                ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);

                if (link.length != 0) {
                    if (action == MotionEvent.ACTION_UP) {
                        link[0].onClick(widget);  //ClickableSpan的onClick

                    } else if (action == MotionEvent.ACTION_DOWN) {
                        Selection.setSelection(buffer,
                                buffer.getSpanStart(link[0]),
                                buffer.getSpanEnd(link[0]));
                    }

                    if (widget instanceof MyTextView) {
                        ((MyTextView) widget).linkHit = true;     //點了標記的文字
                    }

                    return true;
                } else {
                    Selection.removeSelection(buffer);
                    super.onTouchEvent(widget, buffer, event);
                    return false;
                }
            }

            return Touch.onTouchEvent(widget, buffer, event);
        }

        public static CustomLinkMovementMethod getInstance() {
            if (sInstance == null) {
                sInstance = new CustomLinkMovementMethod();
            }
            return sInstance;
        }
    }

}

參考連結:https://www.jianshu.com/p/84067ad289d2