android 富文本及展示更多组件

在这里插入图片描述
模拟微博 #热贴 和 @用户 的这种 富文本形式组件,不说了, 直接上代码

package com.tongtong.feat_watch.view

import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.isVisible
import com.didi.drouter.annotation.Router
import com.google.android.material.chip.ChipGroup
import com.google.gson.Gson
import com.tongtong.feat_watch.R
import com.tongtong.feat_watch.databinding.ViewRichBinding
import com.tongtong.feat_watch.ui.topic.bean.ContentBean
import com.tongtong.feat_watch.ui.topic.bean.ContentType
import com.tongtong.lib_router.TTRouter
import com.tongtong.lib_util.ToastUtils
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber


/**
 * @date: 2024/11/7
 * desc:
 * author: shuhuai
 * version:
 */
class ContentView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) :
    FrameLayout(context, attrs, defStyleAttr) {
    //文本颜色
    private var textColor: Int

    //字体大小
    private var textSize: Float

    //折叠后显示的行数
    private val showLines: Int

    private var isBold: Boolean
    private var urltextColor: Int


    //是否可折叠
    private var expandEnable: Boolean
    private lateinit var binding: ViewRichBinding
    private var moreBack: (v: TextView?) -> Unit = {}

    init {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandTextView)
        textColor = typedArray.getColor(
            R.styleable.ExpandTextView_contentTextColor,
            context.resources.getColor(R.color.color_131314)
        )
        urltextColor = typedArray.getColor(
            R.styleable.ExpandTextView_urlTextColor,
            context.resources.getColor(R.color.color_5D34D0)
        )
        showLines = typedArray.getColor(R.styleable.ExpandTextView_showLines, Int.MAX_VALUE)
        textSize = typedArray.getDimension(R.styleable.ExpandTextView_contentTextSize, 16f)
        expandEnable = typedArray.getBoolean(R.styleable.ExpandTextView_expandEnable, false)
        isBold = typedArray.getBoolean(R.styleable.ExpandTextView_isBold, false)
        typedArray.recycle()
        initView()
    }


    private fun initView() {
        binding = ViewRichBinding.inflate(LayoutInflater.from(context), this, true)
        bindContent()
    }

    fun bindContent(
        s: String = "",
        array: List<ContentBean> = arrayListOf(),
        moreback: (view: TextView?) -> Unit = {}
    ): ContentView {
        try {
            if (!::binding.isInitialized) {
                return this
            }
            this.moreBack = moreback
            if (s?.isEmpty() == true && array.isNullOrEmpty()) isVisible = true
            build(content(s, array))

        } catch (e: Exception) {
            Timber.e(e)
        }
        return this;
    }

    private fun content(str: String = "", array: List<ContentBean>): String {
        var s = ""
        if (array.isEmpty()) {
            s = "$str"
        } else {
            array.forEach {
                when (it.type()) {
                    ContentType.TEXT -> s += "${it.content}"
                    ContentType.TOPIC -> s += "<a href='{type:1,desc:\"话题详情\", id:${it.id}}'>#${it.content}</a>"
                    ContentType.USER -> s += "<a href='{type:0, desc:\"用户\", id:${it.id}}'>@${it.content}</a>"
                }
            }
        }
        return s;
    }


    fun build(content: String? = "") {
        binding.content
            .setUrlColor(urltextColor)
            .setUrlBlod(isBold)
            .setText(content)
            .setTextColor(textColor)
            .setTextSize(textSize)
            .setShowLines(showLines)
            .setExpandEnable(expandEnable)
            .setSpanClickable(true, object :
                CustomTextView.TextSpanClickListener {
                //设置有标签或@某人的点击, 默认为false
                override fun onTextSpanClick(data: String?) {
                    try {
                        val jsonObject = JSONObject(data)
                        val type = jsonObject.getInt("type")
                        val id = jsonObject.getString("id")
                        when (type) {
                            0 -> {
                                if (id.isNotEmpty()) {
                                    TTRouter.goOtherUser(id.toLong())
                                }
                            }

                            1 -> {
                                if (id.isNotEmpty()) {
                                    TTRouter.goTopicManagerN(id)
                                }
                            }
                        }
                    } catch (e: JSONException) {
                        e.printStackTrace()
                    }
                }
            }).setMore(moreBack).requestLayout()
    }

    fun setTextColor(textColor: Int): ContentView {
        binding.content.setTextColor(textColor)
        return this
    }

    fun setTextSize(textSize: Float): ContentView {
        binding.content.setTextSize(textSize)
        return this
    }

    fun setUrlColor(color: Int): ContentView {
        this.urltextColor = color
        return this
    }

    fun setUrlBlod(bold: Boolean): ContentView {
        this.isBold = bold
        return this
    }

}



package com.tongtong.feat_watch.view

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.TextPaint
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.google.android.material.chip.ChipGroup
import com.tongtong.feat_watch.R

/**
 * @author: shuhuai
 * @desc:
 * @date: 2024/12/4
 * @version:
 * @remark
 */
class CustomTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) :
    ChipGroup(context, attrs, defStyleAttr) {
    //是否可折叠
    private var expandEnable = false
    //文本内容
    private var text: String? = null

    //文本颜色
    private var textColor: Int
    private var mtextColor: Int
    private var urltextColor: Int

    //字体大小
    private var textSize: Float
    private var mtextSize: Float

    //折叠后显示的行数
    private var showLines: Int

    //内容文本
    private var mTextView: NonScrollingTextView? = null
    private var moreTextView: NonScrollingTextView? = null

    //true, 表示拦截标签span点击事件
    //false, 表示普通文本
    private var spanClickable: Boolean
    private var isBold: Boolean


    //spanString点击的回调
    interface TextSpanClickListener {
        fun onTextSpanClick(data: String?)
    }

    private var mTextSpanClick: TextSpanClickListener? = null

    private fun init(context: Context) {
        removeAllViews()
        chipSpacingVertical = 0
        chipSpacingHorizontal = 0
        val params =
            LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
        layoutParams = params
        //内容显示文本
        mTextView = NonScrollingTextView(context)
        //        mTextView.setText(text);
        mTextView!!.textSize = textSize
        mTextView!!.setTextColor(textColor)
        mTextView!!.ellipsize = TextUtils.TruncateAt.END
        mTextView!!.layoutParams = params
        mTextView!!.maxLines = showLines

        //根据SpanClickable状态来设置文本
        setTextBySpanClickableStatus()
        mTextView!!.movementMethod = LinkMovementMethod.getInstance()
        addView(mTextView)

        moreTextView = NonScrollingTextView(context)
        moreTextView!!.setText("...展开");
        moreTextView!!.textSize = mtextSize
        moreTextView!!.setTextColor(mtextColor)
        addView(moreTextView)
    }

    init {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandTextView)
        textColor = typedArray.getColor(
            R.styleable.ExpandTextView_contentTextColor,
            context.resources.getColor(R.color.color_131314)
        )
        mtextColor = typedArray.getColor(
            R.styleable.ExpandTextView_moreTextColor,
            context.resources.getColor(R.color.extended_color)
        )
        urltextColor = typedArray.getColor(
            R.styleable.ExpandTextView_urlTextColor,
            context.resources.getColor(R.color.color_5D34D0)
        )
        textSize = typedArray.getDimension(R.styleable.ExpandTextView_contentTextSize, 16f)
        mtextSize = typedArray.getDimension(R.styleable.ExpandTextView_moreTextSize, 16f)
        spanClickable = typedArray.getBoolean(R.styleable.ExpandTextView_spanClickable, false)
        showLines = typedArray.getColor(R.styleable.ExpandTextView_showLines, Int.MAX_VALUE)
        expandEnable = typedArray.getBoolean(R.styleable.ExpandTextView_expandEnable, false)
        isBold = typedArray.getBoolean(R.styleable.ExpandTextView_isBold, false)
        typedArray.recycle()
        init(context)
    }

    @SuppressLint("RestrictedApi")
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (expandEnable) {
            moreTextView!!.visibility = if (mTextView!!.lineCount > showLines) VISIBLE else GONE
        }else {
            moreTextView!!.visibility = GONE
        }
    }


    /**
     * 将dip或dp值转换为px值,保证尺寸大小不变
     */
    private fun dip2px(context: Context, dipValue: Float): Int {
        val scale = context.resources.displayMetrics.density
        return (dipValue * scale + 0.5f).toInt()
    }

    fun setText(text: String?): CustomTextView {
        this.text = text

        setTextBySpanClickableStatus()

        return this
    }


    fun setTextColor(textColor: Int): CustomTextView {
        this.textColor = textColor
        mTextView!!.setTextColor(textColor)
        return this
    }

    fun setTextSize(textSize: Float): CustomTextView {
        this.textSize = textSize
        mTextView!!.textSize = textSize
        return this
    }

    fun setExpandEnable(able: Boolean): CustomTextView {
        this.expandEnable = able
        return this
    }

    fun setShowLines(lines: Int): CustomTextView {
        this.showLines = lines
        mTextView?.maxLines = lines
        return this
    }

    fun setSpanClickable(
        spanClickable: Boolean,
        textSpanClick: TextSpanClickListener
    ): CustomTextView {
        this.spanClickable = spanClickable
        mTextSpanClick = textSpanClick

        setTextBySpanClickableStatus()
        return this
    }

    fun setUrlColor(color: Int): CustomTextView {
        this.urltextColor = color
        return this
    }

    fun setUrlBlod(bold: Boolean): CustomTextView {
        this.isBold = bold
        return this
    }

    /**
     * 格式化超链接文本内容并设置点击处理
     */
    private fun getClickableHtml(html: String?): CharSequence {
        val spannedHtml = Html.fromHtml(html)

        val clickableHtmlBuilder = SpannableStringBuilder(spannedHtml)
        val urls = clickableHtmlBuilder.getSpans(
            0, spannedHtml.length,
            URLSpan::class.java
        )
        for (span in urls) {
            setLinkClickable(clickableHtmlBuilder, span)
        }
        return clickableHtmlBuilder
    }

    /**
     * 设置点击超链接对应的处理内容
     */
    private fun setLinkClickable(clickableHtmlBuilder: SpannableStringBuilder, urlSpan: URLSpan) {
        val start = clickableHtmlBuilder.getSpanStart(urlSpan)
        val end = clickableHtmlBuilder.getSpanEnd(urlSpan)
        val flags = clickableHtmlBuilder.getSpanFlags(urlSpan)

        clickableHtmlBuilder.setSpan(object : ClickableSpan() {
            override fun onClick(view: View) {
                if (mTextSpanClick != null) {
                    //取出a标签的href携带的数据, 并回调到调用处
                    //href的数据类型根据个人业务来定, demo是传的json字符串
                    mTextSpanClick!!.onTextSpanClick(urlSpan.url)
                }
            }

            override fun updateDrawState(ds: TextPaint) {
                super.updateDrawState(ds)
                ds.linkColor = Color.TRANSPARENT
                ds.color = urltextColor
                ds.isUnderlineText = false
                ds.isFakeBoldText = isBold
            }
        }, start, end, flags)
    }


    /**
     * 根据SpanClickable的状态来设置文本
     */
    private fun setTextBySpanClickableStatus() {
        if (!TextUtils.isEmpty(text)) {
            if (spanClickable) {
                mTextView!!.autoLinkMask = Linkify.ALL
                mTextView!!.text = getClickableHtml(text)
            } else {
                mTextView!!.text = text
            }
        }
    }

    fun setMore(moreback: (view: TextView?) -> Unit = {}): CustomTextView {
        moreTextView?.setOnClickListener{
            moreback.invoke(moreTextView)
        }
        return this
    }


}

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ExpandTextView">
        <!--内容文本颜色-->
        <attr name="contentTextColor" format="reference|color"/>
        <attr name="moreTextColor" format="reference|color"/>
        <attr name="urlTextColor" format="reference|color"/>
        <!--内容文本字体大小-->
        <attr name="contentTextSize" format="dimension"/>
        <attr name="moreTextSize" format="dimension"/>
        <!--按钮文本颜色-->
        <attr name="btnTextColor" format="reference|color"/>
        <!--按钮折叠的描述-->
        <attr name="btnExpandText" format="string"/>
        <!--按钮展开的描述-->
        <attr name="btnSpreadText" format="string"/>
        <!--按钮字体大小-->
        <attr name="btnTextSize" format="dimension"/>
        <!--最大显示行数-->
        <attr name="showLines" format="integer"/>
        <!--是否显示箭头图标-->
        <attr name="showIcon" format="boolean"/>
        <!--箭头资源-->
        <attr name="iconRes" format="reference"/>
        <!--动画时长-->
        <attr name="animationDuration" format="integer"/>
        <!--@或者#话题#是否自定义点击事件-->
        <attr name="spanClickable" format="boolean"/>
        <!--是否可折叠-->
        <attr name="expandEnable" format="boolean"/>
        <attr name="isBold" format="boolean"/>
    </declare-styleable>
</resources>

这个组件还有一点问题, 就是 富文本形式下,限制行数,不展示… 需要优化下,可以考虑下从html标签入手,或者直接自己画一个,这个就稍微麻烦点
后面有时间再优化吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值