900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > 实现TextView尾部追加可点击的Icon和文本

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

时间:2022-02-04 12:15:13

相关推荐

实现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和文本。

具体看看下面实现代码。

三、实现代码

布局:

<TextViewandroid: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占位,后面会把这两个点替换为iconval showText = "$text .."textView.text = showTextval layout = textView.layoutif (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 ?: returnif (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 = 1val subEndIndex = lineEnd + ellipsisStart //未省略的字的最后一个的索引值if (subEndIndex < showText.length) {span.clear()if (count <= 3) {//省略的刚好是拼接的空格和两个点,说明text刚刚好显示完,再加一个字符就会导致省略span.append(showText.substring(0, subEndIndex - 2))} else {//源文本就很长,被省略了//截取显示的文本。-2少截取两个,以便有位置拼接省略号和iconspan.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 ?: returncount = 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 = textval layout = textView.layoutif (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 ?: returnif (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.lengthif (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.maxLinestextView.maxLines = Int.MAX_VALUEtextView.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 ?: returncount = 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.lengthif (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 = 0fvar downY = 0foverride fun onTouch(v: View?, event: MotionEvent?): Boolean {val spanned = textView.text as? Spanned ?: return falsewhen (event?.action) {MotionEvent.ACTION_DOWN -> {downX = event.xdownY = event.yreturn 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.onTouchEventx -= textView.totalPaddingLefty -= textView.totalPaddingTopx += textView.scrollXy += textView.scrollYval layout: Layout = textView.layoutval 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)//效果1TextEllipsizeSpanUtil.setTextEndImageSpan(textView, text, drawable)//效果2//TextEllipsizeSpanUtil.setTextEndTextSpan(textView, text, "全文")

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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。