Compose 中的视图互操作性

1. 开始之前

简介

在本课程的这个阶段,您已经熟练掌握了使用 Compose 构建应用程序,并且对使用 XML、视图、视图绑定和片段构建应用程序有一定的了解。在使用视图构建应用程序后,您可能会体会到使用 Compose 这样的声明式 UI 构建应用程序的便利性。但是,在某些情况下,使用视图而不是 Compose 更有意义。在本 Codelab 中,您将学习如何使用视图互操作性将视图组件添加到现代 Compose 应用程序中。

在撰写本 Codelab 时,您即将创建的 UI 组件在 Compose 中尚不可用。这是利用视图互操作性的绝佳机会!

先决条件

您需要什么

  • 一台连接互联网的电脑和 Android Studio
  • 一台设备或模拟器
  • Juice Tracker 应用程序的起始代码

您将构建什么

在本 Codelab 中,您需要将三个视图集成到 Compose UI 中以完成 Juice Tracker 应用程序的 UI;一个 Spinner、一个 RatingBar 和一个 AdView。要构建这些组件,您将使用视图互操作性(简称视图互操作)。使用视图互操作,您可以通过将视图包装在 Composable 中来实际将视图添加到您的应用程序中。

a02177f6b6277edc.png afc4551fde8c3113.png 5dab7f58a3649c04.png

代码演练

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

bottomsheet 目录包含与输入对话框相关的全部 UI 组件。创建这些组件后,此包应包含颜色和评分输入的 UI 组件。

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

主要 UI 组件(例如底部工作表和果汁列表)托管在 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 services ads 依赖项添加到应用程序 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. 完成输入对话框

在本节中,您将通过创建颜色微调器和评分条来完成输入对话框。颜色微调器是允许您选择颜色的组件,而评分条允许您为果汁选择评分。请参阅下面的设计

Color spinner with the multiple colors listed

Rating bar with 4 out of 5 stars selected

创建颜色微调器

要在 Compose 中实现微调器,必须使用 Spinner 类。Spinner 是一个视图组件,而不是一个 Composable,因此必须使用互操作性来实现它。

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

bottomsheet/ColorSpinnerRow.kt

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

实现此接口可以让您定义微调器的点击行为。稍后您将在 Composable 中设置此适配器。

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() 回调函数,以便当您选择颜色时,应用程序会更新 UI 中所选的值。

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 的新 Composable。
  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() Composable 并传递输入标签的颜色字符串资源和一个修饰符,该修饰符定义 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 是一个视图类,因此必须利用 Compose 的视图互操作性 API 将其包装到 Composable 中。这是通过 AndroidView Composable 实现的。

  1. 要在 Compose 中使用 Spinner,请在 InputRow lambda 主体中创建一个 AndroidView() Composable。AndroidView() Composable 在 Composable 中创建视图元素或层次结构。

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 **Composable** 接受三个参数

  • **factory** **lambda**,这是一个创建视图的函数。
  • **update** **回调**,在 factory 中创建的视图膨胀时调用。
  • 一个 **Composable** modifier

3bb9f605719b173.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 提供数据一样,ArrayAdapterSpinner 提供数据。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 回调返回在其内部创建的视图的实例。update 是一个回调,它接受与 factory 回调返回的类型相同的参数。此参数是 factory 膨胀的视图的实例。在本例中,由于在 factory 中创建了一个 Spinner,因此可以在 update lambda 主体中访问该 Spinner 的实例。

  1. 添加一个 update 回调,该回调传递一个 spinner。使用 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 Composable 中实现 ColorSpinnerRow。将颜色微调器放在“描述”文本之后,按钮上方。

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() 的新 Composable。
  3. 在方法签名中,为评分传递一个 Int,一个带 Int 参数的回调函数来处理选择更改,以及一个修饰符。

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
}
  1. ColorSpinnerRow 一样,向包含 AndroidView 的 Composable 添加一个 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 = {}
        )
    }
}

膨胀视图时,将设置评分。请记住,factoryRatingBar 的实例返回到更新回调。

  1. 使用传递给 Composable 的评分来设置RatingBar实例在update lambda 体中的评分。
  2. 当设置新的评分时,使用RatingBar回调来调用onRatingChange()回调函数以更新 UI 中的评分。

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. JuiceTrackerApp中,将AdBanner放在JuiceTrackerList之前。JuiceTrackerList声明在第 83 行。

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 应用程序的现代 UI 工具包。在本课程中,您构建了带有列表、单个或多个屏幕的应用程序,并在它们之间进行了导航。您学习了如何创建交互式应用程序,使您的应用程序响应用户输入并更新 UI。您应用了 Material Design 并使用颜色、形状和排版来为您的应用程序设置主题。您还使用了 Jetpack 和其他第三方库来安排任务、从远程服务器检索数据、本地持久化数据等等。

完成本课程后,您不仅对如何使用 Jetpack Compose 创建漂亮且响应迅速的应用程序有了很好的理解,而且还掌握了创建高效、可维护和视觉上吸引人的 Android 应用程序所需的知识和技能。这个基础将帮助您继续学习和提高您在现代 Android 开发和 Compose 中的技能。

我们要感谢大家参与并完成本课程!我们鼓励大家继续学习,并通过其他资源来扩展您的技能,例如Android 开发者文档面向 Android 开发人员的 Jetpack Compose 课程现代 Android 应用架构Android 开发者博客、其他codelab示例项目

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

快乐创作!!