18章开始学习 Compose 中的弹层组件,Snackbar 和 NavigationDrawer 这两个是插槽形式实现的,Popup 和 Dialog 基于 AbstractComposeView 显示的内容在子组合中,但控制和显示还是在原来的组合中。前两个布局是组件中固定的,后面两个使用时都要先在 @Composable 函数中写好,然后再利用 State 来控制。
像这样的
@Composable
fun PopupDemo() {
var showPopup by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxWidth().height(200.dp))
{
if (showPopup) {
Popup(alignment = Alignment.Center) {
Box(modifier = Modifier.size(100.dp).background(Color.LightGray)) {
Text(text = "Popup Content")
}
}
}
Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Red))
Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Yellow))
Button(onClick = { showPopup = !showPopup }) { Text(text = "Switch Popup") }
}
}
利用前面几章学习的内容
AbstractComposeView:
组件需要一个跟 Popup 类似的 Layout ,通过 windowManager add/remove view 实现显示隐藏并且在 Layout 中显示弹出的 Compose UI 。
CompositionContext:
组件中能够使用项目中的主题
remember() + RememberOberver
保证一个 window 中只有一个这样的 Layout 实例 (显示不同 UI 时 给它 set 不同的 content,不需要重复生成)
自动销毁生成的 Layout 实例
来自定义一个可以这样在 Compose 中控制显示隐藏的组件
HUD.showLoading(modal = false)
HUD.dismiss()
这个组件主要是为了加深 19 章内容的理解,实现一种外部控制 Compose 的方式,不要拿来直接使用,没测试过!!! 不要拿来直接使用,没测试过!!! 不要拿来直接使用,没测试过!!!
组件核心
实现显示/隐藏功能,和自动管理功能。
HudLayout
模仿 PopupLayout 实现显示隐藏、设置 content 功能
interface HudLayout {
val composeViewWindowToken: IBinder
val isShowing: State<Boolean>
fun setContent(parent: CompositionContext? = null, content: @Composable () -> Unit)
fun show()
fun dismiss()
fun dispose()
fun resetLayoutParams()
fun setIsFocusable(isFocusable: Boolean)
fun setLayoutGravity(@GravityInt gravity: Int)
fun setLayoutOffset(offset: IntOffset)
}
internal class HudLayoutImpl(
composeView: View,
layoutId: UUID,
) : AbstractComposeView(composeView.context), HudLayout {
override val composeViewWindowToken: IBinder = composeView.applicationWindowToken
init {
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
setTag(R.id.compose_view_saveable_id_tag, "HudLayout:$layoutId")
}
private var _showing = mutableStateOf(false)
override val isShowing: State<Boolean>
get() = _showing
private val windowManager =
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private var windowParams = createLayoutParams()
private var content: @Composable () -> Unit by mutableStateOf({})
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@Composable
override fun Content() {
content()
}
override fun setContent(parent: CompositionContext?, content: @Composable () -> Unit) {
parent?.let {
setParentCompositionContext(it)
}
shouldCreateCompositionOnAttachedToWindow = true
this.content = content
if (isAttachedToWindow) {
createComposition()
}
}
override fun show() {
if (_showing.value) {
dismiss()
}
_showing.value = true
windowManager.addView(this, windowParams)
}
override fun dismiss() {
if (!_showing.value) return
_showing.value = false
disposeComposition()
windowManager.removeViewImmediate(this)
}
override fun dispose() {
disposeComposition()
ViewTreeLifecycleOwner.set(this, null)
ViewTreeViewModelStoreOwner.set(this, null)
setViewTreeSavedStateRegistryOwner(null)
if (_showing.value) {
windowManager.removeViewImmediate(this)
}
}
override fun resetLayoutParams() {
windowParams = createLayoutParams()
updateWindowParams()
}
private fun createLayoutParams(): WindowManager.LayoutParams {
return WindowManager.LayoutParams().apply {
gravity = Gravity.CENTER
type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL
token = composeViewWindowToken
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
format = PixelFormat.TRANSLUCENT
}
}
override fun setIsFocusable(isFocusable: Boolean) {
windowParams.flags = if (!isFocusable) {
windowParams.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
} else {
windowParams.flags and (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv())
}
updateWindowParams()
}
override fun setLayoutGravity(@GravityInt gravity: Int) {
windowParams.gravity = gravity
updateWindowParams()
}
override fun setLayoutOffset(offset: IntOffset) {
windowParams.x = offset.x
windowParams.y = offset.y
updateWindowParams()
}
private fun updateWindowParams(){
if(isAttachedToWindow){
windowManager.updateViewLayout(this,windowParams)
}
}
}
抽出 HudLayout 接口,将显示无关的功能放到 warpper 中
LayoutWrapper
internal class LayoutWrapper(
private val layout: HudLayoutImpl,
private val parentContext: CompositionContext,
val coroutineScope: CoroutineScope, // toast 功能使用的协程 scope
) : RememberObserver,HudLayout by layout{
override fun setContent(parent: CompositionContext?,content: @Composable () -> Unit) {
layout.setContent(parentContext,content)
}
//利用 RememberObserver 实现自动 dispose
override fun onAbandoned() {
dispose()
}
override fun onForgotten() {
dispose()
}
override fun onRemembered() {}
}
HudLayoutManager
管理 Layout , 使用当前 window 中的 layout 来显示弹层
internal object HudLayoutManager {
private val layouts = mutableListOf<LayoutWrapper>()
private var currentLayout: HudLayout? = null
fun show(
isFocusable: Boolean = true,
@GravityInt gravity: Int = Gravity.CENTER,
offset: IntOffset = IntOffset.Zero,
content: @Composable (State<Boolean>) -> Unit
) {
currentLayout?.let {
if (it.isShowing.value) {
it.dismiss()
}
it.setContent{
content(it.isShowing)
}
it.resetLayoutParams()
it.setIsFocusable(isFocusable)
it.setLayoutGravity(gravity)
it.setLayoutOffset(offset)
it.show()
}
}
fun toast(
@GravityInt gravity: Int = Gravity.CENTER,
offset: IntOffset = IntOffset.Zero,
@IntRange(0, 1) duration: Int = 0,
content: @Composable (State<Boolean>) -> Unit
) {
show(false,gravity,offset,content)
(currentLayout as LayoutWrapper?)?.let {
it.coroutineScope.launch {
val delay = if (duration == 0) 500L else 1_000L
delay(delay)
dismiss()
}
}
}
fun dismiss() {
currentLayout?.dismiss()
}
//判断 view 所在的 window 中是否需要创建新的 Layout
fun needNewLayout(view: View): Boolean {
val token = view.applicationWindowToken
return layouts.find { it.composeViewWindowToken == token } == null
}
fun newLayout(
composeView: View,
layoutID: UUID,
parentContext: CompositionContext,
coroutineScope: CoroutineScope
): LayoutWrapper {
val layout = HudLayoutImpl(composeView, layoutID)
val wrapper = LayoutWrapper(layout, parentContext, coroutineScope)
//监听 composeView lifecycle 自动在 manager 中添加删除layout
composeView.findViewTreeLifecycleOwner()!!.lifecycle.addObserver(object :
LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_RESUME) {
currentLayout = wrapper
} else if (event == Lifecycle.Event.ON_DESTROY) {
layouts.remove(wrapper)
if (currentLayout == wrapper) {
currentLayout = null
}
}
}
})
layouts.add(wrapper)
return wrapper
}
}
实现共用主题和 LayoutWrapper 的自动 dispose()
定义 remember 方法并在项目主题中使用
@Composable
fun rememberHudLayout(){
val composeView = LocalView.current
//防止同一个 window 重复生成 layout
if (HudLayoutManager.needNewLayout(composeView)){
val parentContext = rememberCompositionContext()
val layoutId = remember{ UUID.randomUUID()}
val coroutineScope = rememberCoroutineScope()
remember(parentContext,layoutId) {
HudLayoutManager.newLayout(composeView,layoutId,parentContext,coroutineScope)
}
}
}
修改项目中的主题
@Composable
fun ComposeHudTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
){
//放到 MaterialTheme 中,CompositionLocal 通过 CompositionContext
//传递给 Layout 中的子组合
rememberHudLayout()
content()
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeHudTheme {
App()
}
}
}
}
Activity setContent 中使用 ComposeHudTheme ,调用 rememberHudLayout() 会自动生成一个 layout 添加到 manager 中。
onDestroy() 时 layout 自动从 manager 中移除。
Activity 中的父组合 dispose 时 ,LayoutWrapper 通过 RememberObserver 接口自动 dispose。
提供对外使用的 Api
核心中除了大部分都是 internal 修饰的,定义组件对外API
HudComposables
组件默认实现的 ui
@Composable
internal fun LoadingHud(visible: Boolean ) { //想实现显示/隐藏动画,没成功 T_T
val bgColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
val infiniteTransition = rememberInfiniteTransition()
val rotate by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(300),
repeatMode = RepeatMode.Restart
)
)
Surface(
color = bgColor,
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Image(
painter = painterResource(id = R.drawable.loading),
contentDescription = "loading",
modifier = Modifier.graphicsLayer {
rotationZ = rotate
}
)
}
}
}
@Composable
internal fun ToastHud(msg:String){
val bgColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
Surface(
color = bgColor,
shape = RoundedCornerShape(4.dp)
) {
Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Text(text = msg, modifier = Modifier.align(Alignment.Center), textAlign = TextAlign.Center)
}
}
}
Hud
object HUD {
fun showLoading(modal:Boolean = true) {
popup(isFocusable = modal) {
LoadingHud(it.value)
}
}
fun toastMessage(
message: String,
@IntRange(0, 1)duration: Int = Toast.LENGTH_SHORT,
) {
toast(duration = duration) {
ToastHud(msg = message)
}
}
fun popup(
isFocusable: Boolean = true,
@GravityInt gravity: Int = Gravity.CENTER,
offset: IntOffset = IntOffset.Zero,
content: @Composable (State<Boolean>) -> Unit
) {
HudLayoutManager.show(isFocusable, gravity, offset, content)
}
fun toast(
@GravityInt gravity: Int = Gravity.CENTER,
offset: IntOffset = IntOffset.Zero,
@IntRange(0, 1) duration: Int = Toast.LENGTH_SHORT,
content: @Composable (State<Boolean>) -> Unit,
) {
HudLayoutManager.toast(gravity,offset,duration,content)
}
fun dismiss(){
HudLayoutManager.dismiss()
}
}
使用组件的项目中就可以这样调用啦
HUD.toastMessage("Message", Toast.LENGTH_LONG)
HUD.showLoading(modal = false)
HUD.dismiss()
还可以用 popup 或 toast 显示隐藏自定义的 Compose UI
不足:
没有显示隐藏动画
没处理 back 事件和 touch 事件
没有处理显示位置
没有测试功能肯定有 BUG
等等
分享一下思路 源码