Android實現(xiàn)自定義飄雪效果
背景
隨著冬季的腳步越來越遠,南方的我今年就看了一場雪,下一場雪遙遙無期。
那我們來實現(xiàn)一個自定義的 View,它能模擬雪花飄落的景象。我們一起來看一下如何讓這些數(shù)字雪花在屏幕上輕盈地飛舞。
一個雪球下落
我們繪制一個圓,讓其勻速下落,當超出屏幕就刷新:
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() }
效果如下:
多個雪球下落
我們先簡單的寫個雪花數(shù)據(jù)類:
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之后創(chuàng)建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 } }
弦波動:讓雪花有飄落的感覺
上面的雪花是降落的,不是很逼真,我們?nèi)绾巫屟┗ㄓ酗h落的感覺了?我們可以給水平/豎直方向都加上弦波動。
我們這里是以所有雪花為一個整體做弦波動。
理解一下這句話的意思,就是說所有的雪花水平/豎直方向波動符合一個弦波動,而不是單個雪花的運動符合弦波動。
[想象一下如果每個雪花都在左右扭動,數(shù)量一多,是不是就很亂!]
我們結(jié)合代碼在理解一下上述的話,記得看一下注釋:
// 通過角度->轉(zhuǎn)為弧度的值->正弦/余弦的值 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方向,區(qū)分左右 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的創(chuàng)建:
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創(chuàng)建 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 } }
看一下效果圖,再理解一下所有雪花為一個整體做弦波動這句話。
正態(tài)分布:讓雪花大小更符合現(xiàn)實
隨機獲取一個正態(tài)分布的值,并通過遞歸的方式讓其在(-1,1).
private fun getRandomGaussian(): Double { val gaussian = mRandom.nextGaussian() / 2 if (gaussian > -1 && gaussian < 1) { return gaussian } else { return getRandomGaussian() // 遞歸:確保在(-1, 1)之間 } }
根據(jù)正態(tài)分布修改一下雪花的大?。?/p>
//舊 val size = mRandom.nextInt(maxSize - minSize) + minSize //新 val size = abs(getRandomGaussian()) * (maxSize - minSize) + minSize
雪球變雪花
我們這里就不自己去畫雪花了,我們?nèi)フ覀€雪花的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() }
到這里我們飄雪的效果基本實現(xiàn)了,但是目前的代碼結(jié)構(gòu)一團糟,接下來我們整理一下代碼。
邏輯完善&性能優(yōu)化
首先我們將雪花的屬性如大小,速度等封裝一下:
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, // 雪花的圖片 )
然后,讓每個雪花控制自己的繪制和更新。其次需要讓每個雪花可以復(fù)用從而減少資源消耗。
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() } //復(fù)用雪花 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) } //做一個線性插值,根據(jù)雪花的大小,來確定雪花的速度 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() } //根據(jù)雪花的位置,來確定雪花的透明度 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() } }
下面是添加了透明度和優(yōu)化下落速度的效果圖,現(xiàn)在更加自然了。
在Snowflake中有不少隨機函數(shù)的計算,尤其是雪花數(shù)量非常龐大的時候,可能會引起卡頓, 我們將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 { //子線程更新雪花位置/狀態(tài) for (snowItem in mSnowItemList) { snowItem.update() } postInvalidateOnAnimation() } } ... override fun onDetachedFromWindow() { mHandlerThread.quitSafely() super.onDetachedFromWindow() }
這里還有個小問題, 就是多次創(chuàng)建新的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) { // 創(chuàng)建新的 Bitmap 并放入緩存 Bitmap.createScaledBitmap(params.snowflakeImage, size, size, false) } }
在1000個雪花下,模擬器沒有任何卡頓,內(nèi)存也沒有啥漲幅。
最后就是將各個屬性跑給外面去設(shè)置.
- 方法1: 通過styleable的方式在xml里面使用,我就不多描述了
- 方法2: Builder模式去設(shè)置:
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模式創(chuàng)建:
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實現(xiàn)自定義飄雪效果的詳細內(nèi)容,更多關(guān)于Android飄雪效果的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
圖解 Kotlin SharedFlow 緩存系統(tǒng)及示例詳解
這篇文章主要為大家介紹了圖解 Kotlin SharedFlow 緩存系統(tǒng)及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10Android Studio 中運行 groovy 程序的方法圖文詳解
這篇文章主要介紹了Android Studio 中 運行 groovy 程序的方法,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03從源碼編譯Android系統(tǒng)的Java類庫和JNI動態(tài)庫的方法
這篇文章主要介紹了從源碼編譯Android系統(tǒng)的Java類庫和JNI動態(tài)庫的方法,例子基于Linux系統(tǒng)環(huán)境下來講,需要的朋友可以參考下2016-02-02