效果:

消息工具演示
本文示例代码API基于compose UI 1.0.0

Toast在compose的尴尬

Toast需要上下文,这个在Compose 方法中可以通过LocalContext拿到。在其他类中可以用hilt @ApplicationContext 注入application也可以拿到,但是总有不能注入的地方比如object等等情况。Toast最尴尬的是在主线程执行,MVVM架构下,函数在viewModel拿到执行结果,反馈用户总不能一直runOnUiThread,至于handle不用好多年了。kotlin和rxjava的线程切换是能解决问题,但是Toast还有一个最大的问题是,一次只能执行一次。

别提snackbar,它居然还要一个view参数

总结一下要解决的问题:

  1. 随时可用(静态方法)
  2. 子线程可用,并且线程安全(响应式)
  3. 支持队列,不要丢消息(queue)
  4. 扩展功能(与日志工具结合,不需要写Toast一大串)

一个消息框

先建一个object类
POPWindows.kt
我们需要一个线程安全的队列

private val queue = ConcurrentLinkedQueue<String>()

一个StateFlow做观察

private val message = MutableStateFlow("") 

一个加入队列的方法:

    fun postValue(value: String) {
        //存入队列
        queue.add(value)
    }

一个compose 方法做ui

@Composable
fun PopWin() {
	val msg by message.collectAsState()
	if (msg != ""){
		//这里可以换Box,用BoxWithConstraints是我一开始还想搞一些别的花样
		BoxWithConstraints(
            modifier = Modifier
                .padding(10.dp)
                .width(200.dp)
                .height(80.dp)
                .background(color = Color.Black.copy(0.6f))
            ) {
                Text(
                    text = msg,
                    color = Color.White,
                    modifier = Modifier
                        .padding(start = 15.dp, end = 15.dp, top = 10.dp, bottom = 10.dp),
                    maxLines = 2,
                    overflow = TextOverflow.Ellipsis,
                    fontSize = 20.sp
                )
            }
	}
}

写完有一个问题,队列跟MutableStateFlow没有绑定起来,队列是队列,flow是flow。
所以,在加入队列是,我们要查一个MutableStateFlow 是否空闲,修改postValue

	@Synchronized
	fun postValue(value: String) {
        //存入队列
        queue.add(value)
        //是否有空闲显示位
        if(message.value == ""){
        	message.value = queue.poll() ?: ""
        }
    }

这里有很多并发的知识,为什么加锁,为什么先放入队列不先查。
上面的还有一个问题,显示不消失的问题,所以,我们再compose方法中使用协程的方式让它只显示3秒

		LaunchedEffect(key1 = msg) {
            if (msg != "") {
                delay(3000)
                //移除时去查询队列,取值
                val message= queue.poll() ?: ""
            }
        }

差不多一个消息提示框就完成了(大致回忆了一些过程,可能有遗漏)。

使用

消息作为最上层以及全局展示的内容,我们需要在mainAcitivity(多Acitity的需要在每一个)的setContent { }的最后

setContent {
	...
	POPWindows.PopWin()
 }

多个框

我这定了三个框,其实这个也可以动态,只是我这业务暂时3个够用,大家自己扩展。
把message 修改为

private val msgList = MutableList(3) { MutableStateFlow("") }

然后通用代码上面的PopWin 提取成PopItem,给予一个垂直布局
大致是:

@Composable
    private fun PopWin () {
        Column(modifier = modifier) {
            repeat(msgList.size) {
                PopItem(index = it,pos)
            }
        }
    }
    @Composable
    private fun PopItem(index: Int){
    	val msg by msgList[index].collectAsState()
    	......
    	LaunchedEffect(key1 = msg) {
            if (msg != "") {
                delay(3000)
                //移除时去查询队列,取值
                val msgList[index].value = queue.poll() ?: ""
            }
        }
    }

加入滑入滑出动画

加入动画就不要if去判断是否显示,可见交给动画
动画由专门一期,这里不再讲。
阶段性代码如下:

		AnimatedVisibility(
            visible = (msg.trim() != ""),
            enter = slideInHorizontally(
                initialOffsetX = { fullWidth -> -fullWidth },
                animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
            ),
            exit = slideOutHorizontally(
                targetOffsetX = { fullWidth -> -fullWidth },
                animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
            )
        )
        //if (msg != "")
        {
        	......
        }

提供切换显示位置的功能

有多个框会遮挡一些ui,提供位置变换更贴近实际应用
增加一个属性去处理位置相关

	private val position = MutableStateFlow(Position.TOP_LEFT)
	enum class Position {
        TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT
    }
    @Composable
    fun SetPosition(pos: Position) {
        Log.i(TAG, "setPosition")
        position.value = pos
    }

位置的关键是布局
布局我在spinner中讲过。
我们把布局作为Modifier传递给Column,于是,我们再抽取一个私有的compose函数

	@Composable
    fun PopWin() {
        val pos by position.collectAsState()
        var modifier: Modifier = Modifier
        Log.d(TAG, "PopWin: $pos")
        when (pos) {
            Position.TOP_RIGHT -> {
                modifier = Modifier
                    .layout { measurable, constraints ->
                        val placeable = measurable.measure(constraints)
                        layout(constraints.maxWidth, constraints.maxHeight) {
                            placeable.placeRelative(constraints.maxWidth - placeable.width, 0)
                        }
                    }
            }
            Position.BOTTOM_LEFT -> {
                modifier = Modifier.layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    layout(constraints.maxWidth, constraints.maxHeight) {
                        placeable.placeRelative(0, constraints.maxHeight - placeable.height)
                    }
                }
            }
            Position.BOTTOM_RIGHT -> {
                modifier = Modifier.layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    layout(constraints.maxWidth, constraints.maxHeight) {
                        placeable.placeRelative(
                            constraints.maxWidth - placeable.width,
                            constraints.maxHeight - placeable.height
                        )
                    }
                }
            }
            else -> {}
        }
        Wins(modifier = modifier,pos)
    }

    @Composable
    private fun Wins(
        modifier: Modifier,
        pos: Position
    ) {
        Column(modifier = modifier) {
            repeat(msgList.size) {
                PopItem(index = it,pos)
            }
        }
    }

在右边时还需要修改动画,从左滑入改为右滑入。
@Synchronized 修改为显式锁。
LaunchedEffect 中msg要去重,不然前后一样的消息,消息框不消失。
差不多工具类就完成了(应该没漏什么,整个工具类我这也就188行代码)。

扩展

跟日志工具结合。以前我用自定义的日志工具,现在用Timber。用法都差不多,在打印方法上加入

POPWindows.postValue(message)

平时写代码只要打印日志就可以了,自动展示。
具体讲一下Timber的融合

timber

timer需要初始化Tree,一般是在application中
我的BaseApplication

	open fun initLogUtil(){
        if(BuildConfig.DEBUG) {
            Timber.plant(ProxyDebugTree)
        } else {
            Timber.plant(CrashReportingTree)
        }
    }

CrashReportingTree是我自己写的生产环境崩溃处理类,不介绍这个
ProxyDebugTree 是继承Timber.DebugTree() 然后重写log方法:

object ProxyDebugTree: Timber.DebugTree(){

    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        super.log(priority, tag, message, t)
        when (priority) {
            Log.WARN -> POPWindows.postValue("警告:$message")
            Log.ERROR -> POPWindows.postValue("错误:$message")
            else -> POPWindows.postValue(message)
        }
    }
}

完整代码,等我什么时候放链接。

完整代码地址gist.github,可能会打不开


8月19修正一下Toast在主线程的说法。Toast是windowManger上的,不会走到checkThread方法,所以不强制需要在主线程执行。

Looper.prepare();
Toast.....
Looper.loop();
GitHub 加速计划 / compose / compose
39
5
下载
compose - Docker Compose是一个用于定义和运行多容器Docker应用程序的工具,通过Compose文件格式简化应用部署过程。
最近提交(Master分支:4 个月前 )
8f644eea Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> 2 天前
56e92e34 Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> 3 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐