Android 实现一个爱心形状 并且爱心装满液体的样式,液体需要持续晃动的效果,可以根据进度控制当前瓶中装满液体的进度
一、自定义view实现效果
package com.example.testdemo
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import androidx.core.animation.doOnEnd
import com.qbb.cqdazong.direr.appsharelib.utils.DensityUtil
import kotlin.math.sin
class LiquidHeartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 视图尺寸
private var viewWidth = 0f
private var viewHeight = 0f
// 爱心相关参数
private var heartSize = 0f
private var heartPath = Path()
private var heartBounds = RectF()
// 液体相关参数
private var liquidLevel = 0.5f // 液体高度比例 (0.0-1.0)
private var waveOffset = 0f // 波浪偏移量
private var waveAmplitude = 0f // 波浪振幅
private var waveLength = 0f // 波长
private var liquidPath = Path()
private var liquidPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#FF6767")
}
// 爱心背景画笔
private var heartPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.TRANSPARENT
}
// 边框画笔
private var borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#FFE8E8")
style = Paint.Style.STROKE
strokeWidth = DensityUtil.dip2px(context, 1.35.toFloat()).toFloat()
}
// 波浪动画
private var waveAnimator: ValueAnimator? = null
init {
// 加载自定义属性
context.obtainStyledAttributes(attrs, R.styleable.LiquidHeartView).apply {
liquidLevel = getFloat(R.styleable.LiquidHeartView_liquidLevel, 0f)
recycle()
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w.toFloat()
viewHeight = h.toFloat()
// 计算爱心尺寸 (略小于视图)
heartSize = minOf(viewWidth, viewHeight) * 0.93f
// 计算波浪参数
waveAmplitude = heartSize * 0.02f
waveLength = heartSize * 1.2f
// 初始化爱心路径
createHeartPath()
// 开始波浪动画
startWaveAnimation()
}
private fun createHeartPath() {
heartPath.reset()
// 计算爱心控制点和终点
val centerX = viewWidth / 2
val centerY = viewHeight / 2
// 绘制爱心
heartPath.moveTo(centerX, centerY - heartSize * 0.4f) // 顶部顶点
// 左上部曲线
heartPath.cubicTo(
centerX - heartSize * 0.5f, centerY - heartSize * 0.8f,
centerX - heartSize * 0.9f, centerY - heartSize * 0.2f,
centerX, centerY + heartSize * 0.53f
)
// 右上部曲线
heartPath.cubicTo(
centerX + heartSize * 0.9f, centerY - heartSize * 0.2f,
centerX + heartSize * 0.5f, centerY - heartSize * 0.8f,
centerX, centerY - heartSize * 0.4f
)
heartPath.close()
// 计算爱心边界
heartPath.computeBounds(heartBounds, true)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制爱心背景
canvas.drawPath(heartPath, heartPaint)
// 绘制液体
drawLiquid(canvas)
// 绘制爱心边框
canvas.drawPath(heartPath, borderPaint)
}
private fun drawLiquid(canvas: Canvas) {
liquidPath.reset()
// 计算液体高度
val liquidHeight = heartBounds.height() * liquidLevel * 0.8f
val liquidTop = heartBounds.bottom - liquidHeight
// 创建波浪路径
val startX = heartBounds.left
val endX = heartBounds.right
liquidPath.moveTo(startX, liquidTop)
// 绘制波浪线
for (x in startX.toInt()..endX.toInt()) {
// 使用正弦函数计算波浪的Y坐标
val y = liquidTop + waveAmplitude * sin((x + waveOffset) * 2 * Math.PI / waveLength)
liquidPath.lineTo(x.toFloat(), y.toFloat())
}
// 闭合路径形成液体区域
liquidPath.lineTo(endX, heartBounds.bottom)
liquidPath.lineTo(startX, heartBounds.bottom)
liquidPath.close()
// 裁剪画布为爱心形状,确保液体不溢出
canvas.save()
canvas.clipPath(heartPath)
canvas.drawPath(liquidPath, liquidPaint)
canvas.restore()
}
private fun startWaveAnimation() {
// 停止之前的动画
waveAnimator?.cancel()
// 创建波浪动画
waveAnimator = ValueAnimator.ofFloat(0f, waveLength).apply {
duration = 2000
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { animation ->
waveOffset = animation.animatedValue as Float
invalidate() // 重绘视图
}
start()
}
}
/**
* 设置液体高度
* @param level 液体高度比例 (0.0-1.0)
*/
fun setLiquidLevel(level: Float, animate: Boolean = true) {
val targetLevel = level.coerceIn(0f, 1f)
if (!animate) {
liquidLevel = targetLevel
invalidate()
return
}
// 创建动画过渡到目标高度
ValueAnimator.ofFloat(liquidLevel, targetLevel).apply {
duration = 500
addUpdateListener { animation ->
liquidLevel = animation.animatedValue as Float
invalidate()
}
doOnEnd {
// 动画结束后调整波浪振幅,使液体静止
if (targetLevel == 0f || targetLevel == 1f) {
ValueAnimator.ofFloat(waveAmplitude, 0f).apply {
duration = 500
addUpdateListener { anim ->
waveAmplitude = anim.animatedValue as Float
}
start()
}
} else if (waveAmplitude < heartSize * 0.05f) {
ValueAnimator.ofFloat(waveAmplitude, heartSize * 0.05f).apply {
duration = 500
addUpdateListener { anim ->
waveAmplitude = anim.animatedValue as Float
}
start()
}
}
}
start()
}
}
/**
* 获取当前液体高度
*/
fun getLiquidLevel(): Float = liquidLevel
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// 释放动画资源
waveAnimator?.cancel()
waveAnimator = null
}
}
二、attrs中
<declare-styleable name="LiquidHeartView">
<attr name="liquidLevel" format="float" />
</declare-styleable>
三、act_main_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<com.example.testdemo.LiquidHeartView
android:id="@+id/liquidHeartView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
app:liquidLevel="0.5" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/decreaseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-"
android:padding="12dp"
android:layout_marginEnd="16dp"
android:backgroundTint="@color/primary"
android:textColor="@android:color/white"
android:elevation="4dp" />
<TextView
android:id="@+id/levelTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="50%"
android:textSize="18sp"
android:layout_gravity="center_vertical"
android:padding="8dp" />
<Button
android:id="@+id/increaseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:padding="12dp"
android:layout_marginStart="16dp"
android:backgroundTint="@color/primary"
android:textColor="@android:color/white"
android:elevation="4dp" />
</LinearLayout>
<SeekBar
android:id="@+id/levelSeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:max="100"
android:progress="50" />
</LinearLayout>
四、MainActivity.java
package com.example.testdemo
import android.app.Activity
import android.os.Bundle
import android.widget.Button
import android.widget.SeekBar
import android.widget.TextView
/**
* Created by wsm on 2025/6/23
* <p>
*/
class MainActivity : Activity() {
var levelTextView: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.act_main_layout)
val liquidHeartView = findViewById<LiquidHeartView>(R.id.liquidHeartView)
levelTextView = findViewById(R.id.levelTextView)
val levelSeekBar = findViewById<SeekBar>(R.id.levelSeekBar)
// 初始化进度显示
updateLevelText(liquidHeartView.getLiquidLevel())
// 减少按钮点击事件
findViewById<Button>(R.id.decreaseButton).setOnClickListener {
val currentLevel = liquidHeartView.getLiquidLevel()
val newLevel = (currentLevel - 0.1f).coerceAtLeast(0f)
liquidHeartView.setLiquidLevel(newLevel)
updateLevelText(newLevel)
levelSeekBar.progress = (newLevel * 100).toInt()
}
// 增加按钮点击事件
findViewById<Button>(R.id.increaseButton).setOnClickListener {
val currentLevel = liquidHeartView.getLiquidLevel()
val newLevel = (currentLevel + 0.1f).coerceAtMost(1f)
liquidHeartView.setLiquidLevel(newLevel)
updateLevelText(newLevel)
levelSeekBar.progress = (newLevel * 100).toInt()
}
// SeekBar进度变化事件
levelSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
val level = progress / 100f
liquidHeartView.setLiquidLevel(level)
updateLevelText(level)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
private fun updateLevelText(level: Float) {
val percentage = (level * 100).toInt()
levelTextView?.text = "$percentage%"
}
}
五、color.xml颜色值
<color name="primary">#FF4D6D</color>
<color name="secondary">#FFB3C1</color>
<color name="liquid">#FF758F</color>