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.mzph.cn/diannao/62545.shtml

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

相关文章

Web开发基础学习——通过React示例学习模态对话框

Web开发基础学习系列文章目录 第一章 基础知识学习之通过React组件学习模态对话框 文章目录 Web开发基础学习系列文章目录前言一、创建新的 React 应用二、 创建模态对话框组件三、修改 App.js四、 添加样式五、启动应用六、访问应用总结 前言 模态对话框&#xff08;Modal D…

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

`console.log`调试完全指南

大家好&#xff0c;这里是 Geek技术前线。 今天我们来探讨 Console.log() 的一些优点。并分析一些基本概念和实践&#xff0c;这些可以让我们的调试工作变得更加高效。 理解前端 log 与后端 log 的区别 前端 log 与后端 log 有着显著的不同&#xff0c;理解这一点至关重要。…

k8s 1.28 聚合层部署信息记录

–requestheader-client-ca-file –requestheader-allowed-namesfront-proxy-client –requestheader-extra-headers-prefixX-Remote-Extra- –requestheader-group-headersX-Remote-Group –requestheader-username-headersX-Remote-User –proxy-client-cert-file –proxy-cl…

Flutter:列表分页,上拉加载下拉刷新,在GetBuilder模板使用方式

GetBuilder模板使用方式参考上一节 本篇主要代码记录如何使用上拉加载下拉刷新&#xff0c; 接口请求和商品组件的代码不包括在内 pubspec.yaml装包 cupertino_icons: ^1.0.8# 分页 上拉加载&#xff0c;下拉刷新pull_to_refresh_flutter3: 2.0.2商品列表&#xff1a;controlle…

使用Cmake导入OpenCV库的大坑记录

CMakeLists.txt cmake_minimum_required(VERSION 3.20)set(OpenCV_DIR D:/Package/opencv4/opencv/mingw-build/install) #这里根据自己OpenCV位置设定find_package(OpenCV REQUIRED)project(PROJ1 CXX)add_executable(PROJ1 main.cpp)target_include_directories(PROJ1 PR…

MySQL —— MySQL 程序

目录 前言 一、MySQL 程序简介 二、mysqld -- MySQL 服务器 三、mysql -- MySQL 客户端 1. mysql 客户端简介 2. mysql 客户端选项 &#xff08;1&#xff09;指定选项的方式 &#xff08;2&#xff09;mysql 客户端命令常用选项 &#xff08;3&#xff09;在命令行中使…

STM32 PWM波形详细图解

目录 前言 一 PWM介绍 1.1 PWM简介 1.2 STM32F103 PWM介绍 1.3 时钟周期与占空比 二.引脚映像关系 2.1引脚映像与寄存器 2.2 复用功能映像 三. PWM 配置步骤 3.1相关原理图 3.2配置流程 3.2.1 步骤一二&#xff1a; 3.2.2 步骤三&#xff1a; 3.2.3 步骤四五六七&#xff1a; …

【软件安装】在Ubuntu中安装mysql5.7

参考&#xff1a;cubuntu安装mysql5.6_mob649e81553a70的技术博客_51CTO博客 问题1&#xff1a;sudo apt install mysql-server-5.7 -y 若提示mysql-server 没有可安装候选 答&#xff1a; sudo nano /etc/apt/sources.list 在开头加入&#xff1a; # 阿里源 deb http://mi…

多方法做配对样本t检验(三)

Wilcoxon符号秩检验 Wilcoxon符号秩检验&#xff08;Wilcoxon Signed-Rank Test&#xff09; 是一种非参数统计方法&#xff0c;用于检验两组相关样本&#xff08;配对样本&#xff09;之间的差异是否显著。它通常用来代替配对样本t检验&#xff0c;特别是在数据不符合正态分布…

修改IDEA配置导致Spring Boot项目读取application.properties中文乱码问题

之前很多配置都是放在nacos里面&#xff0c;然后这次同事有个配置写在application.properties中&#xff0c;这个配置含有中文&#xff0c;启动之后发现拿到的中文值会乱码&#xff0c;然后就帮忙看了一下问题。 排查问题 经过不停的百度、排查发现&#xff0c;spring读取app…

0.shell 脚本执行方式

1.脚本格式要求 &#x1f951;脚本以 #!/bin/bash 开头 &#x1f966; 脚本要有可执行权限 2.执行脚本的两种方式 &#x1f96c; 方式1&#xff1a;赋予x执行权限 &#x1f952; ​​​​​​​方式2&#xff1a; sh执行 ​​​​​​​

[2024年3月10日]第15届蓝桥杯青少组stema选拔赛C++中高级(第二子卷、编程题(6))

参考程序&#xff1a; #include<bits/stdc.h> using namespace std; int n; int a[305]; int dp[305][305];//打掉ij之间所有靶子可以获得的最大积分&#xff08;不含i&#xff0c;j&#xff09; int main() {cin>>n;for(int i1;i<n;i){cin>>a[i];}a[0]1…

深入了解 Adam 优化器对显存的需求:以 LLaMA-2 7B 模型为例 (中英双语)

中文版 深入了解 Adam 优化器对显存的额外需求&#xff1a;模型参数与优化器状态的显存开销分析 在深度学习模型的训练过程中&#xff0c;显存是一个关键的资源&#xff0c;尤其在处理大型语言模型或深度神经网络时。训练时的显存需求不仅包括模型参数本身&#xff0c;还涉及…

k8s Init:ImagePullBackOff 的解决方法

kubectl describe po (pod名字) -n kube-system 可查看pod所在的节点信息 例如&#xff1a; kubectl describe po calico-node-2lcxx -n kube-system 执行拉取前先把用到的节点的源换了 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"re…

Vue--------导航守卫(全局,组件,路由独享)

全局导航守卫 beforeEach 全局前置守卫 afterEach 全局后置守卫 路由独享守卫 beforeEnter 路由独享守卫 组件导航守卫 beforeRouteEnter 进入组件前 beforeRouteUpdate 路由改变但是组件复调用 beforeRouteLeave 离开组件之前 执行顺…

入门产品经理,考PMP还是NPDP?

PMP和NPDP都是管理行业内的高含金量证书&#xff0c;不过PMP侧重项目管理&#xff0c;NPDP侧重产品开发。如果你已经确定了自己的职业方向是产品经理的话&#xff0c;那么考NPDP证书会更加契合一些。 PMP和NPDP有什么不同? 1、认证体系不同 PMP&#xff08;项目管理专业人士…

【继承】—— 我与C++的不解之缘(十九)

前言&#xff1a; 面向对象编程语言的三大特性&#xff1a;封装、继承和多态 本篇博客来学习C中的继承&#xff0c;加油&#xff01; 一、什么是继承&#xff1f; ​ 继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段&#xff0c;它允许我们在保持原有类…

OpenCV相机标定与3D重建(6)将3D物体点投影到2D图像平面上函数projectPoints()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::fisheye::projectPoints 是 OpenCV 库中用于鱼眼镜头模型的函数&#xff0c;它将3D物体点投影到2D图像平面上。这个函数对于模拟或者理解鱼眼…

自闭症全托管理:提供综合关怀的专业机构

在当今社会&#xff0c;随着对特殊儿童教育需求的日益增长&#xff0c;寻找一个能够提供全面、专业且充满爱心的全托管理机构&#xff0c;成为了许多自闭症、ADHD&#xff08;注意力缺陷多动障碍&#xff09;、谱系障碍、发育迟缓及注意力缺失等特殊儿童家庭的首要任务。星贝育…