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;理解这一点至关重要。…

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

多方法做配对样本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…

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…

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

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

安卓-碎片的使用入门

1.碎片(Fragment)是什么 Fragment是依赖于Activity的&#xff0c;不能独立存在的,是Activity界面中的一部分&#xff0c;可理解为模块化的Activity,它能让程序更加合理和充分地利用大屏幕的空间&#xff0c;因而在平板上应用得非常广泛. Fragment不能独立存在&#xff0c;必须…

WRF-Chem模式安装、环境配置、原理、调试、运行方法;数据准备及相关参数设置方法

大气污染是工农业生产、生活、交通、城市化等方面人为活动的综合结果&#xff0c;同时气象因素是控制大气污染的关键自然因素。大气污染问题既是局部、当地的&#xff0c;也是区域的&#xff0c;甚至是全球的。本地的污染物排放除了对当地造成严重影响外&#xff0c;同时还会在…

Admin.NET框架使用宝塔面板部署步骤

文章目录 Admin.NET框架使用宝塔面板部署步骤&#x1f381;框架介绍部署步骤1.Centos7 部署宝塔面板2.部署Admin.NET后端3.部署前端Web4.访问前端页面 Admin.NET框架使用宝塔面板部署步骤 &#x1f381;框架介绍 Admin.NET 是基于 .NET6 (Furion/SqlSugar) 实现的通用权限开发…

.net —— Razor

文章目录 项目地址一、创建一个Razor项目1.1 创建项目1.2 创建项目所需文件夹1.3 配置项目二、创建Category页面2.1 创建Category的展示页面2.2 增删改2.2.1 创建Edit的razor视图项目地址 教程作者:教程地址:代码仓库地址:所用到的框架和插件:dbt airflow一、创建一个Razo…

C语言——指针初阶(三)

目录 一.指针-指针 代码1&#xff1a; 运行结果&#xff1a; 代码2&#xff1a; 运行结果&#xff1a; 代码3&#xff1a; 运行结果&#xff1a; 二.指针数组 例&#xff1a; 往期回顾 一.指针-指针 指针减去指针的前提&#xff1a;两个指针指向同一块空间。 指针减去指针…

spring boot2.7集成OpenFeign 3.1.7

1.Feign Feign是一个声明式web服务客户端。它使编写web服务客户端更容易。要使用Feign&#xff0c;请创建一个接口并对其进行注释。它具有可插入注释支持&#xff0c;包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。Spring Cloud增加了对Spring MVC注释的支持&…

大R玩家流失预测在休闲社交游戏中的应用

摘要 预测玩家何时会离开游戏为延长玩家生命周期和增加收入贡献创造了独特的机会。玩家可以被激励留下来&#xff0c;战略性地与公司组合中的其他游戏交叉链接&#xff0c;或者作为最后的手段&#xff0c;通过游戏内广告传递给其他公司。本文重点预测休闲社交游戏中高价值玩家…

【linux学习指南】Linux进程信号产生(三) 硬件异常除零出错?野指针异常?core文件

文章目录 &#x1f4dd;前言&#x1f320;模拟除0&#x1f309;除0出错&#xff1f;&#x1f309;野指针异常? &#x1f320;⼦进程退出coredump&#x1f309;Core Dump &#x1f6a9;总结 &#x1f4dd;前言 硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前…

计算机毕业设计Python异常流量检测 流量分类 流量分析 网络流量分析与可视化系统 网络安全 信息安全 机器学习 深度学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…