背景
随着冬季的脚步越来越远,南方的我今年就看了一场雪,下一场雪遥遥无期。
那我们来实现一个自定义的 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飘雪效果的资料请关注代码网其它相关文章!
发表评论