Compose太香了,不想再写传统 xml View?教你如何在已有View项目中混合使用Compose

news2024/11/28 18:49:52

前言

在我的文章 记一次 kotlin 在 MutableList 中使用 remove 引发的问题 中,我提到有一个功能是将多张动图以N宫格的形式拼接,并且每个动图的宽保证一致,但是高不保证一致。

在原本项目中我使用的是传统 view 配合 RecyclerView 和 GridLayout 布局方式进行拼图的预览,但是这会存在一个问题。

实际上是这样排列的:

s1.png

但是预想中应该是这样排列:

s2.png

可以看到,我们的需求应该是完全按照顺序来排列,但是瀑布流布局却是在每一行中,哪一列的高度最小就优先排到哪一列,而不是严格按照给定顺序排列。

显然,这是不符合我们的需求的。

我曾经试图找到其他的替代方式实现这个效果,或者试图找到 GridLayout 的某个参数可以修改为按顺序排列,但是一直无果。

最终,只能用自定义布局来实现我想要的效果了。但是对于原生 View 的自定义布局非常麻烦,我也没有接触过,所以就一直不了了之了。

最近一直在学习 compose ,发现 compose 的自定义布局还挺简单的,所以就萌生了使用 compose 的自定义布局来实现这个需求的想法。

由于这个项目是使用的传统 View ,并且已经上线运行很久了,不可能一蹴而就直接全部改成使用 compose,并且这个项目也还挺复杂的,移植起来也不简单。所以,我决定先只将此处的预览界面改为使用 compose,也就是混合使用 View 与 compose。

开始移植

compose 自定义布局

在开始之前我们需要先使用 compose 编写一个符合我们需求的自定义布局:

@Composable
fun TestLayout(
    modifier: Modifier = Modifier,
    columns: Int = 2,
    content: @Composable ()->Unit
) {
    Layout(
        modifier = modifier,
        content = content,
    ) { measurables: List<Measurable>, constrains: Constraints ->
        val itemWidth = constrains.maxWidth / columns
        val itemConstraints = constrains.copy(minWidth = itemWidth, maxWidth = itemWidth)
        val placeables = measurables.map { it.measure(itemConstraints) }

        val heights = IntArray(columns)
        var rowNo = 0
        layout(width = constrains.maxWidth, height = constrains.maxHeight){
            placeables.forEach { placeable ->
                placeable.placeRelative(itemWidth * rowNo, heights[rowNo])
                heights[rowNo] += placeable.height

                rowNo++
                if (rowNo >= columns) rowNo = 0
            }
        }
    }
}

这个自定义布局有三个参数:

modifier Modifier 这个不用过多介绍

columns 表示一行需要放多少个 item

content 放置于其中的 itam

布局的实现也很简单,首先由于每个子 item 的宽度都是一致的,所以我们直接定义 item 宽度为当前布局的最大可用尺寸除以一行的 item 数量: val itemWidth = constrains.maxWidth / columns

然后创建一个 Array 用于存放每一列的当前高度,方便后面摆放时计算位置: val heights = IntArray(columns)

接下来遍历所有子项 placeables.forEach { placeable -> } 。并使用绝对坐标放置子项,且 x 坐标为 宽度乘以当前列, y 坐标为 当前列高度 placeable.placeRelative(itemWidth * rowNo, heights[rowNo])

最后将高度累加 heights[rowNo] += placeable.height 并更新列数到下一列 rowNo++if (rowNo >= columns) rowNo = 0

下面预览一下效果:

@Composable
fun Test() {
    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        TestLayout {
            Rectangle(height = 120, color = Color.Blue, index = "1")
            Rectangle(height = 60, color = Color.LightGray, index = "2")
            Rectangle(height = 140, color = Color.Yellow, index = "3")
            Rectangle(height = 80, color = Color.Cyan, index = "4")
        }
    }
}


@Composable
fun Rectangle(height: Int, color: Color, index: String) {
    Column(
        modifier = Modifier
            .size(width = 100.dp, height = height.dp)
            .background(color),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = index, fontWeight = FontWeight.ExtraBold, fontSize = 24.sp)
    }
}


@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
@Composable
fun PreviewTest() {
    Test()
}

效果如下:

s2.png

完美符合我们的需求。

增加修改 gradle 配置

为了给已有项目增加 compose 支持我们需要增加一些依赖以及更新一些参数配置。

检查 AGP 版本

首先,我们需要确保 Android Gradle Plugins(AGP)版本是最新版本。

如果不是的话需要升级到最新版本,确保 compose 的使用,例如我写作时最新稳定版是 7.3.0

点击 Tools - AGP Upgrade Assistant 打开 AGP 升级助手,选择最新版本后升级即可。

检查 kotlin 版本

不同的 Compose Compiler 版本对于 kotlin 版本有要求,具体可以查看 Compose to Kotlin Compatibility Map

例如,我们这里使用 Compose Compiler 版本为 1.3.2 则要求 kotlin 版本为 1.7.20

修改配置信息

首先确保 API 等级大于等于21,然后启用 compose:

buildFeatures {
    // Enables Jetpack Compose for this module
    compose true
}

配置 Compose Compiler 版本:

composeOptions {
    kotlinCompilerExtensionVersion '1.3.2'
}

并且确保使用 JVM 版本为 Java 8 , 需要修改的所有配置信息如下:

android {
    defaultConfig {
        ...
        minSdkVersion 21
    }

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose true
    }
    ...

    // Set both the Java and Kotlin compilers to target Java 8.
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }

    composeOptions {
        kotlinCompilerExtensionVersion '1.3.2'
    }
}

添加依赖

dependencies {
    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.5.1'
    // Compose Material Design
    implementation 'androidx.compose.material:material:1.2.1'
    // Animations
    implementation 'androidx.compose.animation:animation:1.2.1'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.2.1'
    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
    // UI Tests
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.2.1'
}

自此所有配置修改完成,Sync 一下吧~

将 view 替换为 compose

根据我们的需求,我们需要替换的是用于预览拼图的 RecyclerView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragment.gifTools.JointGifPreviewFragment">

    <!-- ... -->

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/jointGif_preview_recyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="48dp"
        android:layout_marginBottom="8dp"
        android:transitionName="shared_element_container_gifImageView"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!-- ... -->

</androidx.constraintlayout.widget.ConstraintLayout>

将其替换为承载 compose 的 ComposeView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragment.gifTools.JointGifPreviewFragment">

    <!-- ... -->

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/jointGif_preview_recyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="48dp"
        android:layout_marginBottom="8dp"
        android:transitionName="shared_element_container_gifImageView"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!-- ... -->

</androidx.constraintlayout.widget.ConstraintLayout>

在原本初始化 RecyclerView 的地方,将我们上面写好的 composable 设置进去。

将:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // ...

    initRecyclerView()

    // ...
}

改为:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // ...

    bind.jointGifPreviewRecyclerView.setContent {
    	Test()
	}

    // ...
}

ComposeViewsetContent(content: @Composable () -> Unit) 方法只有一个 content 参数,而这个参数是一个添加了 @Composable 注解的匿名函数,也就是说,在其中我们可以正常的使用 compose 了。

更改完成后看一下运行效果:

s3.png

可以看到,混合使用完全没有问题。

但是这里我们使用的是写死的 item 数据,而不是用户动态选择的图片数据,所以下一步我们需要搞定 compose 和 view 之间的数据交互。

数据交互

首先,因为我们需要显示的动图,所以需要引入一下对动图的支持,这里我们直接使用 coil 。

引入 coil 依赖:

// coil compose
implementation 'io.coil-kt:coil-compose:2.2.2'
// coil gif 解码支持
implementation 'io.coil-kt:coil-gif:2.2.2'

定义一个用于显示 gif 的 composable:

@Composable
fun GifImage(
    uri: Uri,
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current
    val imageLoader = ImageLoader.Builder(context)
        .components {
            if (SDK_INT >= 28) {
                add(ImageDecoderDecoder.Factory())
            } else {
                add(GifDecoder.Factory())
            }
        }
        .build()

    Image(
        painter = rememberAsyncImagePainter(model = uri, imageLoader = imageLoader),
        contentDescription = null,
        modifier = modifier,
        contentScale = ContentScale.FillWidth
    )
}

其中,rememberAsyncImagePaintermodel 参数支持多种类型的图片,例如:File Uri String Drawable Bitmap 等,这里因为我们原本项目中使用的是 Uri ,所以我们也定义为使用 Uri。

而 coil 对于不同 API 版本支持两种解码器 ImageDecoderDecoderGifDecoder 按照官方的说法:

Coil includes two separate decoders to support decoding GIFs. GifDecoder supports all API levels, but is slower. ImageDecoderDecoder is powered by Android’s ImageDecoder API which is only available on API 28 and above. ImageDecoderDecoder is faster than GifDecoder and supports decoding animated WebP images and animated HEIF image sequences.

简单翻译就是 GifDecoder 支持所有 API 版本,但是速度较慢; ImageDecoderDecoder 仅支持 API >= 28 但是速度较快。

因为我们的需求是宽度一致,等比缩放长度,所以需要给 Image 加上缩放类型 contentScale = ContentScale.FillWidth

之后把我们的自定义 Layout 改一下名字,其他内容不变: SquareLayout

增加一个 JointGifSquare 用作界面入口:

@Composable
fun JointGifSquare(
    columns: Int,
    uriList: ArrayList<Uri>,
    ) {
    SquareLayout(columns = columns) {
        uriList.forEachIndexed { index, uri ->
            GifImage(
                uri = uri,
            )
        }
    }
}

其中 columns 表示每一行有多少列;uriList 表示需要显示 GIF 动图 Uri 列表。

最后,将 Fragmnet 中原本初始化 RecyclerView 的方法改为:

private fun initRecyclerView() {
    val showGifResolutions = arrayListOf()

    // 获取用户选择的图片列表,初始化 showGifResolutions

    // ...

    var lineLength = GifTools.JointGifSquareLineLength[gifUris!!.size]

    bind.jointGifPreviewRecyclerView.setContent {
        JointGifSquare(
            lineLength,
            gifUris!!
        )
    }
}

其中,GifTools.JointGifSquareLineLength 是我定义的一个 HashMap 用来存放所有图片数量与每一行数量的对应关系:

val JointGifSquareLineLength = hashMapOf(4 to 2, 9 to 3, 16 to 4, 25 to 5, 36 to 6, 49 to 7, 64 to 8, 81 to 9, 100 to 10)

从上面可以看出,其实要从 compose 中拿到 View 的数据也很简单,直接传值进去即可。

最终运行效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IsewV9LJ-1684632656916)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9c9d43bbdbd34bb08e0876781baaba02~tplv-k3u1fbpfcp-watermark.image?)]

原本使用 view 的运行效果:

g2.gif

可以看到,使用 compose 重构后的排列方式才是符合我们预期的排列方式。

总结

自此,我们就完成了将 View 中的其中一个界面替换为使用 compose 实现,也就是混合使用 view 和 compose 。

其实这个功能还有两个特性没有移植,那就是支持点击预览中的任意图片后可以更换图片和长按图片可以拖拽排序。

这两个功能的界面实现非常简单,难点在于,我怎么把更换图片和重新排序图片后的状态传回给 View。

这个问题我们就留着以后再说吧。

参考资料

  1. 深入Jetpack Compose——布局原理与自定义布局(一)
  2. Adding Jetpack Compose to your app

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

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

相关文章

jenkins入门与安装

一、实验环境 selinux iptables off 主机名IP系统版本gitlab10.10.10.200rhel7.5jenkins10.10.10.10rhel7.5tomcat10.10.10.11rhel7.5 二、安装jenkins 1、解压安装包 下载地址&#xff1a;https://download.docker.com/linux/static/stable/x86_64/ [rootjenkins ~]# tar xf …

C语言指针初级

目录 一、什么是指针 二、指针和指针类型 三、野指针 1.野指针的成因&#xff1a; 2.如何规避野指针 四、指针运算 1.指针-整数 2. 指针之间的加减 五、二级指针 六、指针数组 一个男人&#xff0c;到底要走多少的路&#xff0c;才能成为一个真正的男人 本专栏适用于…

【Linux】ubuntu设置ssh密钥登录详细教程,附Mobaxterm和pycharm ssh python解释器配置教程

0、写在前面 我们通常使用SSH 客户端来远程使用 Linux 服务器。但是&#xff0c;一般的密码方式登录&#xff0c;容易有密码被暴力破解的问题。所以&#xff0c;一般我们会将 SSH 的端口设置为默认的 22 以外的端口&#xff0c;或者禁用 root 账户登录。但是即使是将端口设置为…

复杂数据集,召回、精度等突破方法记录【以电科院过检识别模型为参考】

目录 一、数据分析与数据集构建 二、所有相关的脚本 三、模型效果 一、数据分析与数据集构建 由于电科院数据集有17w-18w张&#xff0c;标签错误的非常多&#xff0c;且漏标非常多&#xff0c;但是所有有效时间只有半个月左右&#xff0c;显卡是M60&#xff0c;训练速度特别…

linux防火墙之iptables

一、iptables概述 Linux 系统的防火墙 &#xff1a;IP信息包过滤系统&#xff0c;它实际上由两个组件netfilter 和 iptables组成。 主要工作在网络层&#xff0c;针对IP数据包。体现在对包内的IP地址、端口、协议等信息的处理上。 netfilter&#xff1a; 属于“内核态”&…

Java基础重温巩固

方法 方法与方法之间是平级关系&#xff0c;不能嵌套return表示结束当前方法 基本数据类型和引用数据类型 基本数据类型&#xff1a;数据存储在自己的空间中 引用数据类型&#xff1a;数据存储在其他空间中&#xff0c;自己空间存储的是地址值 值传递 传递基本数据类型时&…

详解Windows系统安装TensorRT

目录 下载TensorRT安装TensorRT测试 TensorRT 是 NVIDIA 推出的一款高性能神经网络部署引擎.Windows系统下TensorRT目前不能简单直接通过pip指令自动下载安装, 安装之前还需要提前安装好 CUDA 和 CUDNN. CUDA和CUDNN安装可参考: 详解 Windows系统下安装 CUDA 与 CUDNN. &…

ES6之Module:export、import

文章目录 前言一、export命令1.export2.export default&#xff08;默认暴露&#xff09; 二、import命令1.通用导入方式2.解析赋值导入方式 三、结果总结 前言 ES6之前&#xff0c;JavaScript语言一直没有模块&#xff08;Module&#xff09;体系&#xff0c;无法将一个大型程…

《计算机网络——自顶向下方法》精炼——3.5.1-3.5.4

人生像攀登一座山,而找寻出路,却是一种学习的过程,我们应当在这过程中,学习稳定、冷静,学习如何从慌乱中找到生机。——席慕蓉 文章目录 TCPTCP协议概述报文段结构序号、确认号 超时问题计算RTT计算重传时间 可靠数据传输 TCP TCP协议概述 TCP是面向连接的协议&#xff0c;在…

nest笔记十一:一个完整的nestjs示例工程(nestjs_template)

概述 链接&#xff1a;nestjs_template 相关文章列表 nestjs系列笔记 示例工程说明 这个工程是我使用nestjs多个项目后&#xff0c;总结出来的模板。这是一个完整的工程&#xff0c;使用了yaml做为配置&#xff0c;使用了log4js和redis和typeorm&#xff0c;sawgger&#…

Less和sass安装及使用

CSS预处理器 由来 CSS本身不是一种编程语言。你可以用它开发网页样式&#xff0c;但是没法用它编程。换句话说&#xff0c;CSS基本上是设计师的工具&#xff0c;不是程序员的工具。它并不像其它程序语言&#xff0c;比如说JavaScript等&#xff0c;有自己的变量、常量、条件语…

手把手教你验证upd与tcp“端口”开发策略

系列文章目录 文章目录 系列文章目录前言一、问题&#xff1f;二、验证网络策略步骤三、增强验证网络策略总结 前言 这篇文章&#xff0c;本意是让大家了解除了常用的telnet之外&#xff0c;在运维过程中&#xff0c;如果在服务器中未发现相关命令还可以借用像ssh、wget 等命令…

震惊!我的电脑居然中病毒了,快来看看我是如何解决的

打开电脑就自动跳转到http://www.exinariuminix.info/ 问题背景 今天刚打开电脑&#xff0c;然后电脑莫名奇妙就自动跳转到http://www.exinariuminix.info/网页&#xff1a; 联网后加载&#xff0c;它就自动重定向到一个莫名其妙的网站&#xff1a; 问题原因 出于好奇&#…

大数据Doris(二十四):Doris数据Insert Into导入方式介绍

文章目录 Doris数据Insert Into导入方式介绍 一、语法及参数 二、案例 三、注意事项 1、关于插入数据量 2、关于insert操作返回结果 3、关于导入任务超时 4、关于Session变量 5、关于数据导入错误 Doris数据Insert Into导入方式介绍 Doris 提供多种数据导入方案&…

SpringMVC执行原理

目录结构 pom.xml依赖 <dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency><!-- https://mvnrepository.co…

【Java校招面试】实战面经(五)

目录 前言一、讲讲short数据类型&#xff0c;short s1 1; s1 s11;对不对&#xff1f;二、使用final关键字的好处三、如果String中重写equals不重写hashCode会出现什么问题&#xff1f;四、ConcurrentHashMap的get方法是不加锁的&#xff0c;如何保证线程安全&#xff1f;五、…

缓存存在的问题

文章目录 缓存问题缓存穿透引入解决方案 缓存雪崩缓存击穿 缓存问题 使用缓存时常见的问题主要分为三个&#xff1a;缓存穿透 、缓存雪崩、缓存击穿。 下面对其进行一一学习 缓存穿透 引入 定义&#xff1a;缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#…

20230521给coolpi4b开发板刷Android12和串口的使用

20230521给coolpi4b开发板刷Android12和串口的使用 2023/5/21 9:31 所需要的配件&#xff1a; 1、2.0mm转2.54mm的杜邦线母头2条&#xff08;2位一起的一条&#xff09; 2、串口板&#xff1a;CH340&#xff08;WIN7下使用&#xff09; 3、USB的公头-公头线 首先&#xff0c;欢…

微星MSI GE66 10SF-416RU电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件配置 硬件型号驱动情况 主板Intel HM470 处理器Intel Core i7-10875H 2.30GHz up to 5.10GHz已驱动 内存Kingston Fury Impact DDR4 2x16Gb 3200mhz已驱动 硬盘NT…

Redis介绍及Linux单机安装

1.Redis介绍 1.1.什么是Redis Redis&#xff08;Remote Dictionary Server )&#xff0c;即远程字典服务&#xff0c;是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库&#xff0c;并提供多种语言的API。 Redis是完全开源的&#…