Android 实现一个爱心液体容器

在这里插入图片描述
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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值