欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android實現(xiàn)自定義飄雪效果

 更新時間:2024年01月14日 15:17:41   作者:蹦蹦蹦  
隨著冬季的腳步越來越遠,南方的我今年就看了一場雪,下一場雪遙遙無期,那我們來實現(xiàn)一個自定義的 View,它能模擬雪花飄落的景象,所以本文給大家介紹了基于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)文章

  • Android動畫入門教程之kotlin

    Android動畫入門教程之kotlin

    最近在學習kotlin,所以下面這篇文章主要給大家介紹了關(guān)于Android動畫入門教程之kotlin的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學習學習吧。
    2017-12-12
  • 詳解Flutter如何獲取Text截斷后的字符串

    詳解Flutter如何獲取Text截斷后的字符串

    當Text文本設(shè)置maxLins屬性將文本強制截斷之后,Text的承載字符串是截斷前,還是截斷后的呢,我們又該如何獲取截斷后的字符串呢,下面就來和大家詳細講講
    2023-06-06
  • Android?拍照后返回縮略圖的兩種方法介紹

    Android?拍照后返回縮略圖的兩種方法介紹

    大家好,本篇文章主要講的是Android?拍照后返回縮略圖的兩種方法介紹,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下
    2022-01-01
  • Android RecylerView入門教程

    Android RecylerView入門教程

    這篇文章主要介紹了Android RecylerView入門教程的相關(guān)資料,很適合剛?cè)腴T的新手學習,非常不錯,具有參考借鑒價值,需要的朋友可以參考下
    2016-07-07
  • 圖解 Kotlin SharedFlow 緩存系統(tǒng)及示例詳解

    圖解 Kotlin SharedFlow 緩存系統(tǒng)及示例詳解

    這篇文章主要為大家介紹了圖解 Kotlin SharedFlow 緩存系統(tǒng)及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10
  • Kotlin實現(xiàn)半圓形進度條的方法示例

    Kotlin實現(xiàn)半圓形進度條的方法示例

    這篇文章主要給大家介紹了關(guān)于Kotlin實現(xiàn)半圓形進度條的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧。
    2018-03-03
  • Android Studio 中運行 groovy 程序的方法圖文詳解

    Android Studio 中運行 groovy 程序的方法圖文詳解

    這篇文章主要介紹了Android Studio 中 運行 groovy 程序的方法,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-03-03
  • 從源碼編譯Android系統(tǒng)的Java類庫和JNI動態(tài)庫的方法

    從源碼編譯Android系統(tǒng)的Java類庫和JNI動態(tài)庫的方法

    這篇文章主要介紹了從源碼編譯Android系統(tǒng)的Java類庫和JNI動態(tài)庫的方法,例子基于Linux系統(tǒng)環(huán)境下來講,需要的朋友可以參考下
    2016-02-02
  • Android中handler使用淺析

    Android中handler使用淺析

    本文主要介紹了Android中handler的使用,具有很好的參考價值。下面跟著小編一起來看下吧
    2017-03-03
  • Flutter進階之實現(xiàn)動畫效果(五)

    Flutter進階之實現(xiàn)動畫效果(五)

    這篇文章主要為大家詳細介紹了Flutter進階之實現(xiàn)動畫效果的第五篇,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-08-08

最新評論