前情提要
上一篇 Android Kotlin之Flow数据流 中介绍了协程Flow
,我们知道Flow
数据流可以按顺序发送多个值,一个倒计时功能刚好符合这种场景,本文就尝试使用Flow
来实现一个倒计时功能。
上文中举过一个简单示例:
flow {
log("send hello")
emit("hello") //发送数据
log("send world")
emit("world") //发送数据
}.flowOn(Dispatchers.IO)
.onEmpty { log("onEmpty") }
.onStart { log("onStart") }
.onEach { log("onEach: $it") }
.onCompletion { log("onCompletion") }
.catch { exception -> exception.message?.let { log(it) } }
.collect {
//接收数据流
log("collect: $it")
}
执行结果:
2021-09-27 19:51:54.433 7240-7240/ E/TTT: onStart
2021-09-27 19:51:54.439 7240-7325/ E/TTT: send hello
2021-09-27 19:51:54.440 7240-7325/ E/TTT: send world
2021-09-27 19:51:54.451 7240-7240/ E/TTT: onEach: hello
2021-09-27 19:51:54.451 7240-7240/ E/TTT: collect:hello
2021-09-27 19:51:54.452 7240-7240/ E/TTT: onEach: world
2021-09-27 19:51:54.452 7240-7240/ E/TTT: collect:world
2021-09-27 19:51:54.453 7240-7240/ E/TTT: onCompletion
onStart
:上游flow{}
开始发送数据之前执行onCompletion
:flow
数据流取消或者结束时执行onEach
:上游向下游发送数据之前调用,每一个上游数据发送后都会经过onEach()
使用上面几个操作符,将传入的数据在flow{}
中每隔1s(通过delay(1000)
实现)发射出去,下游对接收的数据进行处理,即是一个倒计时功能,如下:
/**
* 使用Flow实现一个倒计时功能
*/
private fun countDownByFlow(
max: Int,
scope: CoroutineScope,
onTick: (Int) -> Unit,
onFinish: (() -> Unit)? = null,
): Job {
return flow {
for (num in max downTo 0) {
emit(num)
if (num != 0) delay(1000)
}
}.flowOn(Dispatchers.Main)
.onEach { onTick.invoke(it) }
.onCompletion { cause -> if (cause == null) onFinish?.invoke() }
.launchIn(scope) //保证在一个协程中执行
}
实现倒计时功能
先上效果图:
主要代码逻辑:
class CountDownCircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
private val mPaint: Paint = Paint().apply {
isAntiAlias = true
isDither = true
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
strokeWidth = 8.dp2px().toFloat()
color = Color.parseColor("#F7F9FA")
}
private val mCirclePaint: Paint = Paint().apply {
isAntiAlias = true
isDither = true
style = Paint.Style.FILL
color = Color.WHITE
}
private val mTextPaint: TextPaint = TextPaint().apply {
isAntiAlias = true
isDither = true
color = Color.parseColor("#A1EA42")
textAlign = Paint.Align.CENTER //绘制方向 居中绘制
textSize = 50.sp2px().toFloat()
typeface = Typeface.DEFAULT_BOLD
}
private val mRect = RectF()
private var mCenterX: Float = 0f
private var mCenterY: Float = 0f
private var mRadius: Float = 0f
private var mMinWH: Float = 0f
private val colorArr = intArrayOf(Color.parseColor("#BAF900"), Color.parseColor("#84F000"))
//设置渐变色
private val mLinearShader =
LinearGradient(0f, 0f, mMinWH, mMinWH, colorArr, null, Shader.TileMode.MIRROR)
private var mMaxCount: Int = 0 //最大数
private var mSweepAngle: Float = 360f //扫描过的度数
private var mCountDown: Job? = null
private var mText = ""
/**
* 开始倒计数
*/
fun startCountDown(count: Int, finishFuc: () -> Unit) {
if (context !is FragmentActivity) return
this.mMaxCount = count
mCountDown = countDownByFlow(mMaxCount, (context as FragmentActivity).lifecycleScope,
onTick = {
if (it == 0) mCountDown?.cancel()
mText = it.toString()
mSweepAngle = (it / mMaxCount.toFloat()) * 360
invalidate()
}, onFinish = {
finishFuc.invoke()
})
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
mCenterX = (w / 2).toFloat()
mCenterY = (h / 2).toFloat()
mMinWH = min(w, h).toFloat()
mRadius = (mMinWH - mPaint.strokeWidth) / 2
//设置矩形范围
val strokeHalf = mPaint.strokeWidth / 2
mRect.set(strokeHalf, strokeHalf, mMinWH - strokeHalf, mMinWH - strokeHalf)
}
override fun onDraw(canvas: Canvas?) {
canvas?.let {
mPaint.shader = null
//绘制白色背景圆
canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint)
//绘制灰色背景圆
canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint)
//绘制渐变色弧形 从12点方向开始绘制
mPaint.shader = mLinearShader
canvas.drawArc(mRect, -90f, mSweepAngle, false, mPaint)
//绘制中间倒计时数字
//如果设置的 Align = LEFT,那么baseX = (mMinWH - mTextPaint.measureText(mText)) / 2
val baseX = mCenterX
// 计算Baseline绘制的Y坐标 ,计算方式:画布高度的一半 - 文字总高度的一半
val baseY =
(mCenterY - (mTextPaint.descent() + mTextPaint.ascent()) / 2).toInt()
// 居中画一个文字
canvas.drawText(mText, baseX, baseY.toFloat(), mTextPaint)
}
}
/**
* 使用Flow实现一个倒计时功能
*/
private fun countDownByFlow(
max: Int,
scope: CoroutineScope,
onTick: (Int) -> Unit,
onFinish: (() -> Unit)? = null,
): Job {
return flow {
for (num in max downTo 0) {
emit(num)
if (num != 0) delay(1000)
}
}.flowOn(Dispatchers.Main)
.onEach { onTick.invoke(it) }
.onCompletion { cause -> if (cause == null) onFinish?.invoke() }
.launchIn(scope) //保证在一个协程中执行
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mCountDown?.cancel()
}
}
Activity中:
mBtnStart.setOnClickListener {
mCountDownView.startCountDown(10) {
showToast("倒计时结束")
}
}
注意事项
- 这里实现的倒计时功能是基于
协程Flow
实现的,所以必须保证项目里是支持Kotlin
协程的才能使用; - 如果未使用协程
Flow
,将这里的倒计时逻辑改成CountDownTimer
或者Timer
来实现即可。
完整代码地址
完整代码地址:Kotlin Flow实现一个倒计时功能