文本折叠展开效果

前言:当文本过长时(超过指定行数),打点省略显示并在其后添加“展开”,点击则展开显示全部,再次点击收起,效果如下

分析:(1)超过n行折叠  (2)“【展开】”内嵌至文本中且通过颜色标识(3)点击展开收缩

方案一:
计算n行的总长度,获取“【更多】”所占长度,两者差值即为能容纳的字符串长度,截取字符串之后再拼接上“【更多】”即可

object TextUtil{
    /**
     * textView:文本控件
     * textStr:文本内容
     * lineWidth:行宽度
     * minLine:最小行数
     * endStr:尾部追加字符
     * endColor:尾部字符颜色
     */
    fun ellipsize(textView: TextView, textStr: String, lineWidth: Float, minLine: Int, endStr: String, endColor: Int){
        //最小行数可容文本长度
        val availableTextWidth = lineWidth * minLine
        val newStr = TextUtils.ellipsize(textStr, textView.paint, availableTextWidth, TextUtils.TruncateAt.END)
        if (newStr.length < textStr.length){
            //文本超过限制行数
            val endTextWidth = textView.paint.measureText(endStr)
            val resultStr = TextUtils.ellipsize(textStr, textView.paint, availableTextWidth - endTextWidth, TextUtils.TruncateAt.END).toString() + endStr
            val ssb = SpannableStringBuilder(resultStr)
            ssb.setSpan(ForegroundColorSpan(endColor), ssb.length - endStr.length, ssb.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
            textView.text = ssb
            textView.tag = ssb
        }else{
            textView.text = textStr
        }
    }
}
class HomeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)
        val textStr = "我志愿加入中国共产党,拥护党的纲领,遵守党的章程,履行党员义务,执行党的决定,严守党的纪律,保守党的秘密,对党忠诚,积极工作,为共产主义奋斗终身,随时准备为党和人民牺牲一切,永不叛党。"
        val lineWidth = dpTopx(220f)
        TextUtil.ellipsize(tv, textStr, lineWidth, 2, "【展开】", resources.getColor(R.color.colorAccent))
        tv.setOnClickListener {
            if (it.tag != null){//文本需要折叠展开效果
                if (tv.text.toString() == textStr){
                    tv.text = tv.tag as SpannableStringBuilder
                }else{
                    tv.text = textStr
                }
            }
        }
    }

    fun dpTopx(dp: Float): Float{
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
    }
}

主要api:

(1)获取指定长度能容纳的字符串,超长则截取并将尾部字符替换为省略符

TextUtils.ellipsize(textStr, textView.paint, availableTextWidth, TextUtils.TruncateAt.END)

(2)测量字符串所占宽度
textView.paint.measureText(endStr)

总结:方案看似很完美,然而却翻车,首先不支持换行符,另外还有换行的一些规则(如换行后首字符是标点则会将上一行的尾部字符拉下来显示,如下图),该方案只是提供了一个思路,如果想解决该问题,可以重新TextView,再onDraw中按照我们测量的规则进行绘制即可

 方案二:

布局完整字符串,总行数超过指定行则获取指定行末尾字符位置,通过该位置获取到新字符串(追加上省略符和结束语),然后再布局新字符串循环处理直到行数不超过指定行为止

object TextUtil {
    /**
     * textView:文本控件
     * textStr:文本内容
     * lineWidth:行宽度
     * minLine:最小行数
     * endStr:尾部追加字符
     * endColor:尾部字符颜色
     */
    fun ellipsize(textView: TextView, textStr: String, lineWidth: Float, minLine: Int, endStr: String, endColor: Int) {
        val layout = createLayout(textView, textStr, lineWidth.toInt())
        if (layout.lineCount > minLine) {
            //获取指定行最后位置
            var endPosition = layout.getLineEnd(minLine - 1)
            var showStr = "${textStr.substring(0, endPosition)}...$endStr"
            var newLayout = createLayout(textView, showStr, lineWidth.toInt())
            while (newLayout.lineCount > minLine){
                endPosition--
                showStr = "${textStr.substring(0, endPosition)}...$endStr"
                newLayout = createLayout(textView, showStr, lineWidth.toInt())
            }
            val ssb = SpannableStringBuilder(showStr)
            ssb.setSpan(ForegroundColorSpan(endColor), ssb.length - endStr.length, ssb.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
            textView.text = ssb
            textView.tag = ssb
        } else {
            textView.text = textStr
        }
    }

    private fun createLayout(textView: TextView, textStr: String, lineWidth: Int): StaticLayout {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            StaticLayout(textStr, textView.paint, lineWidth, Layout.Alignment.ALIGN_NORMAL,
                    textView.lineSpacingMultiplier, textView.lineSpacingExtra, textView.includeFontPadding)
        } else {
            val builder = StaticLayout.Builder.obtain(textStr, 0, textStr.length, textView.paint, lineWidth)
            builder.setAlignment(Layout.Alignment.ALIGN_NORMAL)
                    .setLineSpacing(textView.lineSpacingExtra, textView.lineSpacingMultiplier)
                    .setIncludePad(textView.includeFontPadding)
                    .setBreakStrategy(textView.breakStrategy)
                    .setHyphenationFrequency(textView.hyphenationFrequency)
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
                builder.setJustificationMode(textView.justificationMode)
            }
            builder.build()
        }
    }
}

 主要api:

(1)获取指定行最后位置

layout.getLineEnd(minLine - 1)

(2)获取总行数

layout.lineCount

总结:由于TextView的测量规则也是通过StaticLayout,保证了一致性,所以不会发生方案一的问题,方案一可以通过重写TextView的onDraw,依据我们的测量规则进行绘制来保持一致性则可解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值