Android 自定义View-圆圈扩散动画

UX妹妹不想搞gif图,苦逼程序员无奈只能自定义view实现,要求:View中央放置一张图片,有圆形动画从中央图片向四周扩散,圆形动画半径越大颜色越浅,中央图片/圆圈颜色/圆圈层数/间隔可以自定义

效果展示

动画录制不方便 静态效果:

/res/values/attrs.xml自定义属性:

自定义属性说明:

spreadColor - 设置扩散圆圈的颜色(示例中使用红色 #FF6B6B)

centerImage - 设置中央显示的图片(示例中使用应用图标)

circleInterval - 设置圆圈之间的间隔(示例中设置为80dp)

maxAlpha - 设置最大透明度值(示例中设置为200)

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleSpreadView">
        <attr name="spreadColor" format="color" />
        <attr name="centerImage" format="reference" />
        <attr name="circleInterval" format="dimension" />
        <attr name="maxAlpha" format="integer" />
        <attr name="maxCircles" format="integer" />
    </declare-styleable>
</resources>

自定义View实现:

添加了动画控制功能:

startAnimation() - 开始动画

pauseAnimation() - 暂停动画

toggleAnimation() - 切换动画状态(播放/暂停)

isAnimationRunning() - 检查动画是否正在运行

实现了动画状态管理:

添加了 isAnimationRunning 标志来跟踪动画状态

在动画循环中检查此标志以确保只在需要时运行

Kotlin 复制代码
/**
 * 圆形扩散视图
 * 中央放置一张图片,有圆形动画从中央图片向四周扩散,圆形动画半径越大颜色越浅
 */
class CircleSpreadView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val circles = mutableListOf<CircleInfo>()
    private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var centerImage: Drawable? = null
    private var spreadColor = Color.BLUE
    private var maxRadius = 0f
    private var circleInterval = 100f // 圆圈之间的间隔
    private var maxAlpha = 255 // 最大透明度
    private var maxCircles = 5 // 最大同时存在的圆圈数量
    
    private var centerX = 0f
    private var centerY = 0f
    
    private var animationRunnable: Runnable? = null
    private var isAnimationRunning = false
    
    init {
        // 读取自定义属性
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleSpreadView)
        try {
            spreadColor = typedArray.getColor(R.styleable.CircleSpreadView_spreadColor, Color.BLUE)
            val imageResource = typedArray.getResourceId(R.styleable.CircleSpreadView_centerImage, -1)
            if (imageResource != -1) {
                centerImage = ContextCompat.getDrawable(context, imageResource)
            }
            circleInterval = typedArray.getDimension(R.styleable.CircleSpreadView_circleInterval, 100f)
            maxAlpha = typedArray.getInt(R.styleable.CircleSpreadView_maxAlpha, 255)
            maxCircles = typedArray.getInt(R.styleable.CircleSpreadView_maxCircles, 5)
        } finally {
            typedArray.recycle()
        }
        
        circlePaint.style = Paint.Style.STROKE
        circlePaint.color = spreadColor
        circlePaint.strokeWidth = 5f
        
        // 开始动画
        startAnimation()
    }
    
    /**
     * 设置扩散圆圈的颜色
     */
    fun setSpreadColor(color: Int) {
        spreadColor = color
        circlePaint.color = color
        invalidate()
    }
    
    /**
     * 设置中心图片
     */
    fun setCenterImage(drawable: Drawable?) {
        centerImage = drawable
        invalidate()
    }
    
    /**
     * 设置中心图片资源
     */
    fun setCenterImageResource(resourceId: Int) {
        centerImage = ContextCompat.getDrawable(context, resourceId)
        invalidate()
    }
    
    /**
     * 开始动画
     */
    fun startAnimation() {
        if (!isAnimationRunning) {
            isAnimationRunning = true
            animationRunnable = object : Runnable {
                override fun run() {
                    if (isAnimationRunning) {
                        // 更新圆圈信息
                        updateCircles()
                        // 添加新的圆圈(如果需要)
                        addNewCircleIfNeeded()
                        // 重绘
                        invalidate()
                        // 继续动画 - 使用固定的时间间隔来控制帧率,避免过度重绘
                        postDelayed(this, 50)
                    }
                }
            }
            post(animationRunnable!!)
        }
    }
    
    /**
     * 暂停动画
     */
    fun pauseAnimation() {
        isAnimationRunning = false
        animationRunnable?.let { removeCallbacks(it) }
    }
    
    /**
     * 切换动画状态
     */
    fun toggleAnimation() {
        if (isAnimationRunning) {
            pauseAnimation()
        } else {
            startAnimation()
        }
    }
    
    /**
     * 检查动画是否正在运行
     */
    fun isAnimationRunning(): Boolean {
        return isAnimationRunning
    }
    
    /**
     * 设置最大圆圈数
     */
    fun setMaxCircles(max: Int) {
        maxCircles = max
    }
    
    /**
     * 获取最大圆圈数
     */
    fun getMaxCircles(): Int {
        return maxCircles
    }
    
    /**
     * 更新圆圈信息
     */
    private fun updateCircles() {
        // 增加每个圆圈的半径
        circles.forEach { it.radius += 2f } // 半径增长速度
        
        // 移除超出最大半径的圆圈
        circles.removeAll { it.radius > maxRadius}
    }
    
    /**
     * 如果需要则添加新圆圈
     */
    private fun addNewCircleIfNeeded() {
        // 如果没有圆圈或者最新圆圈与上一个圆圈间距足够,并且未超过最大圆圈数,则添加新圆圈
        if ((circles.isEmpty() || (circles.last().radius >= circleInterval)) && circles.size < maxCircles) {
            circles.add(CircleInfo(0f))
        }
    }
    
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        centerX = w / 2f
        centerY = h / 2f
        maxRadius = Math.min(w, h) / 2f  // 最大半径
    }
    
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        
        // 绘制扩散圆圈
        circles.forEach { circle ->
            val normalizedRadius = circle.radius / maxRadius
            // 根据半径计算透明度,半径越大透明度越低
            val alpha = (maxAlpha * (1f - normalizedRadius)).toInt().coerceIn(0, maxAlpha)
            
            circlePaint.alpha = alpha
            circlePaint.strokeWidth = 5f * (1f - normalizedRadius * 0.5f) // 随着半径增大,线条变细
            
            canvas.drawCircle(centerX, centerY, circle.radius, circlePaint)
        }
        
        // 绘制中心图片
        centerImage?.let { drawable ->
            val drawableWidth = drawable.intrinsicWidth
            val drawableHeight = drawable.intrinsicHeight
            
            // 计算居中位置
            val left = centerX - drawableWidth / 2f
            val top = centerY - drawableHeight / 2f
            val right = centerX + drawableWidth / 2f
            val bottom = centerY + drawableHeight / 2f
            
            drawable.setBounds(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
            drawable.draw(canvas)
        }
    }
    
    /**
     * 圆圈信息类
     */
    private data class CircleInfo(var radius: Float)
    
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // 停止动画
        animationRunnable?.let { removeCallbacks(it) }
    }
}

使用示例:

XML 复制代码
    <com.xxx.xxx.CircleSpreadView
            android:id="@+id/cs_anim"
            android:layout_width="285.5dp"
            android:layout_height="285.5dp"
            app:centerImage="@drawable/ic_phone"
            app:spreadColor="#290099FF"
            app:circleInterval="20dp"
            app:maxAlpha="200"
    />
相关推荐
stevenzqzq10 小时前
android启动和注入理解1
android
qq_7174100110 小时前
修改飞行模式
android
Larry_Yanan10 小时前
Qt安卓开发(一)Qt6.10环境配置
android·开发语言·c++·qt·学习·ui
冬奇Lab10 小时前
稳定性性能系列之十——卡顿问题分析:从掉帧到流畅体验
android·性能优化
stevenzqzq11 小时前
android启动初始化和注入理解2
android
DOUBLEDdinosaur11 小时前
屏幕数字监控 + 警报
android
M00668811 小时前
低代码平台使用留存的技术基础与系统设计逻辑
android·rxjava
nono牛11 小时前
深入理解gatekeeperd 与 android.hardware.gatekeeper@1.0-service调用规则
android
lxysbly11 小时前
红白机模拟器安卓版带金手指
android