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"
/>