实现TextView尾部追加可点击的Icon和文本

一、概述

就到国庆节了,上一次写还是五一的时候,真的好快好快,时间按周的单位在奔跑,一周,一周,又一周…
七天,没有计划,人多,哪里都堵,还有疫情(主要还是没人一起玩😂)就窝在小出租屋里写写代码,看看书好了。
祝祖国繁荣昌盛,世界和平共处。

今天写的是 TextView 相关的效果,是之前项目中写过的,感觉还是有必要记录一下,还是花了一些时间,现在整理一下,方便以后使用。如果你看到了觉得有用那更好。

看下效果1:
在文本尾部显示一个icon。如果文本过长,后面部分ellipsize,超过TextView的最大行数,也会在尾部显示icon,并且icon可以点击:
在这里插入图片描述
效果2:
显示文本。如果文本过长,超过TextView的最大行数,后面部分ellipsize,在尾部追加 “全文”,并可以点击全文展开,再点击恢复初始:
在这里插入图片描述

二、思路

两个效果都是使用 Spannable 实现。基本逻辑:

  • 给 TextView setText。效果1是在源文本尾部增加了几个字符,是为了给icon占位(因为可能文本刚好显示完,这时再去拼接一定会导致再次省略)。效果2是直接设置源文本;
  • 等到布局完成后,获取 TextView 的 layout ,判断是否有省略的文字;
  • 有省略的文字的话,就获取未省略的文字个数,把个数减去一点点(为了能放的下后面要拼接的icon或文本),从文本中截取0到个数这段文字,然后再拼接icon或文本;
  • 没有省略的文字的话,效果1是直接设置ImageSpan,效果2是不做处理。
  • 设置 ClickableSpan,把组装好的 Spannable 设置给 TextView ;
  • 再次获取 TextView 的 layout ,判断是否有省略的文字,因为有可能上面的 “ 减去一点点 ” 减的太少了,再次导致出现省略。所以这次减多一点,再执行拼接,设置 Spannable 。

所以至少是两次 setText ,第一次是为了在第二次能通过layout拿到出省略的位置,好拼接icon和文本。

具体看看下面实现代码。

三、实现代码

布局:

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_marginStart="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginTop="28dp"
        android:textColor="@color/black"
        android:textSize="20sp"
        android:textColorHighlight="#00ffffff"
        android:maxLines="3"
        android:ellipsize="end"/>

android:textColorHighlight 属性需要设置为透明,不然点击尾部的 icon 或 “全文” 会有背景色。
android:ellipsize=“end” 和 android:maxLines = “n” 也是必须要设置的。

TextEllipsizeSpanUtil ,处理逻辑都封装在这个类中,方便外部使用。

object TextEllipsizeSpanUtil {

    /**
     * 在文本尾部显示一个icon。如果文本过长超过TextView的最大行数,后面部分ellipsis,也会在尾部显示icon
     * 并且icon可以点击
     */
    fun setTextEndImageSpan(textView: TextView, text: String, drawable: Drawable?) {
        //拼接一个空格和两个点,空格为文本和icon分割,两个点为icon占位,后面会把这两个点替换为icon
        val showText = "$text .."
        textView.text = showText
        val layout = textView.layout
        if (layout == null) {//还没有布局完成,等到布局完成再执行
            textView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    textView.viewTreeObserver.removeOnGlobalLayoutListener(this)
                    setImageSpan(textView, text, showText, drawable)
                }
            })
        } else {
            setImageSpan(textView, text, showText, drawable)
        }
    }

    /**
     * 执行设置imageSpan
     */
    private fun setImageSpan(textView: TextView, text: String, showText: String, drawable: Drawable?) {
        var layout = textView.layout ?: return
        if (drawable != null && layout.lineCount > 0) {
            setTouchListener(textView)
            val line = layout.lineCount - 1 //最后一行
            var ellipsisStart = layout.getEllipsisStart(line) //从哪里开始省略的索引值
            var count = layout.getEllipsisCount(line) //省略的文字数
            val span = SpannableStringBuilder(showText)
            //省略行之前的总字数
            val lineEnd = if (line - 1 >= 0) { layout.getLineEnd(line - 1) } else { 0 }
            var offset = 2//imageSpan从倒数第几个开始放置
            if (count > 0) { //文字过长,有省略的文字
                offset = 1
                val subEndIndex = lineEnd + ellipsisStart //未省略的字的最后一个的索引值
                if (subEndIndex < showText.length) {
                    span.clear()
                    if (count <= 3) { //省略的刚好是拼接的空格和两个点,说明text刚刚好显示完,再加一个字符就会导致省略
                        span.append(showText.substring(0, subEndIndex - 2))
                    } else { //源文本就很长,被省略了
                        //截取显示的文本。-2少截取两个,以便有位置拼接省略号和icon
                        span.append(showText.substring(0, subEndIndex - 1))
                    }
                    span.append("....") //拼接四个省略号,最后一个点会被icon替换
                }
            }
            drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
            val imageSpan = ImageSpan(drawable)
            span.setSpan(imageSpan, span.length - offset, span.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
            //设置icon可点击
            val clickableSpan = object : ClickableSpan() {
                override fun onClick(widget: View) {//点击事件
                    Toast.makeText(textView.context, "copy success", Toast.LENGTH_SHORT).show()
                }

                override fun updateDrawState(ds: TextPaint) {//不设置颜色和下划线
                }
            }
            span.setSpan(clickableSpan, span.length - 2, span.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
            textView.text = span
            //再次判断是否还有省略,有省略的话,说明imageSpan没显示全
            layout = textView.layout ?: return
            count = layout.getEllipsisCount(line) //省略的文字数
            if (count > 0) {//如果还有省略的文字,说明上面showText.substring截出来的文字太长了,要少截一点
                ellipsisStart = layout.getEllipsisStart(line) //从哪里开始省略的索引值
                val subEndIndex = lineEnd + ellipsisStart //未省略的字的最后一个的索引值
                if (subEndIndex < showText.length) {
                    span.clear()
                    span.append(showText.substring(0, subEndIndex - 3))//减去3,少截取一些,保证不再出现省略
                    span.append("....") //拼接省略号和两个点,两个点会被icon替换
                    span.setSpan(imageSpan, span.length - offset, span.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                    span.setSpan(clickableSpan, span.length - offset, span.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                    textView.text = span
                }
            }
        } else {
            textView.text = text
        }
    }


    /**
     * 显示文本。如果文本过长超过TextView的最大行数,后面部分ellipsis,在尾部追加 “全文”,并可以点击全文展开
     */
    fun setTextEndTextSpan(textView: TextView, text: String, endText: String) {
        textView.text = text
        val layout = textView.layout
        if (layout == null) {
            textView.viewTreeObserver.addOnGlobalLayoutListener(object :
                ViewTreeObserver.OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    textView.viewTreeObserver.removeOnGlobalLayoutListener(this)
                    setTextSpan(textView, text, endText)
                }
            })
        } else {
            setTextSpan(textView, text, endText)
        }
    }

    /**
     * 执行设置span
     */
    private fun setTextSpan(textView: TextView, text: String, endText: String) {
        var layout = textView.layout ?: return
        if (layout.lineCount > 0) {
            setTouchListener(textView)
            val line = layout.lineCount - 1 //最后一行
            var ellipsisStart = layout.getEllipsisStart(line) //从哪里开始省略的索引值
            var count = layout.getEllipsisCount(line) //省略的文字数
            //省略行之前的总字数
            val lineEnd = if (line - 1 >= 0) { layout.getLineEnd(line - 1) } else { 0 }
            if (count > 0) { //文字过长,有省略的文字
                var subEndIndex = lineEnd + ellipsisStart //未省略的字的最后一个的索引值
                if (subEndIndex < text.length) {
                    val span = SpannableStringBuilder()
                    subEndIndex = subEndIndex - 1 - endText.length
                    if (subEndIndex > 0) {
                        span.append(text.substring(0, subEndIndex))
                    }
                    span.append("...$endText") //拼接省略号和endText
                    //设置endText可点击
                    val clickableSpan = object : ClickableSpan() {
                        override fun onClick(widget: View) {
                            val maxLine = textView.maxLines
                            textView.maxLines = Int.MAX_VALUE
                            textView.text = text//展示全文
                            textView.setOnClickListener {
                                textView.setOnClickListener(null)
                                textView.maxLines = maxLine//恢复初始
                                setTextEndTextSpan(textView, text, endText)
                            }
                        }
                    }
                    span.setSpan(clickableSpan, span.length - endText.length, span.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                    textView.text = span
                    //再次判断是否还有省略,有省略的话,说明imageSpan没显示全
                    layout = textView.layout ?: return
                    count = layout.getEllipsisCount(line) //省略的文字数
                    if (count > 0) {//如果还有省略的文字,说明上面text.substring截的文字太长了,要少截一点,不然endText显示不全
                        ellipsisStart = layout.getEllipsisStart(line) //从哪里开始省略的索引值
                        subEndIndex = lineEnd + ellipsisStart //未省略的字的最后一个的索引值
                        if (subEndIndex < text.length) {
                            span.clear()
                            subEndIndex = subEndIndex - 5 - endText.length
                            if (subEndIndex > 0) {
                                span.append(text.substring(0, subEndIndex))
                            }
                            span.append("...$endText") //拼接省略号和两个点,两个点会被icon替换
                            span.setSpan(
                                clickableSpan, span.length - endText.length,
                                span.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
                            )
                            textView.text = span
                        }
                    }
                }
            }
        }
    }

    /**
     * 设置触摸事件,让TextView响应ClickableSpan。此处代码参考LinkMovementMethod.onTouchEvent方法
     * 直接给TextView设置LinkMovementMethod后,文本过长时可以滑动,与ellipsize冲突
     */
    @SuppressLint("ClickableViewAccessibility")
    private fun setTouchListener(textView: TextView) {
        textView.setOnTouchListener(object : View.OnTouchListener {
            var downX = 0f
            var downY = 0f
            override fun onTouch(v: View?, event: MotionEvent?): Boolean {
                val spanned = textView.text as? Spanned ?: return false
                when (event?.action) {
                    MotionEvent.ACTION_DOWN -> {
                        downX = event.x
                        downY = event.y
                        return true//返回true,接受后续的UP事件
                    }
                    MotionEvent.ACTION_UP -> {
                        var x = event.x.toInt()
                        var y = event.y.toInt()
                        //判断一下按下和抬起的位置,相差太大就不处理
                        if (abs(downX - x) > 8 || abs(downY - y) > 8) {
                            return false
                        }
                        //下面代码都是照搬LinkMovementMethod.onTouchEvent
                        x -= textView.totalPaddingLeft
                        y -= textView.totalPaddingTop

                        x += textView.scrollX
                        y += textView.scrollY

                        val layout: Layout = textView.layout
                        val line = layout.getLineForVertical(y)
                        val off = layout.getOffsetForHorizontal(line, x.toFloat())

                        val links: Array<ClickableSpan> =
                            spanned.getSpans(off, off, ClickableSpan::class.java)

                        if (links.isNotEmpty()) {
                            links[0].onClick(textView)
                            return true
                        }
                    }
                }
                return false
            }
        })
    }
}

这里没有使用 LinkMovementMethod ,而是给TextView 设置触摸监听,参考了 LinkMovementMethod 的代码处理 ClickableSpan 。如果给TextView 设置 LinkMovementMethod ,文本过长时可以滑动,与ellipsize冲突

四、使用

 val drawable = ContextCompat.getDrawable(context, R.drawable.icon_copy)
 //效果1
 TextEllipsizeSpanUtil.setTextEndImageSpan(textView, text, drawable)
 //效果2
//TextEllipsizeSpanUtil.setTextEndTextSpan(textView, text, "全文")

我自己写了文本测试,暂时还没有发现问题。如果你发现问题,可以改改里面的一些代码来解决。或许有些场景,上面的实现无法满足,这只是我想到的办法,可能有更好的方案来实现。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
安卓中的TextView控件默认是不支持富文本的,即无法直接实现文字可点击的效果。不过我们可以通过一些方法来实现这个需求。 一种常用的方法是使用SpannableString类来实现文本文字可点击的效果。SpannableString是一个可以调整文字的样式和属性的类。我们可以使用它的setSpan()方法来设置文字的点击事件。 首先,我们需要创建一个ClickableSpan对象,它是一个可以实现文字可点击的类。在ClickableSpan的onClick()方法中,我们可以编写点击文字后的逻辑代码。 ``` ClickableSpan clickableSpan = new ClickableSpan() { @Override public void onClick(View widget) { // 在这里编写点击文字后的逻辑代码 } }; ``` 然后,我们创建一个SpannableString对象,并使用setSpan()方法将ClickableSpan对象应用于需要点击的文字范围。 ``` SpannableString spannableString = new SpannableString("需要设置点击事件的文字"); spannableString.setSpan(clickableSpan, startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ``` 其中,startIndex和endIndex分别表示需要设置点击事件的文字的起始和结束位置。 接下来,我们将SpannableString对象设置给TextView控件,并为TextView控件设置setMovementMethod()方法,使其具有点击效果。 ``` textView.setText(spannableString); textView.setMovementMethod(LinkMovementMethod.getInstance()); ``` 最后,我们就可以在TextView实现文字可点击的效果了。 需要注意的是,在使用这种方法时,TextView的同时三个属性要设置为true:android:focusable="true"、android:focusableInTouchMode="true"和android:clickable="true",以确保TextView本身可以获得焦点和点击事件。 以上就是使用SpannableString实现安卓TextView文本文字可点击的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值