在 Compose 中使用 View Interoperability

1. 准备工作

简介

在本课程的这一阶段,您已经非常熟悉如何使用 Compose 构建应用,并已初步了解如何使用 XML、View、View 绑定和 Fragment 构建应用。使用 View 构建应用后,您可能会领会到使用声明式界面(例如 Compose)构建应用的便捷性。不过,在某些情况下,最好使用 View,而不使用 Compose。在此 Codelab 中,您将学习如何使用 View Interops 将 View 组件添加到现代 Compose 应用中。

在编写此 Codelab 时,Compose 中尚未提供您要创建的界面组件。这是学会如何使用 View Interop 的绝佳机会!

前提条件:

所需条件

  • 一台连接到互联网并安装了 Android Studio 的计算机
  • 一台设备或模拟器
  • Juice Tracker 应用的起始代码

构建内容

在此 Codelab 中,您需要将 Spinner、RatingBar 和 AdView 这三个 View 集成到 Compose 界面中,以便完成 Juice Tracker 应用界面。如需构建这些组件,您需要使用 View Interoperability(简称“View Interop”)。借助 View Interop,您实际上可以将 View 封装到可组合项中,从而将其添加到应用中。

包含测试广告的应用屏幕、用于输入果汁详细信息的底部动作条,以及向列表中添加的橙汁。

代码演示

在此 Codelab 中,您将使用与使用 View 构建 Android 应用 Codelab 和将 Compose 添加到基于 View 的应用 Codelab 中相同的 JuiceTracker 应用。与此版本的不同之处在于,提供的起始代码完全采用 Compose。目前,此应用缺少条目对话框工作表中的颜色和评分输入,以及列表界面顶部的广告横幅。

bottomsheet 目录包含与条目对话框相关的所有界面组件。在创建颜色和评分输入后,此软件包应包含颜色和评分输入的界面组件。

homescreen 包含主屏幕托管的界面组件,其中包括 JuiceTracker 列表。在创建广告横幅后,此软件包最终应包含广告横幅。

主要界面组件(例如底部动作条和果汁列表)托管在 JuiceTrackerApp.kt 文件中。

2. 获取起始代码

首先,请下载起始代码:

或者,您也可以克隆 GitHub 代码库:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout compose-starter
  1. 在 Android Studio 中,打开 basic-android-kotlin-compose-training-juice-tracker 文件夹。
  2. 在 Android Studio 中,打开 Juice Tracker 应用代码。

3. Gradle 配置

将 Play 服务广告依赖项添加到应用 build.gradle.kts 文件中。

app/build.gradle.kts

android {
   ...
   dependencies {
      ...
      implementation("com.google.android.gms:play-services-ads:22.2.0")
   }
}

4. 设置

将以下值添加到 Android 清单中的 activity 标记上方,以启用广告横幅进行测试:

AndroidManifest.xml

...
<meta-data
   android:name="com.google.android.gms.ads.APPLICATION_ID"
   android:value="ca-app-pub-3940256099942544~3347511713" />

...

5. 完成条目对话框

在本部分中,您将创建颜色旋转图标和评分栏,从而完成条目对话框。颜色旋转图标是可让您选择颜色的组件,而评分栏可让您为果汁选择评分。请参阅以下设计:

列有多种颜色的颜色旋转图标

评分栏(当前评分 4 颗星,满分为 5 颗星)

创建颜色旋转图标

如需在 Compose 中实现旋转图标,必须使用 Spinner 类。Spinner 是 View 组件,而不是可组合项,因此必须通过互操作实现。

  1. bottomsheet 目录中,新建一个名为 ColorSpinnerRow.kt 的文件。
  2. 在文件中新建一个名为 SpinnerAdapter 的类。
  3. SpinnerAdapter 的构造函数中,定义一个名为 onColorChange 的回调函数,该函数接受 Int 参数。SpinnerAdapter 会处理 Spinner 的回调函数。

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit){
}
  1. 实现 AdapterView.OnItemSelectedListener 接口。

通过实现此接口,您可以定义旋转图标的点击行为。稍后,您将在可组合项中设置此适配器。

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
}
  1. 实现 AdapterView.OnItemSelectedListener 成员函数:onItemSelected()onNothingSelected()

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        TODO("Not yet implemented")
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        TODO("Not yet implemented")
    }
}
  1. 修改 onItemSelected() 函数以调用 onColorChange() 回调函数,以便在您选择某个颜色后,应用会更新界面中的选定值。

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        onColorChange(position)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        TODO("Not yet implemented")
    }
}
  1. 修改 onNothingSelected() 函数以将颜色设置为 0,以便在您未选择任何颜色时,使用第一个颜色(红色)作为默认颜色。

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        onColorChange(position)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        onColorChange(0)
    }
}

之前已经构建了用于通过回调函数定义旋转图标行为的 SpinnerAdapter。现在,您需要构建旋转图标的内容并为其填充数据。

  1. ColorSpinnerRow.kt 文件内(但 SpinnerAdapter 类之外),新建一个名为 ColorSpinnerRow 的可组合项。
  2. ColorSpinnerRow() 的方法签名中,添加旋转图标位置的 Int 参数、接受 Int 参数的回调函数以及修饰符。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
}
  1. 在此函数内,使用 JuiceColor 枚举创建果汁颜色字符串资源的数组。此数组将用作填充旋转图标的内容。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }

}
  1. 添加 InputRow() 可组合项并传递输入标签的颜色字符串资源和修饰符,以定义用于显示 Spinner 的输入行。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
   InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
   }
}

接下来,您将创建 Spinner!由于 Spinner 属于 View 类,因此必须使用 Compose 的 View Interoperability API 将其封装到可组合项中。这可通过 AndroidView 可组合项实现。

  1. 如需在 Compose 中使用 Spinner,请在 InputRow lambda 正文中创建一个 AndroidView() 可组合项。AndroidView() 可组合项会在可组合项中创建 View 元素或层次结构。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
   InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
      AndroidView()
   }
}

AndroidView 可组合项接受三个参数:

  • factory lambda,这是一个用于创建 View 的函数。
  • update 回调,该回调将在 factory 中创建的 View 膨胀时调用。
  • 可组合项 modifier

575cce0368632b79.png

  1. 如需实现 AndroidView,请先传递修饰符并填充屏幕的最大宽度。
  2. factory 参数传递 lambda。
  3. factory lambda 接受 Context 作为参数。创建一个 Spinner 类并传递上下文。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         modifier = Modifier.fillMaxWidth(),
         factory = { context ->
            Spinner(context)
         }
      )
   }
}

就像 RecyclerView.AdapterRecyclerView 提供数据一样,ArrayAdapter 也为 Spinner 提供数据。Spinner 需要用适配器来存储颜色数组。

  1. 使用 ArrayAdapter 设置适配器。ArrayAdapter 需要上下文、XML 布局和数组。为布局传递 simple_spinner_dropdown_item;此布局作为 Android 的默认布局。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         ​​modifier = Modifier.fillMaxWidth(),
         factory = { context ->
             Spinner(context).apply {
                 adapter =
                     ArrayAdapter(
                         context,
                         android.R.layout.simple_spinner_dropdown_item,
                         juiceColorArray
                     )
             }
         }
      )
   }
}

factory 回调会返回在其中创建的 View 的实例。update 是一种回调,它接受 factory 回调所返回的相同类型的参数。此参数是由 factory 膨胀的 View 的实例。在本例中,由于在工厂中创建了 Spinner,因此可以在 update lambda 正文中访问此 Spinner 的实例。

  1. 添加传递 spinnerupdate 回调。使用 update 中提供的回调来调用 setSelection() 方法。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      //...
         },
         update = { spinner ->
             spinner.setSelection(colorSpinnerPosition)
             spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
         }
      )
   }
}
  1. 使用您之前创建的 SpinnerAdapterupdate 中设置 onItemSelectedListener() 回调。

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         // ...
         },
         update = { spinner ->
             spinner.setSelection(colorSpinnerPosition)
             spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
         }
      )
   }
}

现在,颜色旋转图标组件的代码已完成。

  1. 添加以下实用函数以获取 JuiceColor 的枚举索引。您将在下一步中使用此索引。
private fun findColorIndex(color: String): Int {
   val juiceColor = JuiceColor.valueOf(color)
   return JuiceColor.values().indexOf(juiceColor)
}
  1. EntryBottomSheet.kt 文件的 SheetForm 可组合项中实现 ColorSpinnerRow。将颜色旋转图标放在“Description”文本之后、按钮上方的位置。

bottomsheet/EntryBottomSheet.kt

...
@Composable
fun SheetForm(
   juice: Juice,
   onUpdateJuice: (Juice) -> Unit,
   onCancel: () -> Unit,
   onSubmit: () -> Unit,
   modifier: Modifier = Modifier,
) {
   ...
   TextInputRow(
            inputLabel = stringResource(R.string.juice_description),
            fieldValue = juice.description,
            onValueChange = { description -> onUpdateJuice(juice.copy(description = description)) },
            modifier = Modifier.fillMaxWidth()
        )
        ColorSpinnerRow(
            colorSpinnerPosition = findColorIndex(juice.color),
            onColorChange = { color ->
                onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
            }
        )
   ButtonRow(
            modifier = Modifier
                .align(Alignment.End)
                .padding(bottom = dimensionResource(R.dimen.padding_medium)),
            onCancel = onCancel,
            onSubmit = onSubmit,
            submitButtonEnabled = juice.name.isNotEmpty()
        )
    }
}

创建评分输入

  1. bottomsheet 目录中,新建一个名为 RatingInputRow.kt 的文件。
  2. RatingInputRow.kt 文件中,新建一个名为 RatingInputRow() 的可组合项。
  3. 在方法签名中,传递评分的 Int、用于处理选择更改的包含 Int 参数的回调以及修饰符。

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
}
  1. ColorSpinnerRow 一样,向包含 AndroidView 的可组合项添加 InputRow,如以下示例代码所示。

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = {},
            update = {}
        )
    }
}
  1. factory lambda 正文中,创建 RatingBar 类的实例,用于提供此设计所需的评分栏的类型。将 stepSize 设置为 1f,将评分强制设置为整数。

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = { context ->
                RatingBar(context).apply {
                    stepSize = 1f
                }
            },
            update = {}
        )
    }
}

当 View 膨胀时,系统会设置评分。回想一下,factory 会将 RatingBar 的实例返回到更新回调。

  1. 使用传递给可组合项的评分,在 update lambda 正文中为 RatingBar 实例设置评分。
  2. 设置新的评分后,使用 RatingBar 回调调用 onRatingChange() 回调函数,以在界面中更新评分。

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = { context ->
                RatingBar(context).apply {
                    stepSize = 1f
                }
            },
            update = { ratingBar ->
                ratingBar.rating = rating.toFloat()
                ratingBar.setOnRatingBarChangeListener { _, _, _ ->
                    onRatingChange(ratingBar.rating.toInt())
                }
            }
        )
    }
}

现在,评分输入可组合项已完成。

  1. EntryBottomSheet 中使用 RatingInputRow() 可组合项。将其放置在颜色旋转图标之后、按钮上方的位置。

bottomsheet/EntryBottomSheet.kt

@Composable
fun SheetForm(
    juice: Juice,
    onUpdateJuice: (Juice) -> Unit,
    onCancel: () -> Unit,
    onSubmit: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        ...
        ColorSpinnerRow(
            colorSpinnerPosition = findColorIndex(juice.color),
            onColorChange = { color ->
                onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
            }
        )
        RatingInputRow(
            rating = juice.rating,
            onRatingChange = { rating -> onUpdateJuice(juice.copy(rating = rating)) }
        )
        ButtonRow(
            modifier = Modifier.align(Alignment.CenterHorizontally),
            onCancel = onCancel,
            onSubmit = onSubmit,
            submitButtonEnabled = juice.name.isNotEmpty()
        )
    }
}

创建广告横幅

  1. homescreen 软件包中,新建一个名为 AdBanner.kt 的文件。
  2. AdBanner.kt 文件中,新建一个名为 AdBanner() 的可组合项。

与您之前构建的可组合项不同,AdBanner 不需要输入。因此,您无需将其封装在 InputRow 可组合项中。不过,它需要 AndroidView

  1. 尝试使用 AdView 类自行构建横幅广告。请务必将广告尺寸设置为 AdSize.BANNER 并将广告单元 ID 设置为 "ca-app-pub-3940256099942544/6300978111"
  2. AdView 膨胀后,使用 AdRequest Builder 加载广告。

homescreen/AdBanner.kt

@Composable
fun AdBanner(modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            AdView(context).apply {
                setAdSize(AdSize.BANNER)
                // Use test ad unit ID
                adUnitId = "ca-app-pub-3940256099942544/6300978111"
            }
        },
        update = { adView ->
            adView.loadAd(AdRequest.Builder().build())
        }
    )
}
  1. AdBanner 放在 JuiceTrackerApp 中的 JuiceTrackerList 之前。第 83 行声明了 JuiceTrackerList

ui/JuiceTrackerApp.kt

...
AdBanner(
   Modifier
       .fillMaxWidth()
       .padding(
           top = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_small)
       )
)

JuiceTrackerList(
    juices = trackerState,
    onDelete = { juice -> juiceTrackerViewModel.deleteJuice(juice) },
    onUpdate = { juice ->
        juiceTrackerViewModel.updateCurrentJuice(juice)
        scope.launch {
            bottomSheetScaffoldState.bottomSheetState.expand()
        }
     },
)

6. 获取解决方案代码

如需下载完成后的 Codelab 代码,您可以使用以下 Git 命令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout compose-with-views

或者,您也可以下载 ZIP 文件形式的代码库,将其解压缩并在 Android Studio 中打开。

如果您想查看解决方案代码,请前往 GitHub 查看

7. 了解详情

8. 大功告成!

本课程到此结束,但这您的 Android 应用开发之旅才刚刚开始!

在本课程中,您学习了如何使用 Jetpack Compose 构建新应用。Jetpack Compose 是用于构建原生 Android 应用的现代界面工具包。在本课程中,您构建了包含列表、单个或多个界面的应用,并在这些元素之间导航。您学习了如何构建交互式应用、如何让应用响应用户输入以及如何更新界面。您应用了 Material Design,并使用了颜色、形状和排版来为您的应用设置主题。此外,您还使用了 Jetpack 和其他第三方库来安排任务、从远程服务器检索数据、在本地保留数据等。

通过学习本课程,您不仅充分了解了如何使用 Jetpack Compose 构建精美的自适应应用,还掌握了打造高效、可维护且富有视觉吸引力的 Android 应用所需的知识和技能。这些基础知识将帮助您继续学习和培养 Modern Android Development 和 Compose 的相关技能。

感谢大家参与并完成本课程!我们鼓励大家通过更多资源进一步学习并拓展相关技能,例如:Android 开发者文档“面向 Android 开发者的 Jetpack Compose”课程现代 Android 应用架构Android 开发者博客、其他 Codelab示例项目

最后,别忘了在社交媒体上分享您构建的内容,并使用 #AndroidBasics 标签,以便我们和 Android 开发者社区的其他成员也能及时关注您的学习历程!

祝编程顺利!