老規矩,上圖:
一、實現思路
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可以達到目的麼,有沒有大神留言指教下,好久之前看的了,有點不確定。附上項目地址。