Android 基于Kotlin Flow实现一个倒计时功能

简介: `Flow`数据流可以按顺序发送多个值,一个倒计时功能刚好符合这种场景,本文就尝试使用`Flow`来实现一个倒计时功能

前情提要

上一篇 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{}开始发送数据之前执行
  • onCompletionflow数据流取消或者结束时执行
  • 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实现一个倒计时功能

相关文章
|
4月前
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
190 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
7月前
|
API 数据处理 Android开发
Android网络请求演变:从Retrofit到Flow的转变过程。
通过这个比喻,我们解释了 Android 网络请求从 Retrofit 到 Flow 的转变过程。这不仅是技术升级的体现,更是反映出开发者在面对并发编程问题时,持续探索和迭求更好地解决方案的精神。未来,还会有更多新的技术和工具出现,我们期待一同 witness 这一切的发展。
199 36
|
6月前
|
NoSQL 应用服务中间件 PHP
布谷一对一直播源码android版环境配置流程及功能明细
部署需基于 CentOS 7.9 系统,硬盘不低于 40G,使用宝塔面板安装环境,包括 PHP 7.3(含 Redis、Fileinfo 扩展)、Nginx、MySQL 5.6、Redis 和最新 Composer。Swoole 扩展需按步骤配置。2021.08.05 后部署需将站点目录设为 public 并用 ThinkPHP 伪静态。开发环境建议 Windows 操作系统与最新 Android Studio,基础配置涉及 APP 名称修改、接口域名更换、包名调整及第三方登录分享(如 QQ、微信)的配置,同时需完成阿里云与腾讯云相关设置。
|
8月前
|
Android开发 开发者 Kotlin
Android实战经验之Kotlin中快速实现MVI架构
MVI架构通过单向数据流和不可变状态,提供了一种清晰、可预测的状态管理方式。在Kotlin中实现MVI架构,不仅提高了代码的可维护性和可测试性,还能更好地应对复杂的UI交互和状态管理。通过本文的介绍,希望开发者能够掌握MVI架构的核心思想,并在实际项目中灵活应用。
344 8
|
9月前
|
编译器 Android开发 开发者
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
Lambda表达式和匿名函数都是Kotlin中强大的特性,帮助开发者编写简洁而高效的代码。理解它们的区别和适用场景,有助于选择最合适的方式来解决问题。希望本文的详细讲解和示例能够帮助你在Kotlin开发中更好地运用这些特性。
154 9
|
12月前
|
安全 Android开发 iOS开发
Android vs iOS:探索移动操作系统的设计与功能差异###
【10月更文挑战第20天】 本文深入分析了Android和iOS两个主流移动操作系统在设计哲学、用户体验、技术架构等方面的显著差异。通过对比,揭示了这两种系统各自的独特优势与局限性,并探讨了它们如何塑造了我们的数字生活方式。无论你是开发者还是普通用户,理解这些差异都有助于更好地选择和使用你的移动设备。 ###
371 3
|
12月前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
184 1
|
12月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
189 4
|
12月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
257 1
|
12月前
|
Android开发 Kotlin
Android面试题之Kotlin中如何实现串行和并行任务?
本文介绍了 Kotlin 中 `async` 和 `await` 在并发编程中的应用,包括并行与串行任务的处理方法。并通过示例代码展示了如何启动并收集异步任务的结果。
170 0

热门文章

最新文章