对于手机用户来说,Toast是一个司空见惯的东西,它在一个小窗口中显示简短的消息反馈。

然而这是一个手机端独有的东西,在桌面端我们怎么显示类似的提示呢?
与Toast提示关联的概念
Toast本身是用来展示一个简短的消息提示,然而可以用来展示消息提示的组件却有很多。他们与Toast有哪些联系和区别?
MessageBox
MessageBox是电脑端最常见的消息对话框,它通常是打断的 ,甚至模态的。通常在向用户通知或询问重要信息时才会使用,例如,当用户尝试在没有保存文件时尝试直接退出应用询问用户是否保存。

vbs
result = MsgBox("你想知道生命的意义吗,你想真正地活着吗?", vbYesNo, "人生选择")
If result = vbYes Then
CreateObject("WScript.Shell").Run "https://www.qidian.com/book/109222/"
Else
CreateObject("WScript.Shell").Run "https://www.qidian.com/book/3347854/"
End If
这种标准消息提示实现简单,因为很多编程语言中都内置了,在Win32 API中也直接提供,所以在桌面场景使用相当广泛。

Compose中也提供类似的对话框组件,美观且与界面风格统一,但是用起来就没有那么简便了。
Notification
通知(Notification)是展示在用户界面之外的消息,用来给用户提供实时消息提醒,如来自其他用户的消息、新闻、广告和优惠信息等。

在桌面操作系统上也有类似的通知概念。

相较于Toast,通知脱离了当前应用,可以向用户传达一些实时消息或重要状态,但显然更重一些。
气泡提示
在Windows XP时代,在托盘区显示一个icon看上去很高逼格,而在icon上弹个通知就更厉害了。因为Windows早年这个通知是个气泡的形式,因此又称为气泡提示。

短暂出现,自动消失,这是与我们要的Toast概念最接近的东西了,某种意义上来说它是Windows通知中心的前身。然而感觉它更适合后台运行的应用刷存在感,用户本来在窗口里操作,在托盘区弹出提示反而会转移注意力。
Tooltip
Tooltip是桌面应用常见的交互元素,当用户鼠标在按钮或其他可交互组件上悬停时,向用户提示当前操作的相关信息。

状态栏
状态栏是父窗口底部的水平窗口,应用程序在这里可以显示各种状态信息。

状态栏能直接向用户提供可能关心的状态信息,如文档的字数,当前的操作处理进度等。它能给用户提供很多的信息增量,且信息实时更新的。对于短暂的提示就不太合适了。
封装一个Toast工具
桌面端的交互设计很多,但我感觉没有一个可以替代Toast的使用场景,我还是需要一个轻量、自动消失、不打断用户的提示方式。然而Compose显然没有提供这样的标准API,那么我们只好尝试封装一个。
实现界面外观
实现Toast的界面外观是不复杂的,就是搞一段文本显示到界面的下方嘛。

kotlin
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
val messages = remember { mutableStateListOf<ToastMessage>() }
val scope = rememberCoroutineScope()
App(onShowToast = { message ->
scope.launch {
messages.add(message)
delay(message.duration)
messages.remove(message)
}
})
messages.toList().forEachIndexed { index, message ->
Surface(
shape = MaterialTheme.shapes.medium,
color = Color.Black.copy(alpha = 0.7f),
tonalElevation = 16.dp,
shadowElevation = 16.dp,
modifier = Modifier
.padding(bottom = 24.dp + 48.dp * index)
) {
Text(
text = message.text,
color = Color.White,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
}
界面展示没有问题,但是这段代码实用度为0。因为真实的业务场景中我们不可能在App()
方法中直接放置我们的业务代码,肯定嵌套了无数层,要在点击按钮的位置把事件一路透传到根层级,想想都觉得头皮发麻。
CompositionLocal封装
为了支持无需将颜色作为显式参数依赖项传递给大多数可组合项,Compose 提供了 CompositionLocal,可让您创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式。
Compose中提供了CompositionLocal,以便略过中间层,直接将参数传递到使用处。Compose中也大量使用这种方式,传递类似颜色、样式之类的数据。
首先定义个操作接口
kotlin
interface IToastController {
fun show(text: String, duration: Long = 3000)
fun getMessages(): List<ToastMessage>
}
定义CompositionLocal
kotlin
val LocalToastController = staticCompositionLocalOf<IToastController> {
error("No ToastController Provided. Make sure to wrap your app with ToastHost.")
}
封装一个ToastHost用来提供LocalToastController
kotlin
@Composable
fun ToastHost(content: @Composable () -> Unit) {
val coroutineScope = rememberCoroutineScope()
val controller = remember(coroutineScope) {
DefaultToastController(coroutineScope)
}
CompositionLocalProvider(LocalToastController provides controller) {
content()
val messages by remember { derivedStateOf { controller.getMessages() }}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
messages.forEachIndexed { index, message ->
Surface(
shape = MaterialTheme.shapes.medium,
color = Color.Black.copy(alpha = 0.7f),
tonalElevation = 16.dp,
shadowElevation = 16.dp,
modifier = Modifier
.padding(bottom = 24.dp + 48.dp * index)
) {
Text(
text = message.text,
color = Color.White,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
}
}
}
接下来只要把这个ToastHost包装到App()方法的外面,就可以在任意层级弹出Toast了。
kotlin
// main.kt
ToastHost {
App()
}
// 使用样例
@Composable
fun DemoUsage(modifier: Modifier) {
val toastController = LocalToastController.current
Button(onClick = {
toastController.show("Hello World!")
}) {
Text("show toast")
}
}
完整代码我贴到了Github