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

你可能感兴趣的:(Android基础)