compose UI(八)一个仿mac的全局消息工具,跟Toast说拜拜
效果:
本文示例代码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参数
总结一下要解决的问题:
- 随时可用(静态方法)
- 子线程可用,并且线程安全(响应式)
- 支持队列,不要丢消息(queue)
- 扩展功能(与日志工具结合,不需要写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)
}
}
}
完整代码,等我什么时候放链接。
8月19修正一下Toast在主线程的说法。Toast是windowManger上的,不会走到checkThread方法,所以不强制需要在主线程执行。
Looper.prepare();
Toast.....
Looper.loop();
更多推荐
所有评论(0)