自定义TextView实现结尾加载动画

最近做项目,仿豆包和机器人对话的时候,机器人返回数据是流式返回的,需要在文本结尾添加加载动画,于是自己实现了自定义TextView控件。

在这里插入图片描述

源码如下:

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.TypedValue
import androidx.annotation.Px
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.roundToIntclass LoadingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {var isLoading = trueset(value) {field = valueif (value) {startAnimation()} else {stopAnimation()}requestLayout()invalidate()}private lateinit var loadingDrawable: Drawableprivate var maxLineWidth: Float = 0finit {setLoadingDrawable(BallLoadingDrawable().also {it.color = Color.BLACK}, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 36f, context.resources.displayMetrics).toInt(), TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 22f, context.resources.displayMetrics).toInt())}fun setLoadingDrawable(drawable: Drawable, @Px width: Int, @Px height: Int) {loadingDrawable = drawableloadingDrawable.setBounds(0, 0, width, height)requestLayout()invalidate()}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)if (!isLoading) returnvar widthSize = MeasureSpec.getSize(widthMeasureSpec)var heightSize = MeasureSpec.getSize(heightMeasureSpec)val widthMode = MeasureSpec.getMode(widthMeasureSpec)val heightMode = MeasureSpec.getMode(heightMeasureSpec)layout?.apply {val loadingWidth = loadingDrawable.intrinsicWidthval loadingHeight = loadingDrawable.intrinsicHeightif (lineCount > 0) {val lastLine = lineCount - 1val top = getLineTop(0)val bottom = getLineBottom(lineCount - 1)val textHeight: Int = bottom - topfor (line in 0 until lineCount) {val width = getLineWidth(line)maxLineWidth = maxOf(maxLineWidth, width)}val end = getLineEnd(lastLine)val lastCharIndex = end - 1val lastCharX = getPrimaryHorizontal(lastCharIndex)if ((lastCharX + compoundDrawablePadding + loadingWidth) > maxWidth) {widthSize =(maxLineWidth.roundToInt() + compoundDrawablePadding + loadingWidth).coerceAtMost(maxWidth)heightSize = (loadingHeight + textHeight).coerceAtLeast(heightSize)} else {widthSize =(maxLineWidth.roundToInt() + compoundDrawablePadding + loadingWidth).coerceAtMost(maxWidth)heightSize = textHeight.coerceAtLeast(heightSize)}} else {widthSize = loadingWidthheightSize = loadingHeight}}setMeasuredDimension(MeasureSpec.makeMeasureSpec(widthSize, widthMode),MeasureSpec.makeMeasureSpec(heightSize, heightMode))}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)if (isLoading) {drawLoading(canvas)} else {stopAnimation()}}private fun drawLoading(canvas: Canvas) {startAnimation()layout?.apply {val loadingWidth = loadingDrawable.intrinsicWidthval loadingHeight = loadingDrawable.intrinsicHeightif (lineCount > 0) {val lastLine = lineCount - 1val end = getLineEnd(lastLine)val lastCharIndex = end - 1val lastCharX = getPrimaryHorizontal(lastCharIndex)val top = getLineTop(lastLine)val bottom = getLineBottom(lastLine)val translateX: Floatval translateY: Floatif (lastCharX + compoundDrawablePadding + loadingWidth > maxWidth) {translateX = 0ftranslateY = bottom.toFloat()} else {translateX = lastCharX + compoundDrawablePaddingtranslateY = (bottom + top - loadingHeight) / 2f}canvas.save()canvas.translate(translateX, translateY)loadingDrawable.draw(canvas)canvas.restore()}}}override fun onAttachedToWindow() {super.onAttachedToWindow()startAnimation()}override fun onDetachedFromWindow() {stopAnimation()super.onDetachedFromWindow()}private fun startAnimation() {if (!isLoading || visibility != VISIBLE) {return}if (loadingDrawable is Animatable) {(loadingDrawable as Animatable).start()postInvalidate()}}private fun stopAnimation() {if (loadingDrawable is Animatable) {(loadingDrawable as Animatable).stop()postInvalidate()}}
}

其中BallLoadingDrawable是自定义Drawable,也可以换成其他自定义的Drawable实现不一样的动画效果。

import android.animation.ValueAnimator
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawableclass BallLoadingDrawable : Drawable(), Animatable {private val scaleFloats = floatArrayOf(1.0f, 1.0f, 1.0f)private var animators: ArrayList<ValueAnimator>? = nullprivate var drawBounds = Rect()private val paint = Paint()var color: Int = Color.WHITEset(value) {field = valuepaint.color = colorinvalidateSelf()}init {paint.color = Color.WHITEpaint.style = Paint.Style.FILLpaint.isAntiAlias = true}override fun draw(canvas: Canvas) {val circleSpacing = 4fval radius = (getWidth().coerceAtMost(getHeight()) - circleSpacing * 2) / 6val x = getWidth() / 2 - (radius * 2 + circleSpacing)val y = (getHeight() / 2).toFloat()for (i in 0..2) {canvas.save()val translateX = x + radius * 2 * i + circleSpacing * icanvas.translate(translateX, y)canvas.scale(scaleFloats[i], scaleFloats[i])canvas.drawCircle(0f, 0f, radius, paint)canvas.restore()}}fun getWidth(): Int {return drawBounds.width()}fun getHeight(): Int {return drawBounds.height()}override fun setAlpha(alpha: Int) {}override fun setColorFilter(colorFilter: ColorFilter?) {}override fun getOpacity(): Int {return PixelFormat.OPAQUE}override fun start() {if (isStarted()) {return}if (animators.isNullOrEmpty()) {animators = arrayListOf()val delays = intArrayOf(120, 240, 360)for (i in 0..2) {val scaleAnim = ValueAnimator.ofFloat(1f, 0.3f, 1f)scaleAnim.setDuration(750)scaleAnim.repeatCount = -1scaleAnim.startDelay = delays[i].toLong()scaleAnim.addUpdateListener { animation ->scaleFloats[i] = animation.animatedValue as FloatinvalidateSelf()}animators!!.add(scaleAnim)}}animators?.forEach {it.start()}}override fun stop() {animators?.forEach {it.end()}}override fun isRunning(): Boolean {return animators?.any { it.isRunning } ?: false}private fun isStarted(): Boolean {return animators?.any { it.isStarted } ?: false}override fun onBoundsChange(bounds: Rect) {drawBounds = Rect(bounds.left, bounds.top, bounds.right, bounds.bottom)}override fun getIntrinsicHeight(): Int {return drawBounds.height()}override fun getIntrinsicWidth(): Int {return drawBounds.width()}
}

对应的布局文件为:

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".FirstFragment"><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:padding="16dp"><Buttonandroid:id="@+id/button_first"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/next"app:layout_constraintBottom_toTopOf="@id/textview_first"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><com.zhupeng.ai.pdf.gpt.LoadingTextViewandroid:id="@+id/textview_first"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:maxWidth="300dp"android:text="@string/lorem_ipsum"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/button_first" /></androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

注意:使用该控件必须设置android:maxWidth属性

感谢大家的支持,如有错误请指正,如需转载请标明原文出处!

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

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

相关文章

基于云原生向量数据库 PieCloudVector 的 RAG 实践

近年来&#xff0c;人工智能生成内容&#xff08;AIGC&#xff09;已然成为最热门的话题之一。工业界出现了各种内容生成工具&#xff0c;能够跨多种模态产生多样化的内容。这些主流的模型能够取得卓越表现&#xff0c;归功于创新的算法、模型规模的大幅扩展&#xff0c;以及海…

用Boot写mybatis的增删改查

一、总览 项目结构&#xff1a; 图一 1、JavaBean文件 2、数据库操作 3、Java测试 4、SpringBoot启动类 5、SpringBoot数据库配置 二、配置数据库 在项目资源包中新建名为application.yml的文件&#xff0c;如图一。 建好文件我们就要开始写…

【MySQL00】【 杂七杂八】

文章目录 一、前言二、MySQL 文件1. 参数文件2. 日志文件3. 套接字文件4. pid 文件5. 表结构定义文件6. InnoDB 存储引擎文件 二、BTree 索引排序三、InnoDB 关键特性1. 插入缓冲1.1 Insert Buffer 和 Change Buffer1.1 缓冲合并 2. 两次写2. 自适应哈希索引3. 异步IO4. 刷新邻…

江协科技STM32学习- P9 OLED调试工具

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

# VMware 共享文件

VMware tools快速安装 VMware 提供了 open-vm-tools&#xff0c;这是 VMware 官方推荐的开源工具包&#xff0c;通常不需要手动安装 VMware Tools&#xff0c;因为大多数 Linux 发行版&#xff08;包括 Ubuntu、CentOS 等&#xff09;都包含了 open-vm-tools&#xff0c;并且已…

Linux网络编程IO管理

网络 IO 涉及到两个系统对象&#xff0c;一个是用户空间调用 IO 的进程或者线程&#xff0c;一个是内核空间的内核系统&#xff0c;比如发生 IO 操作 read 时&#xff0c;它会经历两个阶段&#xff1a; 等待内核协议栈的数据准备就绪&#xff1b;将内核中的数据拷贝到用户态的…

Kafka【八】如何保证消息发送的可靠性、重复性、有序性

【1】消息发送的可靠性保证 对于生产者发送的数据&#xff0c;我们有的时候是不关心数据是否已经发送成功的&#xff0c;我们只要发送就可以了。在这种场景中&#xff0c;消息可能会因为某些故障或问题导致丢失&#xff0c;我们将这种情况称之为消息不可靠。虽然消息数据可能会…

Spring框架基础介绍2.0

目录 AOP概述 面向切面思想 优点&#xff1a; 核心原理&#xff1a; 使用案例: AOP 的基本概念 springAOP 实现 AspectJ 中常用的通知 Spring事物管理 数据库事务管理? spring 事务管理? Spring中的事物管理分为两种形式&#xff1a; 1、编程式事物管理 2、声明…

低空经济如此火爆,新手如何分一杯羹?

低空经济的火爆为新手提供了诸多参与和分一杯羹的机会。以下是一些具体的建议&#xff0c;帮助新手在这一领域找到切入点&#xff1a; 1. 了解行业概况与趋势 定义与范围&#xff1a;低空经济是指在3000米以下空域内进行各种有人和无人驾驶航空器活动的经济形态&#xff0c;涉…

dubbo的SPI机制

一.dubbo的SPI机制 SPI机制是一个服务发现机制&#xff0c;通过接口的全限定名找到指定目录下对应的文件&#xff0c;然后加载对应的实现类注册到系统中进行使用。 在Java原生跟mysql的驱动加载也使用了这个机制&#xff0c;但是他们只能进行全部实现类的加载&#xff08;遍历…

最新HTML5中的文件详解

第5章 HTML5中的文件 5.1选择文件 可以创建一个file类型的input,添加multiple属性为true,可以实现多个文件上传。 5.1.1 选择单个文件 1.功能描述 创建file类型input元素&#xff0c;页面中不再有文本框&#xff0c;而是 选择文件 按钮&#xff0c;右侧是上次文件的名称&a…

数据分析面试题:客户投保问题分析

目录 0 场景描述 1 数据准备 2 问题分析 2.1 计算小微公司的平均经营时长 2.2 计算小微公司且角色为投保人,保险起期在18年的总保费 2.3 假设,DWD_CUSTOMER_REL客户关联关系表中,存在部分客户保单数很多,部分客户保单数很少的情况,此时DWD_CUSTOMER_BASE表关联,程序…

百度智能云向量数据库创新和应用实践分享

本文整理自第 15 届中国数据库技术大会 DTCC 2024 演讲《百度智能云向量数据库创新和应用实践分享》 在 IT 行业&#xff0c;数据库有超过 70 年的历史了。对于快速发展的 IT 行业来说&#xff0c;一个超过 70 年历史的技术&#xff0c;感觉像恐龙一样&#xff0c;非常稀有和少…

Anaconda Prompt 安装paddle2.6报错

bug描述 python 3.11.9 通过 pip install paddlepaddle2.6.1 安装后&#xff0c;运行 paddle.utils.run_check() 则出现下面的错误&#xff1a; 解决办法 方法一&#xff1a;使用paddle 3的版本 这里要注意我的python版本 方法二&#xff1a;使用低版本的python python3.9…

Lombok jar包引入和用法

大家好&#xff0c;今天分享一个在编写代码时的快捷方法。 当我们在封装实体类时&#xff0c;会使用set、get等一些方法。如下图&#xff0c;不但费事还影响代码的美观。 那么如何才能减少代码的冗余呢&#xff0c;首先lib中导入lombok的jar包并添加库。 此处我已导入&#xf…

Jenkins+Svn+Vue自动化构建部署前端项目(保姆级图文教程)

目录 介绍 准备工作 配置jenkins 构建部署任务 常见问题 介绍 在平常开发前端vue项目时,我们通常需要将vue项目进行打包构建,将打包好的dist目录下的静态文件上传到服务器上,但是这种繁琐的操作是比较浪费时间的,可以使用jenkins进行自动化构建部署前端vue 准备工作 准备…

《粮食科技与经济》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答 问&#xff1a;《粮食科技与经济》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的第一批认定学术期刊。 问&#xff1a;《粮食科技与经济》级别&#xff1f; 答&#xff1a;省级。主管单位&#xff1a; 湖南省粮食和物资储备局 …

bat批处理实现从特定文件夹中提取文件内容并以父文件夹名存储

1、需求分析 标题是bat批处理实现从特定文件夹中提取文件内容并以父文件夹名存储。这里面我们要做的工作是&#xff1a; ①、批处理脚本使用的是bat文件&#xff1b; ②、文件夹下面有很多子文件夹&#xff0c;然后子文件夹下仍然有相同的文件结构&#xff0c;我们需要从三级…

halcon 自定义距离10的一阶导数幅图,摆脱sobel的3掩码困境

一&#xff0c;为什么要摆脱3的掩码 在处理图像的过程中&#xff0c;会用到平滑算子&#xff0c;很容易破坏边际&#xff0c;所谓的一阶导数sobel只计算掩码为3的差分&#xff0c;在幅度图分割中&#xff0c;往往是很难把握的。 举个例子-现在图像头平滑好了&#xff0c;缺陷…

模具要不要建设3D打印中心

随着3D打印技术的日益成熟与广泛应用&#xff0c;模具企业迎来了自建3D打印中心的热潮。这一举措不仅为企业带来了前所未有的发展机遇&#xff0c;同时也伴随着一系列需要克服的挑战&#xff0c;如何看待企业引进增材制造&#xff0c;小编为您全面分析。 机遇篇&#xff1a; 加…