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

一、前言

来个效果图,基于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.LayoutParamswindow.setLayout(child.measuredWidth, child.measuredHeight)}private val displayWidth: Intget() {val density = context.resources.displayMetrics.densityreturn (context.resources.configuration.screenWidthDp * density).roundToInt()}private val displayHeight: Intget() {val density = context.resources.displayMetrics.densityreturn (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 -> windowis 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/BOTTOMproperties = AnyPopDialogProperties(direction = DirectionState.BOTTOM),content = {// 这里放你自己的Dialog内容// 如果你需要在你自己的组件中想动画关闭Dialog,请更新isActiveClose},onDismiss = { showDialog = false })}...
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

eCognition易康操作教程(一):如何利用eCognition易康软件进行影像分割之棋盘分割、四叉树分割、光谱差异分割

一、新建工程 使用eCognition新建工程ImageSegmentation,加载影像数据,并编辑图层名称,将Layer 1、Layer 2、Layer 3、Layer 4的 Layer Alias 分别改为 Blue、Green、Red、如图1-1,图1-2所示: 图 1-1 图 1-2 设置加载…

CSDN博客去水印方法

直接在 创作中心->博客 设置这里关了好像就行了,之前方法是找图片链接?后面的一大串字符给去掉。

k8s 自动扩缩容HPA原理及adapter配置详解

大家好,我是蓝胖子,都知道,k8s拥有自动扩缩容机制HPA,我们能够通过配置针对不同的扩缩容场景进行自动扩缩容,往往初学者在面对其中繁多配置的时候会学了又忘记,今天我将会以一种不同的视角,结合…

【2023年研究生数学建模】E题代码与技术文档

2023年数学建模国赛C题 问题1.1Q1_1_judge.mQ1_1_time.m 问题1.2Q1_2_1.mQ1_2_BP.m 技术文档写在最后 目前已经完成E题的第一、二问的代码和文档,分享一部分给大家,欢迎一起来交流谈论哦。 问题1.1 Q1_1_judge.m clc clear dataxlsread(a表.xlsx); for…

七天学会C语言-第六天(指针)

1.指针变量与普通变量 指针变量与普通变量是C语言中的两种不同类型的变量,它们有一些重要的区别和联系。 普通变量是一种存储数据的容器,可以直接存储和访问数据的值。: int num 10; // 定义一个整数型普通变量num,赋值为10在例…

HTTP 协商缓存 ETag、If-None-Match

(1)浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone header加上ETag。 ETag是服务器根据当前请求的资源生成的一个唯一标识。 这个唯一标识是一个字符串,只要资源有变化这个串就不同&#xff…

一文带你实现从PDF到Word文件的相互转换

一文带你实现从PDF到Word文件的相互转换 01. 前期准备 模块安装 pip install pdf2docx or使用国内镜像源进行安装(清华的镜像源) pip install pdf2docx -i https://pypi.tuna.tsinghua.edu.cn/simple 02. 模块介绍 pdf2docx是一个Python模块&#xff0…

荣湃隔离放大器Pai8300 5.0kVRMS完美代替TI AMC1301

深力科推出了用于电流检测的隔离放大器Pai8300,实现对电流快速、精确的检测。Pai8300采用全新架构,叠加荣湃专利智能分压技术,保证低功耗,高精度和良好的隔离性能,广泛应用于电机驱动,逆变器,变…

Intel酷睿和AMD锐龙

Intel酷睿系列,主要分i3、i5、i7、i9 如:Intel 酷睿i5 10210U i5:品牌修饰符。 10:代次指示符。 210:sku编号。 常见后缀: G1-G7:集显等级。 U:低功耗。 H:标压版…

AI创作工具-AI创作工具技术解读

创作是广告、文章、小说、社交媒体内容等各个领域的关键,但它通常需要创作者花费大量时间和精力,思考、编写和编辑内容。有时候,创作者可能面临写作灵感枯竭、时间紧迫或需要大量内容的情况。 添加图片注释,不超过 140 字&#xf…

2023华为杯数学建模研赛E题全解析

2023华为杯数学建模研赛E题解析,完整版已出!!! 包含所有模型、代码、结果,39页技术文档,详细内容如下! 免费版链接已放在下面,需要的同学可以直接自取~ 【云顶数模】2023研究生数学建模免费链接: https://pan.baid…

Java反序列化和php反序列化的区别

文章目录 PHP反序列化漏洞反序列化漏洞什么是反序列化漏洞?修改序列化后的数据,目的是什么? Java反序列化漏洞反序列化漏洞 PHP反序列化漏洞 序列化存在的意义是为了传输数据/对象,类是无法直接进行传输的。通过序列化后转换为字…

数据结构-----栈(栈的初始化、建立、入栈、出栈、遍历、清空等操作)

目录 前言 栈 1.定义 2.栈的特点 3.栈的储存方式 3.1数组栈 3.2链栈 4.栈的基本操作(C语言) 4.1初始化 4.2判断是否满栈 4.3判断空栈 4.4 入栈 4.5 出栈 4.6获取栈顶元素 4.7遍历栈 4.8清空栈 完整代码示例 前言 大家好呀!今天我…

python使用websocket实现多端数据同步,多个websocket同步消息,断开链接自动清理

我使用的是flask_sock这个模块,我的使用场景是:可以让数据多端实时同步。在游戏控制后台和游戏选手的ipad上都可以实时调整角色的技能和点数什么的,所以需要这样的一个功能来实现数据实时同步。 下面是最小的demo案例: from fla…

【小沐学NLP】关联规则分析Apriori算法(Mlxtend库,Python)

文章目录 1、简介2、Mlxtend库2.1 安装2.2 功能2.2.1 User Guide2.2.2 User Guide - data2.2.3 User Guide - frequent_patterns 2.3 入门示例 3、Apriori算法3.1 基本概念3.2 apriori3.2.1 示例 1 -- 生成频繁项集3.2.2 示例 2 -- 选择和筛选结果3.2.3 示例 3 -- 使用稀疏表示…

UE4 C++ 使用第三方库(动态库) 详解

目录 1 代码共享的方式2 使用三方库2.1 准备一个动态库(包含.h;.lib;.dll)2.2 创建一个UE C工程2.3 配置三方库 1 代码共享的方式 在使用三方库之前,先介绍一下三方库的由来,以及为什么需要三方库。就从程序员共享代码成果开始讲述…

IP 协议

IP协议格式 四位版本号 用来表示IP协议的版本,现有的IP协议只有两个版本,IPv4,IPv6,其他版本只在实验室中存在,没有大规模商用 四位首部长度 设定和TCP一样,IP报头是可变长的,IP报头又是带有选项(可以有,可以没有)的,这里的单位也是4个字节,也就是最大有16*464个字节的长度 …

PHP8中调换数组中的键值和元素值-PHP8知识详解

在php8中使用array_flip()函数可以调换数组中的键值和元素值。 在PHP8中使用array_flip()函数可以调换数组中的键值和元素值&#xff0c;示范代码如下&#xff1a; <?php$stu array("子涵"> 001,"欣怡"> 002,"梓涵">003,"晨曦…

华为云云耀云服务器L实例评测|centos7.9在线使用cloudShell下载rpm解压包安装mysql并开启远程访问

文章目录 ⭐前言⭐使用华为cloudShell连接远程服务器&#x1f496; 进入华为云耀服务器控制台&#x1f496; 选择cloudShell ⭐安装mysql压缩包&#x1f496; wget下载&#x1f496; tar解压&#x1f496; 安装步骤&#x1f496; 初始化数据库&#x1f496; 修改密码&#x1f4…

外卖小程序开发指南:打造完美的点餐体验

第一步&#xff1a;项目设置和初始化 首先&#xff0c;您需要选择一个适合您的开发平台&#xff0c;例如微信小程序、支付宝小程序或其他移动应用平台。接下来&#xff0c;创建一个新的小程序项目&#xff0c;并初始化所需的文件和目录。 示例代码&#xff08;微信小程序&am…