Jetpack Compose干货,如何让Compose Dialog从屏幕任意方向进入

news2024/10/7 8:31:28

一、前言

来个效果图,基于Compose Dialog,最终要实现的库能力如下:

底部/顶部/左侧/右侧.gif

这里使用的是这个包下面的:
androidx.compose.ui.window.Dialog

androidx.compose.material3.AlertDialog它内部调用的也是androidx.compose.ui.window.Dialog

不想阅读文章的,可以直接滑到文章末尾,我提供了源码和集成指南。

谷歌提供给我们的compose-ui-dialog,并没有看到能够控制从屏幕底部进入的方法,都是最基础的属性和参数。

// androidx.compose.ui.window.Dialog
@Composable
fun Dialog(
    onDismissRequest: () -> Unit,
    properties: DialogProperties = DialogProperties(),
    content: @Composable () -> Unit
) {
  ....
}

// androidx.compose.material3.AlertDialog
@Composable
fun AlertDialog(
    onDismissRequest: () -> Unit,
    confirmButton: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    dismissButton: @Composable (() -> Unit)? = null,
    icon: @Composable (() -> Unit)? = null,
    title: @Composable (() -> Unit)? = null,
    text: @Composable (() -> Unit)? = null,
    shape: Shape = AlertDialogDefaults.shape,
    containerColor: Color = AlertDialogDefaults.containerColor,
    iconContentColor: Color = AlertDialogDefaults.iconContentColor,
    titleContentColor: Color = AlertDialogDefaults.titleContentColor,
    textContentColor: Color = AlertDialogDefaults.textContentColor,
    tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
    properties: DialogProperties = DialogProperties()
) {
 ....
}

扫一眼源码,并没有看到能够控制Dialog从哪个方向弹出来,那么我们应该如何解决这个问题呢,毕竟不能直接使用Dialog我就很难受😁,你们是不是一样,请在评论区留言讨论🖋。

二、默认Dialog能怎么玩

不要认为我在凑字数划水,不过说实话确实有点像,不感兴趣的,可以直接跳到目录三

既然用默认Dialog,那就中规中矩的玩😁,来个守规矩的Dialog示例

@Composable
fun MinimalDialog(onDismissRequest: () -> Unit) {
    Dialog(onDismissRequest = { onDismissRequest() }) {
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .padding(16.dp),
            shape = RoundedCornerShape(16.dp),
        ) {
            Text(
                text = "This is a minimal dialog",
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center),
                textAlign = TextAlign.Center,
            )
        }
    }
}

AlertDialog(
        onDismissRequest = onDismissRequest,
        icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
        title = {
            Text(text = "Title")
        },
        text = {
            Text(
                "This area typically contains the supportive text " +
                        "which presents the details regarding the Dialog's purpose."
            )
        },
        confirmButton = {
            TextButton(
                onClick = {
                   // 自己实现点击事件
                }
            ) {
                Text("Confirm")
            }
        },
        dismissButton = {
            TextButton(
                onClick = {
                   // 自己实现点击事件
                }
            ) {
                Text("Dismiss")
            }
        }
    )

我们可以看到都是基础到不能再基础的参数设置了,改改参数试试?

试试就试试,谁怕谁啊,把第一个Dialog示例的Modifier修饰符修改一下使用fillMaxSize试试咯。

我们看到宽度、高度没有真正的全屏,宽度这个好解决,只需要配置一下DialogProperties(usePlatformDefaultWidth = false) 即可(这里读者自己试试吧),那么高度怎么解决呢?

我们需要延伸到系统栏,众所周知Android目前除了有小横条之外,还保留了虚拟键的三大金刚,如果无法把内容延伸到系统栏,这就不是真的全屏,而是伪全屏啊,看着就难受啊。

有同学又会说:我直接用BottomSheetScaffold不也行吗?自己封装一个BottomSheetContent放在Scaffold组件里面。

这些都要根据不同业务重度封装弹出组件了,有些业务你可能需要xml+composeView的形式,用Dialog具有更通用性。

下面我们来研究一下,如何继续下面的目录内容。

三、Dialog实现分析

打开我们的AndroidDialog.android.kt

@Composable
fun Dialog(
    onDismissRequest: () -> Unit,
    properties: DialogProperties = DialogProperties(),
    content: @Composable () -> Unit
) {
    ...
    val dialog = remember(view, density) {
        DialogWrapper(view, ...).apply {
            setContent(composition) {
                DialogLayout(Modifier.semantics { dialog() }) {
                    currentContent()
                }
            }
        }
    }
    ...
}

我们打开DialogWrapper方法,看到setContentView它内部仍然是window.setContentView(View)

@OptIn(ExperimentalComposeUiApi::class)
private class DialogWrapper(
    private val composeView: View,
    ...
) : ComponentDialog(...){
    init {
      ...
      dialogLayout = DialogLayout(context, window)
      ...
      setContentView(dialogLayout)
     ...
    }
}

然后呢,我们打开DialogLayout看看里面实现了什么:

@Suppress("ViewConstructor")
private class DialogLayout(
    context: Context,
    override val window: Window
) : AbstractComposeView(context), DialogWindowProvider {
    ...
    override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        if (usePlatformDefaultWidth) {
            super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
        } else {
            val displayWidthMeasureSpec =
                MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.AT_MOST)
            val displayHeightMeasureSpec =
                MeasureSpec.makeMeasureSpec(displayHeight, MeasureSpec.AT_MOST)
            super.internalOnMeasure(displayWidthMeasureSpec, displayHeightMeasureSpec)
        }
    }

    override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.internalOnLayout(changed, left, top, right, bottom)
        val child = getChildAt(0) ?: return
        // 设置WindowManager.LayoutParams
        window.setLayout(child.measuredWidth, child.measuredHeight)
    }

    private val displayWidth: Int
        get() {
            val density = context.resources.displayMetrics.density
            return (context.resources.configuration.screenWidthDp * density).roundToInt()
        }

    private val displayHeight: Int
        get() {
            val density = context.resources.displayMetrics.density
            return (context.resources.configuration.screenHeightDp * density).roundToInt()
        }
    ...
}

我们从上面代码可以看到 usePlatformDefaultWidth = false,不使用平台默认的宽度,可以实现全屏宽度。
代码中我们又看到了内部有2个变量:displayWidth和displayHeight,他们两个返回的值分别是什么意思,建议大家去看源码注释非常详细,注释内容比较多,这里精简一下,大概的意思就是:

即使你调用:Window#setDecorFitsSystemWindows(boolean)

screenWidthDp、screenHeightDp 它返回的width、height
不包含WindowInsets衬区的大小在内的。

点击查看什么是WindowInsets

e2e-intro.gif

我们是不是可以通过decoreView获取真实的宽度和高度,答案是肯定的,那么我们如何通过Compose实现呢?

四、Dialog从屏幕底部进入

继续上面的内容,我们应该如何获取Window,这里我们需要分别获取2个Window:

一个是Dialog的Window,一个是Activity的Window。

在埋着头写无穷无尽的业务代码的时候😁,任何人都讨厌问十万个为什么问题的同学,因为这个时候人的怒气值是101%的,但是一但闲下来没事做的时候,就喜欢十万个为什么。

理由一:我们不能直接修改dialog.window = activityWindow,即使可以也不能直接使用type不一样晓得不,有人又会问什么type,请读者自己查看源码:window.attributes.type,不可能全部都介绍一遍。

理由二:默认的dialog的window里面的属性无法适应WindowInsets衬区

理由N:你自己想一想…

回到正题,那么如何获取Dialog的Window呢?

细心的同学可能这里已经发现了,因为他是真的在参考源码,阅读文章的,我们上面的代码刚介绍了DialogLayout,不知道你们注意了没,它实现了DialogWindowProvider 接口,而这个接口只有一个变量就是Window,这很重要

interface DialogWindowProvider {
    val window: Window
}

同时我们还看到 DialogLayout 继承 AbstractComposeView,说到这里,我猜应该有同学知道怎么实现了,有点懵的😳,继续往下看。

1、获取Dialog的Window

我们使用Compose的Dialog一般是这样填充视图内容:

Dialog(
    ...
    content = {
       // 这里放可组合项视图
    }
)

如果你不知道Compose UI创建布局绘制的流程,你可以点击查看我这一篇文章,我们可以通过LocalView.current获取当前的AndroidComposeView,刚刚上面讲的内容,忘记的请动动鼠标往上翻一下。

我们定义一个方法返回Dialog的Window的方法:

@Composable
private fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window

2、获取Activity的Window

我们可以通过Context下手,通过递归的方式,判断是不是Activity,如果是则可以获取window了,那么可以定义如下的方法:

@Composable
private fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow()

private tailrec fun Context.getActivityWindow(): Window? = when (this) {
    is Activity -> window
    is ContextWrapper -> baseContext.getActivityWindow()
    else -> null
}

3、实现底部动画弹出Dialog

有了上面的内容支撑之后,我们可以往下实现了,首先需要定义一个全屏的Dialog可组合项:

@Composable
private fun DialogFullScreen(
    onDismissRequest: () -> Unit,
    content: @Composable () -> Unit
) {
    Dialog(
        onDismissRequest = onDismissRequest,
        properties = ...,
        content = {
            // 这里放Dialog的窗口内容...
        }
    )
}

一点点加入代码,把获取Window的代码放在content中,然后在SideEffect去更新DialogWindow:

@Composable
private fun DialogFullScreen(...) {
   Dialog(
       ...
       properties = DialogProperties(
           usePlatformDefaultWidth = true,
           decorFitsSystemWindows = false
       ),
       content = {
           val activityWindow = getActivityWindow()
           val dialogWindow = getDialogWindow()  
            SideEffect {
                if (activityWindow != null && dialogWindow != null) {
                    val attributes = WindowManager.LayoutParams()
                    // 复制Activity窗口属性
                    attributes.copyFrom(activityWindow.attributes)
                    // 这个一定要设置
                    attributes.type = dialogWindow.attributes.type
                    // 更新窗口属性
                    dialogWindow.attributes = attributes

                    // 设置窗口的宽度和高度,这段代码Dialog源码中就有哦,可以自己去查看
                    dialogWindow.setLayout(
                        activityWindow.decorView.width,
                        activityWindow.decorView.height
                    )
                }
            }    
       }
    )
}

可能这个时候又有同学疑问了,不对啊,怎么上面你说宽度铺满,告诉我们usePlatformDefaultWidth设置false,这里怎么是设置为true了?

问的好,请看源码,设置false,会走源码内部的新测量分支,会使用displayWidth、displayHeight,这个是不含WindowInsets衬区的,因为它的 mode = MeasureSpec.AT_MOST,所以这里我们不用,不然竖屏高度,或者横屏宽度,你懂得,还需要再说的更详细嘛,大家都懂的。

这个时候我们如果设置完Dialog内容视图之后,你会发现,Dialog自带的变暗背景色和点击空白区关闭Dialog失效了,并且是没有动画的,我们这个时候可以使用AnimatedVisibility来让内容通过动画的形式进入屏幕。

什么时候执行动画呢,我们只需要在dialogWindow.setLayout代码后面更新visible状态变量就行了。

AnimatedVisibility(
    modifier = Modifier.pointerInput(Unit) {},
    visible = isAnimateLayout,
    enter = slideInVertically(initialOffsetY = { it }),
    exit = fadeOut() + slideOutVertically(targetOffsetY = { it }),
) {
   content()
}

然后暗色背景渐入渐出,我们加个蒙层背景视图即可,使用Animatable更新暗色背景渐入渐出。

大家自行润色一下代码细节就可以使用了,源码在文章目录五查看:

    DialogFullScreen(
        onDismissRequest = onDismiss,
        properties = properties
    ) {
        Column(
            modifier = modifier.navigationBarsPadding(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            content()
        }
    }

我们再更新一下代码,增加控制方向的属性,这样就可以控制它是从:底部/顶部/左侧/右侧 弹出。

如果是屏幕中间弹出,建议你直接使用默认的Dialog可组合项即可。

同样的,增加控制方向的属性之后,只需要更新AnimatedVisibility即可。

底部/顶部/左侧/右侧.gif

五、如何集成

1、源码

AnyPopDialog-Compose

2、集成

implementation("io.github.TheMelody:any_pop_dialog_compose:1.0.0")

3、用法

@Composable
fun TestXXXX() {
    var showDialog by remember { mutableStateOf(false) }
    if (showDialog) {
        var isActiveClose by remember { mutableStateOf(false) }
        AnyPopDialog(
            modifier = Modifier.fillMaxWidth().background(...),
            isActiveClose = isActiveClose,
            // 根据你自己的功能,调整进入方向即可,支持:TOP/LEFT/RIGHT/BOTTOM
            properties = AnyPopDialogProperties(direction = DirectionState.BOTTOM),
            content = {
                // 这里放你自己的Dialog内容
                // 如果你需要在你自己的组件中想动画关闭Dialog,请更新isActiveClose
            },
            onDismiss = { showDialog = false }
        )
    }
    ...
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1033772.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Centos7 安装部署 Kubernetes(k8s) 高可用集群

1:基础环境准备 宿主机系统集群角色服务器IP主机名称容器centos7.6master192.168.2.150ks-m1dockercentos7.6master192.168.2.151ks-n1dockercentos7.6master192.168.2.152ks-n2docker 1.1 服务器初始化及网络配置 VMware安装Centos7并初始化网络使外部可以访问*…

No2.详解【2023年全国大学生数学建模竞赛】C题——蔬菜类商品的自动定价与补货决策(代码 + 详细输出 + 数据集代码 下载)

只有不回避痛苦和迷茫的人,才有资格去谈乐观和坚定。命运不会厚待谁,悲喜也不会单为你准备。 🎯作者主页: 追光者♂🔥 🌸个人简介: 💖[1] 计算机专业硕士研究生💖 🌿[2] 2023年城市之星领跑者TOP1(哈尔滨)🌿 🌟[3] 2022年度博客之星人工智能…

爬虫获取接口数据

上一讲讲的是获取静态网页数据的教程,适用于我们要爬取的数据在网页源代码中出现,但是还是有很多的数据是源代码中没有的,需要通过接口访问服务器来获得,下面我就来讲讲如何爬取这类数据。 以巨潮资讯网爬取比亚迪企业年报为例。…

解决windows端口占用

WINR打开cmd窗口;输入命令查看哪个进程占用,8848为要查询占用的端口号:netstat -ano | findstr 8848; 3.杀死进程,输入taskkill /f /t /im 10672 其中10672为上面命令查出来的进程号。

24. 图论 - 图的表示种类

Hi,你好。我是茶桁。 之前的一节课中,我们了解了图的来由和构成,简单的理解了一下图的一些相关概念。那么这节课,我们要了解一下图的表示,种类。相应的,我们中间需要穿插一些新的知识点用于更好的去理解图…

Python异步编程并发执行爬虫任务,用回调函数解析响应

一、问题:当发送API请求,读写数据库任务较重时,程序运行效率急剧下降。 异步技术是Python编程中对提升性能非常重要的一项技术。在实际应用,经常面临对外发送网络请求,调用外部接口,或者不断更新数据库或文…

漏刻有时数据可视化Echarts组件开发(31):geomap伪3D配置示例

echarts.registerMap("丹东", getData());let data = getData().features.map((item) => {return {name: item.properties.name,};});const points = [[116.289929,40.265374],[116.754101,40.063877],[116.229504,39.764735],[115.883434,39.899721]]let option …

Unity的AB包相关

1、打包 在这个界面左边右键,CreateNewBundle 将要打包的模型制作成预设体 在下面勾选 选好平台路径,点击Build 2、加载AB包 public class ABTest : MonoBehaviour {// Start is called before the first frame updatevoid Start(){//加载AB包AssetB…

pymysql调用存储过程

视频版教程 Python操作Mysql数据库之pymysql模块技术 我们首先创建一个简单的存储过程 DELIMITER //CREATE PROCEDURE test_add(m INT,n INT, OUT result INT) BEGIN SET resultmn;END; //测试: SET s0; CALL test_add(1,2,s); SELECT sPymysql调用存储过程实现&…

从植隆业务中台到金蝶云星空通过接口配置打通数据

从植隆业务中台到金蝶云星空通过接口配置打通数据 数据源系统:植隆业务中台 核心能力以数字化形式沉淀为各种服务中心,其目的是“提供企业能够快速,低成本创新的能力”。业务中台的核心是“构建企业共享服务中心”,其过程是通过业务板块之间的…

代码随想录算法训练营第57天| 647. 回文子串,516.最长回文子序列,动态规划总结

链接: 647. 回文子串 链接: 516.最长回文子序列 链接: 动态规划总结 647. 回文子串 理解dp数组的含义很重 class Solution {public int countSubstrings(String s) {char[] chars s.toCharArray();boolean[][] dp new boolean[s.length()][s.length()];int res 0;// 遍…

FL Studio21水果编曲软件怎么下载中文版?

FL Studio21这款软件在国内被广泛使用,因此又被称为"水果"。它提供音符编辑器,可以针对作曲者的要求编辑出不同音律的节奏,例如鼓、镲、锣、钢琴、笛、大提琴、筝、扬琴等等任何乐器的节奏律动。此外,它还提供了方便快捷…

以小见大,彻底理解 cookie,session,token 之间的关系,通俗易懂

发展史 1、很久很久以前,Web 基本上就是文档的浏览而已,既然是浏览,作为服务器, 不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的 HTTP 协议,就是请求加响应,尤其是我不用记…

菜单栏图标管理软件Bartender mac 5.0.10中文版介绍

Bartender mac是一款菜单栏图标管理软件,功能强大,可以快速管理菜单栏的图标、显示内容和时间,只需在菜单栏中滑动或滚动、单击菜单栏,或者如果您愿意,只需将鼠标悬停即可立即访问隐藏的菜单栏项目。 Bartender软件介绍…

识别准确率达 95%,华能东方电厂财务机器人实践探索

摘 要:基于华能集团公司大数据与人工智能构想理念,结合东方电厂实际工作需要,财务工作要向数字化、智能化纵深推进,随着财务数字化转型和升级加速,信息化水平不断提升,以及内部信息互联互通不断加深&#x…

AI机器人写作-AI机器人写作技术和工具

在这个数字化时代,文本创作是信息传播的主要方式之一。然而,对于许多人来说,写作可能是一项具有挑战性和耗时的任务。文本创作是广告、文章、社交媒体、小说等领域的核心,但却常常需要大量时间和精力来思考、编写和编辑。许多人可…

Leetcode 剑指 Offer II 045. 找树左下角的值

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定一个二叉树的 根节点 root,请找出该二叉树的 最底…

C语言指针变量的引用距离

本段代码&#xff0c;测试&#xff0c;C的函数传参中&#xff0c;形参是基础类型参数和地址参数&#xff0c;对于实参的值影响。 #include <stdio.h> add(int a,int b){a;b;printf("add副本a%d\n",a);printf("add副本b%d\n",b);printf("副本ca…

ubuntu右上角的网络连接图标消失解决办法

ubuntu更新了几个文件后&#xff0c;我的ubuntu系统右上角的网络连接图标就消失了&#xff0c;然后怎么也找不到了&#xff0c;怎么办呢&#xff1f; 1、按快捷键ctrlaltt打开终端 2、按以下顺序输入如下的命令行 sudo service network-manager stop sudo rm /var/lib/Netw…

http和https包解析

简介 1.使用工具 bp 2.http和https的resquest和response的包头解析 请求报文 request 请求头 请求行 请求正文和其他的 response 响应包