android 富文本及展示更多组件

news2025/1/6 17:49:56

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

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 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)
        )
        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)
        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
            .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
    }

}

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 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


    //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)
        )
        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)
        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
    }

    /**
     * 格式化超链接文本内容并设置点击处理
     */
    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 = context.getColor(R.color.color_17006C)
                ds.isUnderlineText = false
            }
        }, 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="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"/>
    </declare-styleable>
</resources>

这个组件还有一点问题, 就是 富文本形式下,限制行数,不展示… 需要优化下, 后面有时间再优化吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2255226.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【踩坑】修复报错libcurl.so.4、LIBFFI_BASE_7.0、libssl.so.3

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ libcurl.so.4&#xff1a; sudo apt install curl -y LIBFFI_BASE_7.0: conda install libffi3.3 -y libssl.so.3: sudo apt install -y openssl li…

AI智能体Prompt预设词指令大全+GPTs应用使用

AI智能体使用指南 直接复制在AI工具助手中使用&#xff08;提问前&#xff09; 可前往SparkAi系统用户官网进行直接使用 SparkAI系统介绍文档&#xff1a;Docs 常见AI智能体GPTs应用大全在线使用 自定义添加制作AI智能体进行使用&#xff1a; 文章润色器 你是一位具有敏锐洞察…

高效查找秘密武器一:位图

有这样的一个问题&#xff1a; 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在这40亿个数 中。 那么我们一般会想到这样做的 1.遍历&#xff0c;时间复杂度O(n) 2.排序&#xff08;N*logN&#xff09;&#xff0c…

爬虫运行后数据如何存储?

爬虫运行后获取的数据可以存储在多种不同的存储系统中&#xff0c;具体选择取决于数据的规模、查询需求以及应用场景。以下是一些常见的数据存储方法&#xff1a; 1. 文件系统 对于小型项目或临时数据存储&#xff0c;可以直接将数据保存到本地文件中。常见的文件格式包括&…

ultralytics-YOLOv11的目标检测解析

1. Python的调用 from ultralytics import YOLO import os def detect_predict():model YOLO(../weights/yolo11n.pt)print(model)results model(../ultralytics/assets/bus.jpg)if not os.path.exists(results[0].save_dir):os.makedirs(results[0].save_dir)for result in…

PowerShell install 一键部署postgres17

postgres 前言 PostgreSQL 是一个功能强大的开源对象关系数据库系统,拥有超过 35 年的积极开发经验 这为其赢得了可靠性、功能稳健性和性能的良好声誉。 通过官方文档可以找到大量描述如何安装和使用 PostgreSQL 的信息。 开源社区提供了许多有用的地方来熟悉PostgreSQL, 了…

Elasticsearch数据迁移(快照)

1. 数据条件 一台原始es服务器&#xff08;192.168.xx.xx&#xff09;&#xff0c;数据迁移后的目标服务器&#xff08;10.2.xx.xx&#xff09;。 2台服务器所处环境&#xff1a; centos7操作系统&#xff0c; elasticsearch-7.3.0。 2. 为原始es服务器数据创建快照 修改elas…

学习threejs,使用VideoTexture实现视频Video更新纹理

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️VideoTexture 视频纹理 二、…

Next.js 实战 (二):搭建 Layouts 基础排版布局

前言 等了许久&#xff0c;Next.js 终于迎来了 v15.x 版本&#xff0c;刚好 Github 上面的旧项目重构完&#xff0c;终于可以放心大胆地去研究 Next.js了。 搭建最新项目可以参考官方文档&#xff1a;Installation 最新的 Next.js 版本&#xff0c;使用的是 React19.x 内测版…

小红薯x-s算法最新补环境教程12-06更新(下)

在上一篇文章中已经讲了如何去定位x-s生成的位置&#xff0c;本篇文章就直接开始撸代码吧 如果没看过的话可以看&#xff1a;小红薯最新x-s算法分析12-06&#xff08;x-s 56&#xff09;&#xff08;上&#xff09;-CSDN博客 1、获取加密块代码 首先来到参数生成的位置&…

Microi吾码产品深度测评:轻量级企业管理应用的全方位剖析

开源低代码平台-Microi吾码-平台简介 技术框架&#xff1a;.NET8 Redis MySql/SqlServer/Oracle Vue2/3 Element-UI/Element-Plus 平台始于2014年&#xff08;基于Avalon.js&#xff09;&#xff0c;2018年使用Vue重构&#xff0c;于2024年10月29日开源 Vue3试用地址&am…

unity打包web,发送post请求,获取地址栏参数,解决TypeError:s.replaceAll is not a function

发送post请求 public string url "http://XXXXXXXXX";// 请求数据public string postData "{\"user_id\": 1}";// Start is called before the first frame updatevoid Start(){// Post();StartCoroutine(PostRequestCoroutine(url, postData…

专业140+总分420+上海交通大学819考研经验上交电子信息与通信工程,真题,大纲,参考书。博睿泽信息通信考研论坛,信息通信考研Jenny

考研结束&#xff0c;专业819信号系统与信号处理140&#xff0c;总分420&#xff0c;终于梦圆交大&#xff0c;高考时敢都不敢想目标&#xff0c;现在已经成为现实&#xff0c;考研后劲很大&#xff0c;这一年的复习经历&#xff0c;还是历历在目&#xff0c;整理一下&#xff…

mac port 安装redis 并设置为系统服务 自定义配置方法

mac系统中&#xff0c;port 包管理工具比brew的速度快N倍&#xff0c;今天就给大家分享一下在macos系统中如何使用 port安装 redis数据库并配置为服务自动启动和自定义redis.conf配置的方法。 1. 安装redis sudo port install redis 2. 启动redis服务 sudo port load redis …

【多线程-第一天-NSThread-互斥锁和自旋锁的区别-练习-异步下载网络图片 Objective-C语言】

一、互斥锁和自旋锁的区别 1.刚刚我们看过了,互斥锁和自旋锁,下边我们来看,互斥锁和自旋锁的一个区别, 1)互斥锁:如果发现其他线程正在执行锁定代码,线程会进入休眠(就绪状态),等其他线程时间片到了,打开锁后,线程会被唤醒(执行) 它是被唤醒的,相当于什么呢,…

Ubuntu环境安装RabbitMQ

1.安装Erlang RabbitMq需要Erlang语⾔的⽀持&#xff0c;在安装rabbitMq之前需要安装erlang # 更新软件包 sudo apt-get update # 安装 erlang sudo apt-get install erlang 查看erlang版本 : erl 退出命令:halt(). 2. 安装RabbitMQ # 更新软件包 sudo apt-get update # 安装 …

基于Huffman编码的GPS定位数据无损压缩算法

目录 一、引言 二、霍夫曼编码 三、经典Huffman编码 四、适应性Huffman编码 五、GPS定位数据压缩 提示&#xff1a;文末附定位数据压缩工具和源码 一、引言 车载监控系统中&#xff0c;车载终端需要获取GPS信号&#xff08;经度、纬 度、速度、方向等&#xff09;实时上传…

智慧油客:从初识、再识OceanBase,到全栈上线

今天&#xff0c;我们邀请了智慧油客的研发总监黄普友&#xff0c;为我们讲述智慧油客与 OceanBase 初识、熟悉和结缘的故事。 智慧油客自2016年诞生以来&#xff0c;秉持新零售的思维&#xff0c;成功从过去二十年间以“以销售产品为中心”的传统思维模式&#xff0c;转向“以…

如何查看电脑的屏幕刷新率?

1、按一下键盘的 win i 键&#xff0c;打开如下界面&#xff0c;选择【系统】&#xff1a; 2、选择【屏幕】-【高级显示设置】 如下位置&#xff0c;显示屏幕的刷新率&#xff1a;60Hz 如果可以更改&#xff0c;则选择更高的刷新率&#xff0c;有助于电脑使用起来界面更加流…

数据结构——有序二叉树的删除

在上一篇博客中&#xff0c;我们介绍了有序二叉树的构建、遍历、查找。 数据结构——有序二叉树的构建&遍历&查找-CSDN博客文章浏览阅读707次&#xff0c;点赞18次&#xff0c;收藏6次。因为数据的类型决定数据在内存中的存储形式。left right示意为左右节点其类型也为…