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

news2024/9/20 20:34:26

最近做项目,仿豆包和机器人对话的时候,机器人返回数据是流式返回的,需要在文本结尾添加加载动画,于是自己实现了自定义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.roundToInt

class LoadingTextView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    var isLoading = true
        set(value) {
            field = value
            if (value) {
                startAnimation()
            } else {
                stopAnimation()
            }
            requestLayout()
            invalidate()
        }

    private lateinit var loadingDrawable: Drawable

    private var maxLineWidth: Float = 0f

    init {
        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 = drawable
        loadingDrawable.setBounds(0, 0, width, height)
        requestLayout()
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        if (!isLoading) return

        var widthSize = MeasureSpec.getSize(widthMeasureSpec)
        var heightSize = MeasureSpec.getSize(heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        layout?.apply {
            val loadingWidth = loadingDrawable.intrinsicWidth
            val loadingHeight = loadingDrawable.intrinsicHeight
            if (lineCount > 0) {
                val lastLine = lineCount - 1
                val top = getLineTop(0)
                val bottom = getLineBottom(lineCount - 1)
                val textHeight: Int = bottom - top
                for (line in 0 until lineCount) {
                    val width = getLineWidth(line)
                    maxLineWidth = maxOf(maxLineWidth, width)
                }
                val end = getLineEnd(lastLine)
                val lastCharIndex = end - 1
                val 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 = loadingWidth
                heightSize = 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.intrinsicWidth
            val loadingHeight = loadingDrawable.intrinsicHeight
            if (lineCount > 0) {
                val lastLine = lineCount - 1
                val end = getLineEnd(lastLine)
                val lastCharIndex = end - 1
                val lastCharX = getPrimaryHorizontal(lastCharIndex)
                val top = getLineTop(lastLine)
                val bottom = getLineBottom(lastLine)
                val translateX: Float
                val translateY: Float
                if (lastCharX + compoundDrawablePadding + loadingWidth > maxWidth) {
                    translateX = 0f
                    translateY = bottom.toFloat()
                } else {
                    translateX = lastCharX + compoundDrawablePadding
                    translateY = (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.Drawable

class BallLoadingDrawable : Drawable(), Animatable {

    private val scaleFloats = floatArrayOf(
        1.0f, 1.0f, 1.0f
    )

    private var animators: ArrayList<ValueAnimator>? = null
    private var drawBounds = Rect()
    private val paint = Paint()

    var color: Int = Color.WHITE
        set(value) {
            field = value
            paint.color = color
            invalidateSelf()
        }

    init {
        paint.color = Color.WHITE
        paint.style = Paint.Style.FILL
        paint.isAntiAlias = true
    }

    override fun draw(canvas: Canvas) {
        val circleSpacing = 4f
        val radius = (getWidth().coerceAtMost(getHeight()) - circleSpacing * 2) / 6
        val 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 * i
            canvas.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 = -1
                scaleAnim.startDelay = delays[i].toLong()
                scaleAnim.addUpdateListener { animation ->
                    scaleFloats[i] = animation.animatedValue as Float
                    invalidateSelf()
                }
                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.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp">

        <Button
            android: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.LoadingTextView
            android: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.coloradmin.cn/o/2114218.html

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

相关文章

Java小程序案例:电子日历记事本

要点 菜单栏中提供编辑&#xff08;剪切、复制、粘贴&#xff09;、保存、打开等功能。使用类组件实现图形界面设计。基于图形界面的日历&#xff0c;用户可编辑或查看指定日期的日志内容。提供按钮实现月份的前后翻动。事件持久化到文件&#xff0c;可再次编辑保存 效果 程序…

【工具】使用 Jackson 实现优雅的 JSON 格式化输出

说明 在 Java 开发中&#xff0c;我们经常需要处理 JSON 数据。无论是从服务器端返回的数据&#xff0c;还是本地存储的数据&#xff0c;JSON 格式都因其轻量级和易于解析的特点而被广泛使用。当我们需要查看或调试 JSON 数据时&#xff0c;优雅、格式化的输出将大大提高我们的…

风控系统之指标回溯,历史数据重跑

个人博客&#xff1a;无奈何杨&#xff08;wnhyang&#xff09; 个人语雀&#xff1a;wnhyang 共享语雀&#xff1a;在线知识共享 Github&#xff1a;wnhyang - Overview 回顾 默认你已经看过之前那篇风控系统指标计算/特征提取分析与实现01&#xff0c;Redis、Zset、模版方…

C++万字解读类和对象(上)

1.类的定义 class为定义类的关键字&#xff0c;Stack为类的名字&#xff0c;{}中为类的主体&#xff0c;注意类定义结束时后面分号不能省略。类体中内容称为类的成员&#xff1a;类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。 为了区分成员变量&…

SprinBoot+Vue应急信息管理系统的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质…

远心镜头选型公式

在当今的机器视觉领域&#xff0c;远心镜头凭借其独特的远心光路设计以及超低畸变、高远心度和高景深等特点&#xff0c;成为尺寸测量和视觉对位中的得力工具。然而&#xff0c;如何进行快速而准确的选型呢&#xff1f;答案就在于选型公式&#xff1a;倍率 焦距 N.A.Sensor 尺…

移动端视频编辑SDK解决方案,AI语音识别添加字幕

对于众多Vlog创作者而言&#xff0c;繁琐的字幕添加过程往往成为提升内容质量的绊脚石。为了彻底改变这一现状&#xff0c;美摄科技凭借其深厚的AI技术积累与创新的移动端视频编辑SDK解决方案&#xff0c;推出了革命性的AI语音识别添加字幕功能&#xff0c;让视频创作更加高效、…

再论大模型做内容消费

前言 今天咱们再体验几个有关大模型聚集内容创作的工作。 之前也专门介绍过一些大模型在该方面的工作&#xff0c;感兴趣的小伙伴可以穿梭&#xff1a;《提升大模型内容创作能力》&#xff1a;https://zhuanlan.zhihu.com/p/716240950 废话不多说&#xff0c;一起来看看吧&a…

【技术调研】三维(0)-webGL、三维基础知识、前置知识、数学知识以及简单示例

前言 ​ 因业务需要了解网页端三维相关技术,故对webGL相关技术学习并记录。旨在了解网页端三维的基本原理。 什么是webGL? ​ WebGL全称叫做Web Graphics Library,它是JavaScript API。用于在任何兼容的Web浏览器中渲染交互式的3D图形,并且无需使用插件。它基于 OpenGL …

参会邀请 | 第二届机器视觉、图像处理与影像技术国际会议(MVIPIT 2024)

第二届机器视觉、图像处理与影像技术国际会议&#xff08;MVIPIT 2024&#xff09;将于2024年9月13日-15日在中国张家口召开。 MVIPIT 2024聚焦机器视觉、图像处理与影像技术&#xff0c;旨在为专家、学者和研究人员提供一个国际平台&#xff0c;分享研究成果&#xff0c;讨论…

上海大学《2022年836+915自动控制原理真题及答案》 (完整版)

Part1&#xff1a;2022年上海大学真题题目 学硕836 专硕915 Part2&#xff1a;2022年上海大学真题答案 学硕836 专硕915

Linux学习之路 -- 线程概念

本文主要介绍线程的相关概念与基础控制 什么是线程&&线程的相关知识 按照书本上的定义&#xff0c;线程就是进程内部的一个执行分支&#xff0c;而线程是cpu调度的基本单位。 如果直接按照书本上的定义理解&#xff0c;线程这个概念是比较模糊的&#xff0c;而且我们…

013.Python爬虫系列_re正则解析

我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈 入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈 虚 拟 环 境 搭 建 :👉👉 Python项目虚拟环境(超详细讲解) 👈👈 PyQt5 系 列 教 程:👉👉 Python GUI(PyQt5)文章合集 👈👈 Oracle数…

SAP学习笔记 - 开发01 - BAPI是什么?通过界面和ABAP代码来调用BAPI

BAPI作为SAP中的重要概念&#xff0c;在SAP系统的开发中几乎是必须的。 本章来学习一下BAPI 的直观印象&#xff0c;以及在ABAP代码中的调用。 目录 1&#xff0c; BAPI概述 1&#xff0c;从画面角度来直观体验一下BAPI 1-1&#xff0c;MM&#xff1a;購買依頼変更BAPI - …

日志服务管理

系统日志管理 sysklogd 系统日志服务 在 CentOS5 以及之前的发行版中&#xff0c;其采用的 sysklogd 服务来记录和管理系统日志的。 sysklogd 服务有两个模块&#xff1a; klogd&#xff1a; 用于记录 linux kernel 相关的日志 syslogd&#xff1a;用于记录用户空间应用日志…

OCC开发_箱梁梁体建模

概述 OCC(全称OpenCascade)是一个近年来比较受欢迎的开源三维CAD建模平台&#xff0c;曲线、曲面、实体、渲染等方面功能强大&#xff0c;并且在机械、航空、船舶等许多领域应用广泛。基于OCC的强大功能考虑&#xff0c;本人尝试将其引入桥梁领域。桥梁设计中&#xff0c;比较常…

Nginx核心配置文件结构

一、简单介绍 源码安装的Nginx的核心配置文件默认是放在/usr/local/nginx/conf/nginx.conf yum安装的Nginx的核心配置文件默认是放在/etc/nginx/nginx.conf 使用命令&#xff1a;nginx -t&#xff0c;可以检查测试nginx的配置文件&#xff08;nginx.conf&#xff09;语法是否…

6.1排序——插入排序与希尔排序

本篇博客来梳理两种常见排序算法&#xff1a;插入排序与希尔排序 常见的排序算法如图 写排序算法的原则&#xff1a;先写单趟&#xff0c;再写整体 一、直接插入排序 1&#xff0e;算法思想 先假定第一个数据有序&#xff0c;把第二个数据插入&#xff1b;再假设前两个数据…

iOS剪贴板同步到Windows剪贴板(无需安装软件的方案)

摘要 剪贴板同步能够提高很多的效率&#xff0c;免去复制、发送、复制、粘贴的步骤&#xff0c;只需要在手机上复制&#xff0c;就可以直接在电脑上 ctrlv 粘贴&#xff0c;这方面在 Apple 设备中是做的非常好的&#xff0c;Apple 设备之间的剪贴板同步功能&#xff08;Univer…

2024整理 iptables防火墙学习笔记大全_modepro iptables

Iptables名词和术语 2iptables表&#xff08;tables&#xff09;和链&#xff08;chains&#xff09; 2表及其链的功能 2  Filter表 2  NAT表 2  MANGLE表 2iptables的工作流程 3iptables表和链的工作流程图 3 二、 iptables实战应用 4iptables命令参数详解 4  iptable…