Android笔记(三十四):封装带省略号图标结尾的TextView

背景

项目需求需要实现在文本末尾显示一个icon,如果文本很长时则在省略号后面显示icon,使用TextView自带的drawableEnd可以实现,但是如果文本换行了则会显示在TextView垂直居中的位置,不满足要求,于是有了本篇的自定义View

效果

在这里插入图片描述

原理分析

在setText的时候计算icon插入的位置,这里采用文本预加载,才能让DynamicLayout计算出准确的行数

override fun setText(text: CharSequence, type: BufferType) {mOrigText = textmBufferType = typesetTextInternal(fixTextInternal(), type)post {setTextInternal(fixTextInternal(), mBufferType)alpha = 1f}}

这里“+”用于图片占位符

val tmpSSb = SpannableStringBuilder(mOrigText)tmpSSb.append(getContentOfString(mGapToExpandHint))if (imgSpan1 != null) {tmpSSb.append("+")tmpSSb.setSpan(imgSpan1,tmpSSb.length - 1,tmpSSb.length,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)}

这个算出最后一行除去占位icon的文本索引起始点和末尾点

val indexEnd = validLayout.getLineEnd(mMaxLinesOnShrink - 1)
val indexStart = validLayout.getLineStart(mMaxLinesOnShrink - 1)
var indexEndTrimmed = (indexEnd- getLengthOfString(mEllipsisHint)- getLengthOfString(mGapToExpandHint))
if (indexEndTrimmed <= indexStart) {indexEndTrimmed = indexEnd
}

indexEndTrimmed为去掉省略号图标后的文本末尾索引,以下需要进一步修正该索引,得出准确的值indexEndTrimmedRevised,将mOrigText进行文本裁剪再加上省略号图标后返回出去

        val remainWidth = validLayout.width - (mTextPaint!!.measureText(mOrigText!!.subSequence(indexStart, indexEndTrimmed).toString()) + 0.5).toInt() - (bitmap1?.width ?: 0)val widthTailReplaced = mTextPaint!!.measureText(getContentOfString(mEllipsisHint)+ getContentOfString(mGapToExpandHint))var indexEndTrimmedRevised = indexEndTrimmedif (remainWidth > widthTailReplaced) {var extraOffset = 0var extraWidth = 0while (remainWidth > widthTailReplaced + extraWidth) {extraOffset++extraWidth = if (indexEndTrimmed + extraOffset <= mOrigText!!.length) {(mTextPaint!!.measureText(mOrigText!!.subSequence(indexEndTrimmed, indexEndTrimmed + extraOffset).toString()) + 0.5).toInt()} else {break}}indexEndTrimmedRevised += extraOffset - 1} else {var extraOffset = 0var extraWidth = 0while (remainWidth + extraWidth < widthTailReplaced) {extraOffset--extraWidth = if (indexEndTrimmed + extraOffset > indexStart) {(mTextPaint!!.measureText(mOrigText!!.subSequence(indexEndTrimmed + extraOffset,indexEndTrimmed).toString()) + 0.5).toInt()} else {break}}indexEndTrimmedRevised += extraOffset}

完整源码

class EllipsisIconTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {companion object {private const val GAP_TO_EXPAND_HINT = " "private const val MAX_LINES_ON_SHRINK = 3}private var mEllipsisHint: String? = nullprivate var mGapToExpandHint: String? = GAP_TO_EXPAND_HINTprivate var mMaxLinesOnShrink = MAX_LINES_ON_SHRINKprivate var mBufferType = BufferType.NORMALprivate var mTextPaint: TextPaint? = nullprivate var mLayout: Layout? = nullprivate var mTextLineCount = -1private var mLayoutWidth = 0private var mFutureTextViewWidth = 0private var mEllipsisIcon: Int = 0private var mOrigText: CharSequence? = nullprivate var bitmap1: Bitmap? = nullprivate var imgSpan1: ImageSpan? = nullprivate var isIconAlign = falseinit {var ellipsisIconWidth = 0var ellipsisIconHeight = 0if (attrs != null) {val a = context.obtainStyledAttributes(attrs, R.styleable.EllipsisIconTextView)val n = a.indexCountfor (i in 0 until n) {when (val attr = a.getIndex(i)) {R.styleable.EllipsisIconTextView_maxLinesOnShrink -> {mMaxLinesOnShrink = a.getInteger(attr, MAX_LINES_ON_SHRINK)}R.styleable.EllipsisIconTextView_ellipsisHint -> {mEllipsisHint = a.getString(attr)}R.styleable.EllipsisIconTextView_gapToExpandHint -> {mGapToExpandHint = a.getString(attr)}R.styleable.EllipsisIconTextView_ellipsisIcon -> {mEllipsisIcon = a.getResourceId(attr, 0)}R.styleable.EllipsisIconTextView_ellipsisIconAlign -> {isIconAlign = a.getBoolean(attr, false)}R.styleable.EllipsisIconTextView_ellipsisIconWidth -> {ellipsisIconWidth = a.getDimensionPixelSize(attr, 0)}R.styleable.EllipsisIconTextView_ellipsisIconHeight -> {ellipsisIconHeight = a.getDimensionPixelSize(attr, 0)}}}a.recycle()}bitmap1 = BitmapFactory.decodeResource(resources, mEllipsisIcon)val drawable = if (mEllipsisIcon == 0) null else AppCompatResources.getDrawable(context, mEllipsisIcon)drawable?.let {if (ellipsisIconWidth > 0 && ellipsisIconHeight > 0) {drawable.setBounds(0, 0, ellipsisIconWidth, ellipsisIconHeight)} else {drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)}imgSpan1 = if (isIconAlign) CenteredImageSpan(drawable) else ImageSpan(drawable)}alpha = 0f}fun updateForRecyclerView(text: CharSequence, futureTextViewWidth: Int) {mFutureTextViewWidth = futureTextViewWidthsetText(text, BufferType.NORMAL)}fun updateForRecyclerView(text: CharSequence, type: BufferType, futureTextViewWidth: Int) {mFutureTextViewWidth = futureTextViewWidthsetText(text, type)}fun setMaxLinesOnShrink(text: CharSequence, mMaxLinesOnShrink: Int) {this.mMaxLinesOnShrink = mMaxLinesOnShrinksetText(text, BufferType.NORMAL)}private fun fixTextInternal(): CharSequence? {if (TextUtils.isEmpty(mOrigText)) {return mOrigText}mLayout = layoutif (mLayout != null) {mLayoutWidth = mLayout!!.width}if (mLayoutWidth <= 0) {mLayoutWidth = if (width == 0) {if (mFutureTextViewWidth == 0) {return mOrigText} else {mFutureTextViewWidth - paddingLeft - paddingRight}} else {width - paddingLeft - paddingRight}}mTextPaint = paintmTextLineCount = -1val tmpSSb = SpannableStringBuilder(mOrigText)tmpSSb.append(getContentOfString(mGapToExpandHint))if (imgSpan1 != null) {tmpSSb.append("+")tmpSSb.setSpan(imgSpan1,tmpSSb.length - 1,tmpSSb.length,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)}mLayout = nullmLayout = DynamicLayout(tmpSSb,mTextPaint!!,mLayoutWidth,Layout.Alignment.ALIGN_NORMAL,1.0f,0.0f,false)mTextLineCount = mLayout!!.lineCountif (mTextLineCount <= mMaxLinesOnShrink) {return tmpSSb}val indexEnd = validLayout.getLineEnd(mMaxLinesOnShrink - 1)val indexStart = validLayout.getLineStart(mMaxLinesOnShrink - 1)var indexEndTrimmed = (indexEnd- getLengthOfString(mEllipsisHint)- getLengthOfString(mGapToExpandHint))if (indexEndTrimmed <= indexStart) {indexEndTrimmed = indexEnd}val remainWidth = validLayout.width - (mTextPaint!!.measureText(mOrigText!!.subSequence(indexStart, indexEndTrimmed).toString()) + 0.5).toInt() - (bitmap1?.width ?: 0)val widthTailReplaced = mTextPaint!!.measureText(getContentOfString(mEllipsisHint)+ getContentOfString(mGapToExpandHint))var indexEndTrimmedRevised = indexEndTrimmedif (remainWidth > widthTailReplaced) {var extraOffset = 0var extraWidth = 0while (remainWidth > widthTailReplaced + extraWidth) {extraOffset++extraWidth = if (indexEndTrimmed + extraOffset <= mOrigText!!.length) {(mTextPaint!!.measureText(mOrigText!!.subSequence(indexEndTrimmed, indexEndTrimmed + extraOffset).toString()) + 0.5).toInt()} else {break}}indexEndTrimmedRevised += extraOffset - 1} else {var extraOffset = 0var extraWidth = 0while (remainWidth + extraWidth < widthTailReplaced) {extraOffset--extraWidth = if (indexEndTrimmed + extraOffset > indexStart) {(mTextPaint!!.measureText(mOrigText!!.subSequence(indexEndTrimmed + extraOffset,indexEndTrimmed).toString()) + 0.5).toInt()} else {break}}indexEndTrimmedRevised += extraOffset}val fixText = removeEndLineBreak(mOrigText!!.subSequence(0, indexEndTrimmedRevised))val ssbShrink = SpannableStringBuilder(fixText)if (mEllipsisHint != null) {ssbShrink.append(mEllipsisHint)}ssbShrink.append(getContentOfString(mGapToExpandHint))if (imgSpan1 != null) {ssbShrink.append("+")ssbShrink.setSpan(imgSpan1,ssbShrink.length - 1,ssbShrink.length,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)}return ssbShrink}private fun removeEndLineBreak(text: CharSequence): String {var str = text.toString()while (str.endsWith("\n")) {str = str.substring(0, str.length - 1)}val mLayout: Layout = DynamicLayout(str,mTextPaint!!,mLayoutWidth,Layout.Alignment.ALIGN_NORMAL,1.0f,0.0f,false)if (mLayout.lineCount > mMaxLinesOnShrink) {if (str.contains("\n")) {str = str.substring(0, str.lastIndexOf("\n"))}}return str}private val validLayout: Layoutget() = if (mLayout != null) mLayout!! else layoutoverride fun setText(text: CharSequence, type: BufferType) {mOrigText = textmBufferType = typesetTextInternal(fixTextInternal(), type)post {setTextInternal(fixTextInternal(), mBufferType)alpha = 1f}}private fun setTextInternal(text: CharSequence?, type: BufferType) {super.setText(text, type)}private fun getLengthOfString(string: String?): Int {return string?.length ?: 0}private fun getContentOfString(string: String?): String {return string ?: ""}internal class CenteredImageSpan(drawableRes: Drawable) : ImageSpan(drawableRes) {override fun draw(canvas: Canvas, text: CharSequence,start: Int, end: Int, x: Float,top: Int, y: Int, bottom: Int, paint: Paint) {val b = drawableval fm = paint.fontMetricsIntval transY = ((y + fm.descent + y + fm.ascent) / 2 - b.bounds.bottom / 2)canvas.save()canvas.translate(x, transY.toFloat())b.draw(canvas)canvas.restore()}}}
<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="EllipsisIconTextView"><attr name="maxLinesOnShrink" format="reference|integer" /><attr name="ellipsisHint" format="reference|string" /><attr name="gapToExpandHint" format="reference|string" /><attr name="ellipsisIcon" format="reference"/><attr name="ellipsisIconAlign" format="boolean"/><attr name="ellipsisIconWidth" format="dimension"/><attr name="ellipsisIconHeight" format="dimension"/></declare-styleable></resources>
  • 测试代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"><com.mask_boy.test.myapplication.EllipsisIconTextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginHorizontal="40dp"android:gravity="center"android:text="My name is Masked Boy, My name is Masked Boy"android:textSize="18sp"app:ellipsisIconAlign="true"app:ellipsisIconHeight="15dp"app:ellipsisIconWidth="15dp"app:ellipsisHint="..."app:gapToExpandHint="More"app:layout_constraintBottom_toTopOf="@+id/ellipsisIconTextView"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:maxLinesOnShrink="1" /><com.mask_boy.test.myapplication.EllipsisIconTextViewandroid:id="@+id/ellipsisIconTextView"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginHorizontal="40dp"android:gravity="center"android:text="My name is Masked Boy, My name is Masked Boy"android:textSize="18sp"app:ellipsisIcon="@drawable/ic_lock_tips_arrow"app:ellipsisIconAlign="true"app:ellipsisIconHeight="15dp"app:ellipsisIconWidth="15dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:maxLinesOnShrink="2" /><com.mask_boy.test.myapplication.EllipsisIconTextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginHorizontal="40dp"android:gravity="center"android:text="My name is Masked Boy, My name is Masked Boy"android:textSize="18sp"app:ellipsisIcon="@drawable/ic_lock_tips_arrow"app:ellipsisIconAlign="true"app:ellipsisIconHeight="15dp"app:ellipsisIconWidth="15dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/ellipsisIconTextView"app:maxLinesOnShrink="1" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CEF127 编译指南 Linux篇 - 安装Git和Python(三)

1. 引言 在前面的文章中&#xff0c;我们已经完成了基础开发工具的安装和配置。接下来&#xff0c;我们需要安装两个同样重要的工具&#xff1a;Git 和 Python。这两个工具在 CEF 的编译过程中扮演着关键角色。Git 负责管理和获取源代码&#xff0c;而 Python 则用于运行各种编…

centos系统设置本地yum源教程

在CentOS系统中,将ISO文件设置为本地源可以加快软件安装速度,特别是在没有网络连接的环境下。以下是详细步骤: 1. 下载和准备ISO镜像文件 首先,从CentOS的官方网站下载适合需求的CentOS ISO镜像文件。可以选择不同的版本,如CentOS 7或CentOS 8,以及适合你硬件架构的版本…

PDF view | Chrome PDF Viewer |Chromium PDF Viewer等指纹修改

1、打开https://www.browserscan.net/zh/ 2、将internal-pdf-viewer改为 internal-pdf-viewer-jdtest看下效果&#xff1a; 3、源码修改&#xff1a; third_party\blink\renderer\modules\plugins\dom_plugin_array.cc namespace { DOMPlugin* MakeFakePlugin(String plugin_…

模糊认知图模型、特征与推理

1. 基础知识 1.1认知图的发展 1948年&#xff0c;Tolman首次提到认知图&#xff3b;I]它把认知图描述为有向图&#xff0c;认为认知图是由一些弧连接起来的结点的集合&#xff0c;其目的是为心理学构建一个模型。后来&#xff0c;认知图被其他学者所借用&#xff0c;不同的学…

Mac 环境下类Xshell 的客户端介绍

在 Mac 环境下&#xff0c;类似于 Windows 环境中 Xshell 用于访问 Linux 服务器的工具主要有以下几种&#xff1a; SecureCRT&#xff1a; 官网地址&#xff1a;https://www.vandyke.com/products/securecrt/介绍&#xff1a;支持多种协议&#xff0c;如 SSH1、SSH2、Telnet 等…

玩转 uni-app 静态资源 static 目录的条件编译

一. 前言 老生常谈&#xff0c;了解 uni-app 的开发都知道&#xff0c;uni-app 可以同时支持编译到多个平台&#xff0c;如小程序、H5、移动端 App 等。它的多端编译能力是 uni-app 的一大特点&#xff0c;让开发者可以使用同一套代码基于 Vue.js 的语法编写程序&#xff0c;然…

【西瓜书】支持向量机(SVM)

支持向量机&#xff08;Support Vector Machine&#xff0c;简称SVM&#xff09;。 超平面 分类学习最基本的想法就是基于训练集合D在样本空间中找到一个划分超平面&#xff0c;将不同类别的样本分开。 但能将训练样本分开的划分超平面可能有很多&#xff0c;应该努力去找到哪…

【开源免费】基于SpringBoot+Vue.JS宠物咖啡馆平台(JAVA毕业设计)

博主说明&#xff1a;本文项目编号 T 064 &#xff0c;文末自助获取源码 \color{red}{T064&#xff0c;文末自助获取源码} T064&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析…

海康VsionMaster学习笔记(学习工具+思路)

一、前言 VisionMaster算法平台集成机器视觉多种算法组件&#xff0c;适用多种应用场景&#xff0c;可快速组合算法&#xff0c;实现对工件或被测物的查找测量与缺陷检测等。VM算法平台依托海康威视在图像领域多年的技术积淀&#xff0c;自带强大的视觉分析工具库&#xff0c;可…

Linux内核编译流程(Ubuntu24.04+Linux Kernel 6.8.12)

万恶的拯救者&#xff0c;使用Ubuntu没有声音&#xff0c;必须要自己修改一下Linux内核中的相关驱动逻辑才可以&#xff0c;所以被迫学习怎么修改内核&编译内核&#xff0c;记录如下 准备工作 下载Linux源码&#xff1a;在Linux发布页下载并使用gpg签名验证 即&#xff1a…

UE5 打包报错 Unknown structure 的解决方法

在虚幻引擎5.5 打包报错如下&#xff1a; UATHelper: 打包 (Windows): LogInit: Display: LogProperty: Error: FStructProperty::Serialize Loading: Property ‘StructProperty /Game/Components/HitReactionComponent/Blueprints/BI_ReactionInterface.BI_ReactionInterface…

根据导数的定义计算导函数

根据导数的定义计算导函数 1. Finding derivatives using the definition (使用定义求导)1.1. **We want to differentiate f ( x ) 1 / x f(x) 1/x f(x)1/x with respect to x x x**</font>1.2. **We want to differentiate f ( x ) x f(x) \sqrt{x} f(x)x ​ wi…

论文阅读:Dual-disentangled Deep Multiple Clustering

目录 摘要 引言 模型 实验 数据集 实验结果 结论 摘要 多重聚类近年来引起了广泛关注&#xff0c;因为它能够从不同的角度揭示数据的多种潜在结构。大多数多重聚类方法通常先通过控制特征之间的差异性来提取特征表示&#xff0c;然后使用传统的聚类方法&#xff08;如 …

Kafka知识体系

一、认识Kafka 1. kafka适用场景 消息系统&#xff1a;kafka不仅具备传统的系统解耦、流量削峰、缓冲、异步通信、可扩展性、可恢复性等功能&#xff0c;还有其他消息系统难以实现的消息顺序消费及消息回溯功能。 存储系统&#xff1a;kafka把消息持久化到磁盘上&#xff0c…

项目切换Java21

目录 项目启动流程 遇到的问题 目前我们所有的项目都是Java8的&#xff0c;突然交接过来一个Java21的项目&#xff0c;项目启动耗时挺久&#xff0c;本篇记录下问题以及解决方案 项目启动流程 1. 下载Java21 2. 配置Java21 本机环境变量配置 确保path里有Java21路径 3. …

【算法day4】链表:应用拓展与快慢指针

题目引用 两两交换链表节点删除链表的倒数第n个节点链表相交环形链表 1.两两交换链表节点 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&am…

电机控制理论基础及其应用

电机控制理论是电气工程和自动化领域中的一个重要分支&#xff0c;它主要研究如何有效地控制电机的运行状态&#xff0c;包括速度、位置、扭矩等&#xff0c;以满足各种应用需求。电机控制理论的基础知识涵盖了电机的工作原理、数学模型、控制策略以及实现技术等方面。下面是一…

【每天一篇深度学习论文】(IEEE 2024)即插即用特征增强模块FEM

目录 论文介绍题目&#xff1a;论文地址&#xff1a; 创新点方法整体结构 即插即用模块作用消融实验结果即插即用模块代码 论文介绍 题目&#xff1a; FFCA-YOLO for Small Object Detection in Remote Sensing Images 论文地址&#xff1a; https://ieeexplore.ieee.org/d…

『 Linux 』数据链路层 - ARP协议及数据链路层周边问题

文章目录 ARP协议ARP欺骗RARP协议 DNS服务ICMP协议ping 命令正向代理服务器反向代理服务器 ARP协议 博客『 Linux 』数据链路层 - MAC帧/以太网帧中提到,当数据需要再数据链路层进行无网络传输时需要封装为MAC帧,而MAC帧的报文结构如下: 帧头部分存在两个字段分别为 “目的地址…

基于Java Springboot Vue3图书管理系统

一、作品包含 源码数据库设计文档万字全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue3、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA 数据库&#x…