继续创作,加速生长!这是我参加「日新方案 10 月更文应战」的第9天,点击检查活动概况

前言

在我的文章 记一次 kotlin 在 MutableList 中运用 remove 引发的问题 中,我说到有一个功用是将多张动图以N宫格的形式拼接,而且每个动图的宽保证共同,可是高不保证共同。

在本来项目中我运用的是传统 view 合作 RecyclerView 和 GridLayout 布局办法进行拼图的预览,可是这会存在一个问题。

实际上是这样摆放的:

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

可是料想中应该是这样摆放:

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

能够看到,咱们的需求应该是彻底依照次序来摆放,可是瀑布流布局却是在每一行中,哪一列的高度最小就优先排到哪一列,而不是严厉依照给定次序摆放。

显然,这是不契合咱们的需求的。

我曾经企图找到其他的替代办法完结这个作用,或者企图找到 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()
}

作用如下:

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

完美契合咱们的需求。

添加修正 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 了。

更改完结后看一下运行作用:

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

能够看到,混合运用彻底没有问题。

可是这儿咱们运用的是写死的 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 的数据也很简单,直接传值进去即可。

终究运行作用:

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

本来运用 view 的运行作用:

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

能够看到,运用 compose 重构后的摆放办法才是契合咱们预期的摆放办法。

总结

自此,咱们就完结了将 View 中的其间一个界面替换为运用 compose 完结,也便是混合运用 view 和 compose 。

其实这个功用还有两个特性没有移植,那便是支撑点击预览中的任意图片后能够替换图片和长按图片能够拖拽排序。

这两个功用的界面完结十分简单,难点在于,我怎么把替换图片和从头排序图片后的状态传回给 View。

这个问题咱们就藏着今后再说吧。

参考资料

  1. 深化Jetpack Compose——布局原理与自界说布局(一)
  2. Adding Jetpack Compose to your app