TextView 和ClickSpan事件冲突的解决,和实现背景带圆角换行的span效果

前言

按照惯例先上效果图

在项目开发中,需求是实现漂亮复杂的textview span效果,如图上图。看到效果图的时候,觉得简单一个BackgroundColorSpan就可以解决,然而并不然,仔细观察发现,背景带有圆角,当整块不能在一行显示的时候,需要换行,背景和字体之间是带有一定边距的。

紧张之下打开了,BackgroundColorSpan源码,发现里面并没像ReplacementSpan一样,提供onDraw的方法。看了其他的span发现,解决方案也只能继承ReplacementSpan。重写绘制了。对应的文本内容了。

一、因为不是特别复杂,所以直接贴上RoundBackgroundSpan源码,具体的参数说明,注释已经写的很清楚了


/**
 * 圆角背景span
 */
public class RoundBackgroundSpan extends ReplacementSpan {
    private int mSize;
    private int mBgColor;
    private int mDrawTextColor;
    private int mRadius;
    private int mSpace;
    private int mDrawTextSize;

    /**
     *
     * @param bgcolor 绘制背景颜色
     * @param drawTextColor 绘制字体颜色
     * @param drawTextSize 绘制字体大小
     * @param radius 绘制圆角背景半径
     * @param space 间距
     */
    public RoundBackgroundSpan(int bgcolor, int drawTextColor, int drawTextSize, int radius, int space) {
        mBgColor = bgcolor;
        mDrawTextColor = drawTextColor;
        mDrawTextSize = drawTextSize;
        mRadius = radius;
        mSpace = space;
    }

    /**
     * mSize就是span的宽度,span有多宽,可以在这里随便定义规则
     * 我的规则:这里text传入的是SpannableString,start,end对应setSpan方法相关参数
     * 可以根据传入起始截至位置获得截取文字的宽度,最后加上左右间距
     * @param paint
     * @param text
     * @param start 设置的对应的span起始位置
     * @param end  设置的对应span终止位置
     * @param fm
     * @return
     */
    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        float textSize = paint.getTextSize();
        paint.setTextSize(SizeUtils.dp2px(12));
        mSize = (int) (paint.measureText(text, start, end) + 2 * mSpace);
        paint.setTextSize(textSize);
        return mSize;

    }


    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        //缓存原有textView的一些属性
        int color = paint.getColor();
        float textSize = paint.getTextSize();
        paint.setAntiAlias(true);
        paint.setDither(true);

        //背景颜色,绘制圆角背景
        paint.setColor(mBgColor);
        //设置文字背景矩形,x为span其实左上角相对整个TextView的x值,y为span左上角相对整个View的y值。paint.ascent()获得文字上边缘,paint.descent()获得文字下边缘
        RectF oval = new RectF(x, y + paint.ascent(), x + mSize, y + paint.descent());
        canvas.drawRoundRect(oval, mRadius, mRadius, paint);
        float bgHeight = paint.descent()-paint.ascent();

        //设置文字大小,文字颜色
        paint.setTextSize(mDrawTextSize);
        paint.setColor(mDrawTextColor);
        float textHeight = paint.descent()-paint.ascent();
        float diff = (bgHeight -textHeight)/4;
        canvas.drawText(text, start, end, x + mSpace, y-diff, paint);

        //恢复textView原有的默属性
        paint.setColor(color);
        paint.setTextSize(textSize);
    }

实现代码有了,那要怎么使用呢?

 /**
     * 设置textView 带圆角背景span 主要类{@link RoundBackgroundSpan}
     * 这边为了演示方便,把需要span数据直接拼接在了文本内容前面
     * @param spanText 需要匹配背景span文字
     * @param context 内容文本
     * @param textColor 字体颜色
     * @param bgColor 背景颜色
     * @return
     */
    public static Spannable getTextSpanBgColor(List<String> spanText,String context, int textColor, int bgColor) {
        SpannableStringBuilder spannable = new SpannableStringBuilder();

        //先拼接文本内容
        if (spanText != null && spanText.size() > 0) {
            for (int i = 0; i < spanText.size(); i++) {
                String text = spanText.get(i);
                spannable.append(text);
                //这边是为了让两个带颜色的span分割开,也可以用replaseSpan去加,这边偷个懒
                spannable.append("  ");
            }
        }
        spannable.append(context);
        //在设置对应span
        
        //需要设置span文本起始位置
        int mathStart =0;
        for (int i = 0; i < spanText.size(); i++) {
            String item = spanText.get(i);
            int mathEnd = item.length() +mathStart;
            RoundBackgroundSpan backgroundColorSpan = new RoundBackgroundSpan(bgColor,textColor,dp2px(12),dp2px(2),dp2px(7));
            spannable.setSpan(backgroundColorSpan,mathStart,mathEnd, Spanned.SPAN_MARK_POINT);
            //需要设置span终止位置
            mathStart=mathEnd+"  ".length();
        }
        return spannable;
    }

效果图的的两个span间隔有空白间距,这个我在拼接文本内容的时候多加了一个空格(偷了个懒,也减少了设置了对应ReplaceSpan)。因为正常情况下,ReplaceSpan是直接替换对应位置上的内容。需要加的地方也是要有填补空格占位。要不然文本内容会直接被替换

***这边有个坑***

就是在使用ReplacementSpan的时候,如果文本内容是使用先设置了ReplacementSpan在去拼接对应文本,在有上面效果图那种存在多个且有换行的时候会导致Clickspan的点击事件位置出现偏差。

先设置ReplacmentSpan在设置文本内容是什么意思呢?

首先像上面的那种效果图的形式,业务场景是:需要设置span的内容后台是单独下发的,后面的文本内容也是单独下发的。需要设置span的内容是拼接在正常文本内容的前面。所以我在初次尝试的时候,我是先把前面需要展示的内容先设置对应ReplacementSpan,然后在去拼上后面的文本,就会出现点击事件错位。这边也给大家提个醒。避免重复踩坑

二、由上面效果,引发出了TextView点击事件和ClickableSpan冲突血案问题。

1.当TextView设置了ClickableSpan点击事件,会拦截TextView对应父容器的点击事件

2.如果TextView也设置了点击事件,点击ClickableSpan会响应对应TextView的 点击事件。

分析原因:

我们知道TextViwe是属于view,根据view的事件分发机制了触发点击事件的时机回顾一下View的事件分发机制

View的事件分发

     1.dispatchTouchEvent();分发

     2.onTouchListener-->onTouch方法  如果有设置的话

     3.onTouchEvent()

     4.onClickListener-->onClick方法

结论:

    1.如果onTouchListener的onTouch方法返回了true,那么view里面的onTouchEvent就不会被调用了。

          顺序dispatchTouchEvent-->onTouchListener---return false-->onTouchEvent

    2.如果view为disenable,则:onTouchListener里面不会执行,但是会执行onTouchEvent(event)方法

    3.onTouchEvent方法中的ACTION_UP分支中触发onclick事件监听

          onTouchListener-->onTouch方法返回true,消耗次事件。down,但是up事件是无法到达onClickListener.

          onTouchListener-->onTouch方法返回false,不会消耗此事件

由上可以知道TextView的onClick事件肯定是在其的onTouchEvent中调用,跟进源码:

发现在TextView中的onTouchEvent中有这段代码

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        

            ....省略部分代码.....



        if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
                && mText instanceof Spannable && mLayout != null) {
            boolean handled = false;

            if (mMovement != null) {
                handled |= mMovement.onTouchEvent(this, mSpannable, event);
            }

            final boolean textIsSelectable = isTextSelectable();
            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
                // The LinkMovementMethod which should handle taps on links has not been installed
                // on non editable text that support text selection.
                // We reproduce its behavior here to open links for these.
                ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(),
                    getSelectionEnd(), ClickableSpan.class);

                if (links.length > 0) {
                    links[0].onClick(this);
                    handled = true;
                }
            }


            ....省略部分代码.....

        return superResult;
    }

 

上面代码中可以看出,当mMovement!=null的时候会调用其的mMovement.onTouchEvnet

MovementMethod 是一个接口,子类有 ArrowKeyMovementMethod, LinkMovementMethod, ScrollingMovementMethod 。

我们已LinkMovementMethod为例子。

    @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[] links = buffer.getSpans(off, off, ClickableSpan.class);

            if (links.length != 0) {
                ClickableSpan link = links[0];
                if (action == MotionEvent.ACTION_UP) {
                    if (link instanceof TextLinkSpan) {
                        ((TextLinkSpan) link).onClick(
                                widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
                    } else {
                        link.onClick(widget);
                    }
                } else if (action == MotionEvent.ACTION_DOWN) {
                    if (widget.getContext().getApplicationInfo().targetSdkVersion
                            >= Build.VERSION_CODES.P) {
                        // Selection change will reposition the toolbar. Hide it for a few ms for a
                        // smoother transition.
                        widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
                    }
                    Selection.setSelection(buffer,
                            buffer.getSpanStart(link),
                            buffer.getSpanEnd(link));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }

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

在他的OnToucheEvent中可以发现,当点击位置区域有设置span即,link.length!=0了会在ACTION_UP的时候调用 ClickableSpan 的 onClick。知道了原因。就可以找到想到了对应的解决思路。

三、解决办法

首先在解决之前,参考了这stackoverflow上的一个解决思路。原文在这边原文链接

在这个文章里面讲到一个点:因为项目中只是TextView可以部分点击,实际上,不需要大多数的功能,所以采用了自定义MovementMethod.

public class ClickableMovementMethod extends BaseMovementMethod {

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

    //action_down 是的时间
    //因为自己项目中用到了长按复杂的功能,不设置加以判断,就不会触发TextView的setOnLongClickListener
    private long downTime = 0;

    @Override
    public boolean canSelectArbitrarily() {
        return false;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            if (action == MotionEvent.ACTION_DOWN) {
                downTime = System.currentTimeMillis();
            }
            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) {
                    long upTime = System.currentTimeMillis();
//按下的时间和抬起的时间差小于500ms就响应对应点击事件,大于的发返回false,不处理
                    if (upTime - downTime < 500) {
                        link[0].onClick(widget);
                    } else {
                        return false;
                    }
                }
                //设置了这个会导致,点击文字时会出现偏移,里面源码会去更新对应buffer的span
//                else {
//                    Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));
//                }
                return true;
            } else {
                //添加富文本点击事件 和 对应的parentView 点击事件时,富文本的点击事件会拦截 TextView的父容器(ParentView)的点击事件;所以要手动调用
                //因为项目中文本有长按复制,也会和ClickSpan冲突,所以手动设置
                if (action == MotionEvent.ACTION_UP) {
                    ViewParent parent = widget.getParent();
                    long upTime = System.currentTimeMillis();
                    if (upTime - downTime < 500) {
                        if (parent instanceof ViewGroup) {
                            ((ViewGroup) parent).performClick();
                        }
                    }
                }
                Selection.removeSelection(buffer);
            }
        }

        return false;
    }

    @Override
    public void initialize(TextView widget, Spannable text) {
        Selection.removeSelection(text);
    }
}

然后使用它ClickableMovementMethod,移动方法将不再消耗触摸事件。但是,TextView.setMovementMethod()调用TextView.fixFocusableAndClickableSettings()将可点击,可长按和可聚焦设置为true,这使得View.onTouchEvent()消耗触摸事件。要解决此问题,只需重置三个属性即可。

textView.setMovementMethod(ClickableMovementMethod.getInstance());
textView.setClickable(false);
textView.setLongClickable(false);

   

但是对应上面的设置了TextView富文本点击事件,会影响到其父View的点击事件,这个具体原因还有待楼主去研究和验证。所以这边就不阐述原因只提高一个解决方案,如果你们知道,欢迎留言告知,不甚感激。。。。 

使用方法:

布局文件代码:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/llRoot"
    android:padding="10dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:textSize="14sp"
        android:lineSpacingExtra="4dp"
        android:text="文本内容"
        android:textColor="#000000"
        android:id="@+id/tvSpanText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

能达到的效果:

1.点击LinearLayout能响应其的点击事件,

2.点击span的点击事件不会触发TextView的点击事件(因为如果文本中设置了ClickSpan,就不设置TextView的点击事件,而是把点击事件交给其父级LinearLayou响应来提高整体ItemView的点击事件范围,从而避免了点击了因ClickableSpan.onClick而触发TextView自身的点击事件,而点击了非span文本内容,把点击事件上移到父级响应)

3.TextView的setOnLongClickListener能够正常响应,且不触发其他点击事件

具体效果请参考效果图

具体的代码实现请参考:附上源码地址

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值