当前位置: 代码网 > it编程>App开发>Android > Android实现自定义飘雪效果

Android实现自定义飘雪效果

2024年05月19日 Android 我要评论
背景随着冬季的脚步越来越远,南方的我今年就看了一场雪,下一场雪遥遥无期。那我们来实现一个自定义的 view,它能模拟雪花飘落的景象。我们一起来看一下如何让这些数字雪花在屏幕上轻盈地飞舞。一个雪球下落我

背景

随着冬季的脚步越来越远,南方的我今年就看了一场雪,下一场雪遥遥无期。
那我们来实现一个自定义的 view,它能模拟雪花飘落的景象。我们一起来看一下如何让这些数字雪花在屏幕上轻盈地飞舞。

一个雪球下落

我们绘制一个圆,让其匀速下落,当超出屏幕就刷新:

private val msnowpaint: paint = paint(paint.anti_alias_flag).apply {
    color = color.white
    style = style.fill
}
// 雪花的位置
private var mpositionx = 300f
private var mpositiony = 0f
private var msize = 20f // 雪花的大小

override fun draw(canvas: canvas) {
    super.draw(canvas)
    canvas.drawcircle(mpositionx, mpositiony, msize, msnowpaint)
    updatesnow()
}

private fun updatesnow() {
    mpositiony += 10f
    if (mpositiony > height) {
        mpositiony = 0f
    }
    postinvalidateonanimation()
}

效果如下:

多个雪球下落

我们先简单的写个雪花数据类:

data class snowitem(
    val size: float,
    var positionx: float,
    var positiony: float,
    val downspeed: float
)

生成50个雪花:

private fun createsnowitemlist(): list<snowitem> {
    val snowitemlist = mutablelistof<snowitem>()
    val minsize = 10
    val maxsize = 20
    for (i in 0..50) {
        val size = mrandom.nextint(maxsize - minsize) + minsize
        val positionx = mrandom.nextint(width)
        val speed = size.tofloat()
        val snowitem = snowitem(size.tofloat(), positionx.tofloat(), 0f, speed)
        snowitemlist.add(snowitem)
    }
    return snowitemlist
}

来看一下50个雪花的效果:

private lateinit var msnowitemlist: list<snowitem>

//需要拿到width,所以在onsizechanged之后创建itemlist
override fun onsizechanged(w: int, h: int, oldw: int, oldh: int) {
    super.onsizechanged(w, h, oldw, oldh)
    msnowitemlist = createsnowitemlist() 
}
    
override fun draw(canvas: canvas) {
    super.draw(canvas)
    for (snowitem in msnowitemlist) {
        canvas.drawcircle(snowitem.positionx, snowitem.positiony, snowitem.size, msnowpaint)
        updatesnow(snowitem)
    }
    postinvalidateonanimation()
}

private fun updatesnow(snowitem: snowitem) {
    snowitem.positiony += snowitem.downspeed
    if (snowitem.positiony > height) {
        snowitem.positiony = 0f
    }
}

弦波动:让雪花有飘落的感觉

上面的雪花是降落的,不是很逼真,我们如何让雪花有飘落的感觉了?我们可以给水平/竖直方向都加上弦波动。
我们这里是以所有雪花为一个整体做弦波动。
理解一下这句话的意思,就是说所有的雪花水平/竖直方向波动符合一个弦波动,而不是单个雪花的运动符合弦波动。

[想象一下如果每个雪花都在左右扭动,数量一多,是不是就很乱!]

我们结合代码在理解一下上述的话,记得看一下注释:

// 通过角度->转为弧度的值->正弦/余弦的值
val anglemax = 10
val leftorright = mrandom.nextboolean() //true: left, false: right
val angle = mrandom.nextdouble() * anglemax
val radians = if (leftorright) {
    math.toradians(-angle)
} else {
    math.toradians(angle)
}
//正弦 在[-90度,90度]分正负,所以给x方向,区分左右
val speedx = speed * sin(radians).tofloat()
val speedy = speed * cos(radians).tofloat()
//speedx和speedy随机后,就确定下来,
//就是说某个雪花的speedx和speedy在下落的过程中是确定的
//即所有雪花为一个整体做弦波动

我们需要添加水平方向的速度,所以我们需要修改snowitem类:

data class snowitem(
    val size: float,
    val originalposx: int,
    var positionx: float,
    var positiony: float,
    val speedx: float,
    val speedy: float
)

修改完后,我们看一下snowitem的创建:

private fun createsnowitemlist(): list<snowitem> {
    val snowitemlist = mutablelistof<snowitem>()
    val minsize = 10
    val maxsize = 20
    for (i in 0..50) {
        val size = mrandom.nextint(maxsize - minsize) + minsize
        val speed = size.tofloat()
        //这一部分看上面代码的注释
        val anglemax = 10
        val leftorright = mrandom.nextboolean()
        val angle = mrandom.nextdouble() * anglemax
        val radians = if (leftorright) {
            math.toradians(-angle)
        } else {
            math.toradians(angle)
        }
        val speedx = speed * sin(radians).tofloat()
        val speedy = speed * cos(radians).tofloat()
        val positionx = mrandom.nextint(width)
        //snowitem创建
        val snowitem = snowitem(
            size.tofloat(),
            positionx.tofloat(),
            positionx.tofloat(),
            0f,
            speedx,
            speedy
        )
        snowitemlist.add(snowitem)
    }
    return snowitemlist
}

雪花位置更新如下:

private fun updatesnow(snowitem: snowitem) {
    snowitem.positiony += snowitem.speedy
    snowitem.positionx += snowitem.speedx
    if (snowitem.positiony > height) {
        snowitem.positiony = 0f
        snowitem.positionx = snowitem.originalposx
    }
}

看一下效果图,再理解一下所有雪花为一个整体做弦波动这句话。

正态分布:让雪花大小更符合现实

随机获取一个正态分布的值,并通过递归的方式让其在(-1,1).

private fun getrandomgaussian(): double {
    val gaussian = mrandom.nextgaussian() / 2
     if (gaussian > -1 && gaussian < 1) {
         return gaussian
    } else {
         return getrandomgaussian() // 递归:确保在(-1, 1)之间
    }
}

根据正态分布修改一下雪花的大小:

//旧
val size = mrandom.nextint(maxsize - minsize) + minsize
//新
val size = abs(getrandomgaussian()) * (maxsize - minsize) + minsize

雪球变雪花

我们这里就不自己去画雪花了,我们去找个雪花的icon就行。
iconfont-阿里巴巴矢量图标库我们给snowitem加上雪花icon资源的属性:

data class snowitem(
    val size: float,
    val originalposx: float,
    var positionx: float,
    var positiony: float,
    val speedx: float,
    val speedy: float,
    val snowflakebitmap: bitmap? = null
)

将icon裁剪为和雪球一样大:

//todo 需要兼容类型
private val msnowflakedrawable = contextcompat.getdrawable(context, r.drawable.icon_snowflake) as bitmapdrawable
...
private fun createsnowitemlist(): list<snowitem> {
    ...
    val size = abs(getrandomgaussian()) * (maxsize - minsize) + minsize
    val bitmap = bitmap.createscaledbitmap(msnowflakedrawable.bitmap, size.toint(), size.toint(), false)
    val snowitem = snowitem(
        size.tofloat(),
        positionx.tofloat(),
        positionx.tofloat(),
        0f,
        speedx,
        speedy,
        bitmap
    )
    ...
}

绘制的时候,我们使用bitmap去绘制:

override fun draw(canvas: canvas) {
    super.draw(canvas)
    for (snowitem in msnowitemlist) {
        if (snowitem.snowflakebitmap != null) {
            //如果有snowflakebitmap,绘制bitmap
            canvas.drawbitmap(snowitem.snowflakebitmap, snowitem.positionx, snowitem.positiony, msnowpaint)
        } else {
            canvas.drawcircle(snowitem.positionx, snowitem.positiony, snowitem.size, msnowpaint)
        }
        updatesnow(snowitem)
    }
    postinvalidateonanimation()
}

到这里我们飘雪的效果基本实现了,但是目前的代码结构一团糟,接下来我们整理一下代码。

逻辑完善&性能优化

首先我们将雪花的属性如大小,速度等封装一下:

data class snowflakeparams(
    val canvaswidth: int, // 画布的宽度
    val canvasheight: int, // 画布的高度
    val sizemininpx: int = 30, // 雪花的最小大小
    val sizemaxinpx: int = 50, // 雪花的最大大小
    val speedmin: int = 10,  // 雪花的最小速度
    val speedmax: int = 20, // 雪花的最大速度
    val alphamin: int = 150, // 雪花的最小透明度
    val alphamax: int = 255, // 雪花的最大透明度
    val anglemax: int = 10, // 雪花的最大角度
    val snowflakeimage: bitmap? = null, // 雪花的图片
)

然后,让每个雪花控制自己的绘制和更新。其次需要让每个雪花可以复用从而减少资源消耗。

class snowflak(private val params: snowflakeparams) {
    private val mrandom = random()

    private var msize: double = 0.0
    private var malpha: int = 255
    private var mspeedx: double = 0.0
    private var mspeedy: double = 0.0
    private var mpositionx: double = 0.0
    private var mpositiony: double = 0.0
    private var msnowflakeimage: bitmap? = null

    private val mpaint = paint(paint.anti_alias_flag).apply {
        color = color.white
        style = style.fill
    }

    init {
        reset()
    }

    //复用雪花
    private fun reset(){
        val deltasize = params.sizemaxinpx - params.sizemininpx
        msize = abs(getrandomgaussian()) * deltasize + params.sizemininpx
        params.snowflakeimage?.let {
            msnowflakeimage = bitmap.createscaledbitmap(it, msize.toint(), msize.toint(), false)
        }
        //做一个线性插值,根据雪花的大小,来确定雪花的速度
        val lerp = (msize - params.sizemininpx) / (params.sizemaxinpx - params.sizemininpx)
        val speed = lerp * (params.speedmax - params.speedmin) + params.speedmin

        val angle = mrandom.nextdouble() * params.anglemax
        val leftorright = mrandom.nextboolean() //true: left, false: right
        val radians = if (leftorright) {
            math.toradians(-angle)
        } else {
            math.toradians(angle)
        }
        mspeedx = speed * sin(radians)
        mspeedy = speed * cos(radians)

        malpha = mrandom.nextint(params.alphamax - params.alphamin) + params.alphamin
        mpaint.alpha = malpha

        mpositionx = mrandom.nextdouble() * params.canvaswidth
        mpositiony = -msize
    }

    fun update() {
        mpositionx += mspeedx
        mpositiony += mspeedy
        if (mpositiony > params.canvasheight) {
            reset()
        }
        //根据雪花的位置,来确定雪花的透明度
        val alphapercentage = (params.canvasheight - mpositiony).tofloat() / params.canvasheight
        mpaint.alpha = (alphapercentage * malpha).toint()
    }

    fun draw(canvas: canvas) {
        if (msnowflakeimage != null) {
            canvas.drawbitmap(msnowflakeimage!!, mpositionx.tofloat(), mpositiony.tofloat(), mpaint)
        } else {
            canvas.drawcircle(mpositionx.tofloat(), mpositiony.tofloat(), msize.tofloat(), mpaint)
        }
    }

    private fun getrandomgaussian(): double {
        val gaussian = mrandom.nextgaussian() / 2
        return if (gaussian > -1 && gaussian < 1) {
            gaussian
        } else {
            getrandomgaussian() // 确保在(-1, 1)之间
        }
    }
}

将绘制和更新逻辑放到每个雪花中,那么snowview就会很简洁:

class snowview @jvmoverloads constructor(
    context: context,
    attrs: attributeset? = null,
    defstyleattr: int = 0
) : view(context, attrs, defstyleattr) {

    private lateinit var msnowitemlist: list<snowflake>

    private val msnowflakeimage = contextcompat.getdrawable(context, r.drawable.icon_snowflake)?.tobitmap()

    override fun onsizechanged(w: int, h: int, oldw: int, oldh: int) {
        super.onsizechanged(w, h, oldw, oldh)
        msnowitemlist = createsnowitemlist()
    }

    private fun createsnowitemlist(): list<snowflake> {
        return list(80) {
            snowflake(snowflakeparams(width, height, snowflakeimage = msnowflakeimage))
        }
    }

    override fun draw(canvas: canvas) {
        super.draw(canvas)
        for (snowitem in msnowitemlist) {
            snowitem.draw(canvas)
            snowitem.update()
        }
        postinvalidateonanimation()
    }
}

下面是添加了透明度和优化下落速度的效果图,现在更加自然了。

在snowflake中有不少随机函数的计算,尤其是雪花数量非常庞大的时候,可能会引起卡顿, 我们将update的方法放子线程中:

...
private lateinit var mhandler: handler
private lateinit var mhandlerthread : handlerthread
...
override fun onattachedtowindow() {
    super.onattachedtowindow()
    mhandlerthread = handlerthread("snowview").apply {
        start()
        mhandler = handler(looper)
    }
}
...
override fun draw(canvas: canvas) {
    super.draw(canvas)
    for (snowitem in msnowitemlist) {
        snowitem.draw(canvas)
    }
    mhandler.post {
        //子线程更新雪花位置/状态
        for (snowitem in msnowitemlist) {
            snowitem.update()
        }
        postinvalidateonanimation()
    }
}
...
override fun ondetachedfromwindow() {
    mhandlerthread.quitsafely()
    super.ondetachedfromwindow()
}

这里还有个小问题, 就是多次创建新的bitmap

 private fun reset(){
    ...
    params.snowflakeimage?.let {
        //这里👇
        msnowflakeimage = bitmap.createscaledbitmap(it, msize.toint(), msize.toint(), false)
    }
    ...
 }

其实snowflakeimage是不变的,msize的范围在min-max之间,也没多少个。我想到的解决方法,将size进行裁剪后bitmap进行缓存。(如果有其他的好办法,可以告知我。)

private fun getsnowflakebitmapfromcache(size: int): bitmap {
    return snowflakebitmapcache.getorput(size) {
        // 创建新的 bitmap 并放入缓存
        bitmap.createscaledbitmap(params.snowflakeimage, size, size, false)
    }
}

在1000个雪花下,模拟器没有任何卡顿,内存也没有啥涨幅。

最后就是将各个属性跑给外面去设置.

  • 方法1: 通过styleable的方式在xml里面使用,我就不多描述了
  • 方法2: builder模式去设置:
 class builder(private val context: context) {
        private var canvaswidth: int = 0
        private var canvasheight: int = 0
        private var sizemininpx: int = 40
        private var sizemaxinpx: int = 60
        private var speedmin: int = 10
        private var speedmax: int = 20
        private var alphamin: int = 150
        private var alphamax: int = 255
        private var anglemax: int = 10
        private var snowflakeimage: bitmap? = null
        
        fun setcanvassize(canvaswidth: int, canvasheight: int) = apply {
            this.canvaswidth = canvaswidth
            this.canvasheight = canvasheight
        }

        fun setsizerangeinpx(sizemin: int, sizemax: int) = apply {
            this.sizemininpx = sizemin
            this.sizemaxinpx = sizemax
        }

        fun setspeedrange(speedmin: int, speedmax: int) = apply {
            this.speedmin = speedmin
            this.speedmax = speedmax
        }

        fun setalpharange(alphamin: int, alphamax: int) = apply {
            this.alphamin = alphamin
            this.alphamax = alphamax
        }

        fun setanglemax(anglemax: int) = apply {
            this.anglemax = anglemax
        }

        fun setsnowflakeimage(snowflakeimage: bitmap) = apply {
            this.snowflakeimage = snowflakeimage
        }

        fun setsnowflakeimageresid(@drawableres snowflakeimageresid: int) = apply {
            this.snowflakeimage = contextcompat.getdrawable(context, snowflakeimageresid)?.let {
                (it as bitmapdrawable).bitmap
            }
        }

        fun build(): snowview {
            return snowview(
                context, params = snowflakeparams(
                    sizemininpx = sizemininpx,
                    sizemaxinpx = sizemaxinpx,
                    speedmin = speedmin,
                    speedmax = speedmax,
                    alphamin = alphamin,
                    alphamax = alphamax,
                    anglemax = anglemax,
                    snowflakeimage = snowflakeimage
                )
            )
        }
    }

使用builder模式创建:

 val snowview = snowview.builder(this)
     .setsnowflakeimageresid(r.drawable.icon_small_snowflake)
     .setsnowflakecount(50)
     .setspeedrange(10, 20)
     .setsizerangeinpx(40, 60)
     .setalpharange(150, 255)
     .setanglemax(10)
     .build()
     
 mbinding.clroot.addview(
     snowview,
     viewgroup.layoutparams(
         viewgroup.layoutparams.match_parent,
         viewgroup.layoutparams.match_parent
     )
 )

最后我们加上背景图片,最终效果如下:

项目代码:https://github.com/mrs-chang/dailylearn/blob/master/snow/src/main/java/com/chang/snow/snowview.kt

以上就是android实现自定义飘雪效果的详细内容,更多关于android飘雪效果的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com