氣泡上浮效果

老規矩,上圖:

一、實現思路 

1.我們先實現一個氣泡,順便熟悉下自定義view。一個氣泡,當然需要畫圓,畫圓就需要計算圓的座標和半徑。如下:

/**
 * Created by xinheng on 2019/12/12 10:52
 * describe:一個圓 前部
 */
class AirBubbles1View @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
    /**
     * 畫筆
     */
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    /**
     * 初始化球半徑
     */
    private var r = 10f
    //圓心座標x
    private var xAir = 200f
    //圓心座標y
    private var yAir = -1f
    //隨機
    private val random = Random(10)

    init {
        paint.isDither = true
        paint.isAntiAlias = true
        //設置背景
        setBackgroundColor(ContextCompat.getColor(context, R.color.colorPrimary))
        paint.color = Color.WHITE
        paint.style = Paint.Style.FILL
    }
    
    //開始繪製
    override fun onDraw(canvas: Canvas) {
        //因爲沒有自己計算view大小(重寫onMeasure()方法),在onDraw初始化
        if (yAir < 0) {
            //剛開始底部Y軸座標
            val startY = measuredHeight + r
            //剛開始X軸座標
            val startX = 200f
            yAir = startY
            xAir = startX
        }
        canvas.drawCircle(xAir, yAir, r, paint)
    }
}

2.步驟1的結果就不展示了。我們需要氣泡動起來,因此需要持續更新圓心座標。若要動起來的過程中氣泡變大,還需要改變圓的半徑。

這裏藉助了屬性動畫更新相應的值,如下:

    private fun startAnimal() {
        val animal = ObjectAnimator.ofFloat(this, "transport", 0f, 1f)
        animal.duration = 8000
        animal.start()
        animal.repeatMode = ValueAnimator.RESTART
        animal.repeatCount = ValueAnimator.INFINITE
    }
    fun setTransport(arg: Float) {
        val startX = 200f
        //x軸添加了抖動(此數值效果不是很好,沒接着試,直接去掉了)
        val x = startX + (random.nextFloat() - .5f) * 2.3f
        r = arg * 30 + 10
        xAir = x
        yAir = measuredHeight - measuredHeight * arg
        invalidate()
    }

效果:

3.一個解決了,多個就容易了。我們需要一個可以不斷更新圓心座標和半徑的機制。這裏一切從簡每一個氣泡綁定一個這樣的機制。然後不斷刷新view,繪製氣泡。

①定義氣泡信息類,定義圓心座標、半徑、更新機制(這裏依舊採用屬性動畫)

/**
 * Created by xinheng on 2019/12/12 12:08
 * describe:氣泡信息
 */
class AirBubblesPoint(private var length: Int) {
    companion object {
        //最小圓心
        const val minR = 5
    }

    private val TAG = "TAG_AirPoint"
    var x = -1f
    var y = 0f
    var r = minR.toFloat()
    var duration = 0
    //動畫進度(0->1)
    var progress = 0f
    fun finallyY(): Float = length - progress * length
    fun finallyX(): Float = x
    fun finallyR(): Float = progress * (r - minR) + minR
    //屬性動畫
    private val animator = ObjectAnimator.ofFloat(this, "value", 0f, 1f).apply {
        repeatCount = ObjectAnimator.INFINITE
        repeatMode = ObjectAnimator.RESTART
        addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationRepeat(animation: Animator?) {
                super.onAnimationRepeat(animation)
                //單次動畫執行完畢後,重設圓信息
                //resetXY()
            }
        })
    }
    fun setValue(value: Float) {
        progress = value
    }

    /**
     * 開啓動畫
     */
    fun start() {
        animator.duration = duration.toLong()
        animator.start()
    }
}

②多個氣泡自然需要在自定義view中存儲(這裏採用linkList),view的定時刷新(依舊屬性動畫),繪製。

/**
 * Created by xinheng on 2019/12/12 10:52
 * describe:好多氣泡
 */
class AirBubblesView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
    private val TAG = "TAG_AirView"
    private val linkListPoint = LinkedList<AirBubblesPoint>()
    private val random = Random(10)
    private var paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        isDither = true
        isAntiAlias = true
    }
    /**
     * 動畫開啓標誌
     */
    private var animationStartTag = false
    private val animation = ValueAnimator().apply {
        setTarget(this@AirBubblesView)
        setFloatValues(0f, 2f)
        duration = 1000
        repeatCount = ValueAnimator.INFINITE
        repeatMode = ValueAnimator.REVERSE
        addUpdateListener {
            if (linkListPoint.size < 31) {//數量限制
                addAirPoint()
            }
            invalidate()
        }
    }

    init {
        paint.color = Color.WHITE
        paint.alpha = 60
        paint.style = Paint.Style.FILL
        //背景設置
        background = ContextCompat.getDrawable(context, R.drawable.drawable_bg)
    }

    /**
     * 添加氣泡,並開啓動畫
     */
    private fun addAirPoint() {
        val airPoint = createAirPoint()
        airPoint.duration = (random.nextInt(7) + 4) * 1000
        linkListPoint.addLast(airPoint)
        airPoint.start()
    }

    private fun createAirPoint(): AirBubblesPoint = AirBubblesPoint(measuredHeight).apply {
        x = random.nextInt(measuredWidth).toFloat()
        r = random.nextInt(40) + minR * 2f
        y = measuredHeight.toFloat() + r
        //airBubblesListener = resetListener
    }

    override fun onDraw(canvas: Canvas) {
        if (!animationStartTag) {
            animationStartTag = true
            animation.start()
        }
        linkListPoint.forEach {
            val cx = it.finallyX()
            val cy = it.finallyY()
            //Log.e(TAG,"cx=$cx cy=$cy r=${it.r}")
            canvas.drawCircle(cx, cy, it.finallyR(), paint)
        }
    }
}

③運行後是不是發現,每個氣泡的x軸的位置都固定了(其實我第一遍寫的時候也沒加,哈哈)。改變這個,我們需要單個氣泡的單次動畫結束後更改其信息,如下:

    //屬性動畫
    private val animator = ObjectAnimator.ofFloat(this, "value", 0f, 1f).apply {
        repeatCount = ObjectAnimator.INFINITE
        repeatMode = ObjectAnimator.RESTART
        addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationRepeat(animation: Animator?) {
                super.onAnimationRepeat(animation)
                //單次動畫執行完畢後,重設圓信息
                resetXY()
            }
        })
    }

    /**
     * 信息重設
     */
    private fun resetXY() {
        airBubblesListener?.let {
            x = it.resetX()
            r = it.resetR()
            y = it.resetY() + r
        }
    }
    
    var airBubblesListener: AirBubblesListener? = null
        set(value) {
            field = value
            if (x < 0) {
                resetXY()
            }
        }

    /**
     * 信息更新數據來源
     */
    interface AirBubblesListener {
        fun resetX(): Float
        fun resetY(): Float
        fun resetR(): Float
    }
    private fun createAirPoint(): AirBubblesPoint = AirBubblesPoint(measuredHeight).apply {
        //接口綁定
        airBubblesListener = resetListener
    }

    /**
     * 設置接口返回值
     */
    private val resetListener = object : AirBubblesPoint.AirBubblesListener {
        override fun resetX(): Float = random.nextInt(measuredWidth).toFloat()
        override fun resetR(): Float = random.nextInt(40) + minR * 2f
        override fun resetY(): Float = measuredHeight.toFloat()
    }

④這樣就可以在單次動畫結束後重新設置圓的信息,達到生成一個新的效果。效果是完成了,還差收尾了。當view所在的ui回收時需要釋放資源:

    //AirBubblesPoint
    fun recycle() {
        animator.cancel()
        airBubblesListener = null
    }
    //AirBubblesView
    override fun onDetachedFromWindow() {
        //疑惑可以在這裏釋放麼或者在這裏釋放可以達到目的麼
        recycle()
        Log.e(TAG, "onDetachedFromWindow")
        super.onDetachedFromWindow()
    }
    fun recycle() {
        animation.cancel()
        linkListPoint.forEach {
            it.recycle()
        }
        linkListPoint.clear()
    }

總結:最後釋放資源那塊寫在onDetachedFromWindow可以達到目的麼,有沒有大神留言指教下,好久之前看的了,有點不確定。附上項目地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章