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

embedded/2025/1/16 3:35:27/

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

在这里插入图片描述

源码如下:

kotlin">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实现不一样的动画效果。

kotlin">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.ppmy.cn/embedded/108407.html

相关文章

Css:属性选择器、关系选择器及伪元素

css的属性选择器&#xff1a; 注&#xff1a;属性值只能由数字&#xff0c;字母&#xff0c;下划线&#xff0c;中划线组成&#xff0c;并且不能以数字开头。 1、[属性] 选择含有指定属性的元素&#xff0c;用[]中括号表示。 <style> /*注意大小写区分 注意前后顺序 样…

[情商-13]:语言的艺术:何为真实和真相,所谓真相,就是别人想让你知道的真相!洞察谎言与真相!

目录 前言&#xff1a; 一、说话的真实程度分级 二、说谎动机分级&#xff1a;善意谎言、中性谎言、恶意谎言 三、小心&#xff1a;所谓真相&#xff1a;只说对自己有利的真相 四、小心&#xff1a;所谓真相&#xff1a;就是别人想让你知道的真相 五、小心&#xff1a;所…

8.分布式服务部署

文章目录 1.分布式服务部署1.1服务器个数1.2 ubuntu 的 MySQL 安装1.3对其他服务器授权1.4Java服务部署1.5常见问题 大家好&#xff0c;我是晓星航。今天为大家带来的是 分布式服务部署 相关的讲解&#xff01;&#x1f600; 1.分布式服务部署 1.1服务器个数 机器个数 1 - N…

Superset二次开发之Select 筛选器源码分析

路径&#xff1a;superset-frontend/src/filters/components/Select 源码文件&#xff1a; 功能点&#xff1a; 作用 交互 功能 index.ts作为模块的入口点,导出其他文件中定义的主要组件和函数。它使其他文件中的导出可以被外部模块使用。 SelectFilterPlugin.tsx 定义主要…

Datawhale X 李宏毅苹果书 AI夏令营(深度学习 之 实践方法论)

1、模型偏差 模型偏差是指的是模型预测结果与真实值之间的差异&#xff0c;这种差异不是由随机因素引起的&#xff0c;而是由模型本身的局限性或训练数据的特性所导致的。 简单来讲&#xff0c;就是由于初期设定模型&#xff0c;给定的模型计算能力过弱&#xff0c;导致在通过…

Python教程:使用 Python 和 PyHive 连接 Hive 数据库

目录 1. 引言 2. 类的设计思路 2.1 类的基本结构 3. 连接到 Hive 3.1 连接方法 4. 执行查询 4.1 查询返回 DataFrame 4.2 查询返回列表 5. 基本的数据库操作 5.1 创建表 5.2 插入数据 5.3 更新数据 5.4 删除数据 6. 表的描述信息和数据库操作 6.1 获取表描述 6…

设计模式 19 观察者模式

设计模式 19 创建型模式&#xff08;5&#xff09;&#xff1a;工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式结构型模式&#xff08;7&#xff09;&#xff1a;适配器模式、桥接模式、组合模式、装饰者模式、外观模式、享元模式、代理模式行为型模式&#xff…

JVM3-双亲委派机制

目录 概述 作用 如何指定加载类的类加载器&#xff1f; 面试题 打破双亲委派机制 自定义类加载器 线程上下文类加载器 Osgi框架的类加载器 概述 由于Java虚拟机中有多个类加载器&#xff0c;双亲委派机制的核心是解决一个类到底由谁加载的问题 双亲委派机制&#xff…