Compose 的集成与导航

首先我们来看如何在 View 体系中集成 Compose。

1、迁移策略

Codelab 给出了从 View 迁移到 Compose 的策略,以下内容基本上来自该 Codelab。

Jetpack Compose 从设计之初就考虑到了 View 互操作性。如需迁移到 Compose,我们建议您执行增量迁移(Compose 和 View 在代码库中共存),直到应用完全迁移至 Compose 为止。

推荐的迁移策略如下:

  1. 使用 Compose 构建新界面
  2. 在构建功能时,确定可重复使用的元素,并开始创建常见界面组件库
  3. 一次替换一个界面的现有功能

1.1 使用 Compose 构建新界面

使用 Compose 构建覆盖整个界面的新功能是提高 Compose 采用率的最佳方式。借助此策略,您可以添加功能并利用 Compose 的优势,同时仍满足公司的业务需求

一项新功能可能涵盖整个界面,在这种情况下,整个界面都在 Compose 中。如果您使用的是基于 fragment 的导航,这意味着您需要创建一个新的 fragment,并在 Compose 中添加其内容。

您还可以在现有界面中引入新功能。在这种情况下,View 和 Compose 将共存在同一个界面上。例如,假设您要添加的功能是 RecyclerView 中的一种新的视图类型。在这种情况下,新的视图类型将位于 Compose 中,而其他项目保持不变。

1.2 构建常见界面组件库

使用 Compose 构建功能时,您很快就会意识到,您最终会构建组件库。您需要确定可重复使用的组件,促使在应用中重复使用这些组件,以便共享组件具有单一可信来源。您构建的功能随后可以依赖于这个库。

1.3 使用 Compose 替换现有功能

除了构建新功能之外,您还需要逐步将应用中的现有功能迁移到 Compose。具体采用哪种方法由您决定,下面是一些适合的方法:

  1. 简单界面 - 包含少数界面元素和动态元素(例如欢迎界面、确认界面或设置界面)的简单界面。这些界面非常适合迁移到 Compose,因为只需几行代码就能搞定。
  2. 混合 View 和 Compose 界面 - 已包含少量 Compose 代码的界面是另一个不错的选择,因为您可以继续逐步迁移该界面中的元素。如果您的某个界面在 Compose 中只有一个子树,您可以继续迁移该树的其他部分,直到整个界面位于 Compose 中。这称为自下而上的迁移方法。

2024-9-30.View迁移到Compose

1.4 此 Codelab 采用的方法

在此 Codelab 中,您将逐步把 Sunflower 的植物详情界面迁移到 Compose,将 Compose 和 View 结合起来使用。之后,您将掌握足够的知识,可以在需要时继续进行迁移。

2、迁移内容

将 Codelab 的起始代码移植到项目中,参考 GitHub commit a49940c5,在此基础上进行修改。

2.1 View 中集成 Compose

以植物详情页面为例,我们想将详情信息由 View 改为 Compose 实现:

2024-9-30.植物详情页面定稿

那么需要将布局文件 fragment_plant_detail 中展示详细信息的 ConstraintLayout 替换为 ComposeView:

<layout 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">

    <data>

        <import type="com.compose.migration.data.Plant" />

        <variable
            name="viewModel"
            type="com.compose.migration.viewmodels.PlantDetailViewModel" />

        <variable
            name="callback"
            type="com.compose.migration.plantdetail.PlantDetailFragment.Callback" />
    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="?attr/colorSurface"
        android:fitsSystemWindows="true"
        tools:context="com.google.samples.apps.sunflower.GardenActivity"
        tools:ignore="MergeRootFrame">

        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appbar"
            android:layout_width="match_parent"
            android:layout_height="@dimen/plant_detail_app_bar_height"
            android:animateLayoutChanges="true"
            android:background="?attr/colorSurface"
            android:fitsSystemWindows="true"
            android:stateListAnimator="@animator/show_toolbar"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <com.google.android.material.appbar.CollapsingToolbarLayout
                android:id="@+id/toolbar_layout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                app:collapsedTitleGravity="center"
                app:collapsedTitleTextAppearance="@style/TextAppearance.Sunflower.Toolbar.Text"
                app:contentScrim="?attr/colorSurface"
                app:layout_scrollFlags="scroll|exitUntilCollapsed"
                app:statusBarScrim="?attr/colorSurface"
                app:title="@{viewModel.plant.name}"
                app:titleEnabled="false"
                app:toolbarId="@id/toolbar">

                <ImageView
                    android:id="@+id/detail_image"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/plant_detail_app_bar_height"
                    android:contentDescription="@string/plant_detail_image_content_description"
                    android:fitsSystemWindows="true"
                    android:scaleType="centerCrop"
                    app:imageFromUrl="@{viewModel.plant.imageUrl}"
                    app:layout_collapseMode="parallax" />

                <androidx.appcompat.widget.Toolbar
                    android:id="@+id/toolbar"
                    android:layout_width="match_parent"
                    android:layout_height="?attr/actionBarSize"
                    android:background="@android:color/transparent"
                    app:contentInsetStartWithNavigation="0dp"
                    app:layout_collapseMode="pin"
                    app:menu="@menu/menu_plant_detail"
                    app:navigationIcon="@drawable/ic_detail_back"
                    app:titleTextColor="?attr/colorOnSurface" />

            </com.google.android.material.appbar.CollapsingToolbarLayout>

        </com.google.android.material.appbar.AppBarLayout>

        <androidx.core.widget.NestedScrollView
            android:id="@+id/plant_detail_scrollview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:paddingBottom="@dimen/fab_bottom_padding"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <!-- 将原本的 ConstraintLayout 替换为 ComposeView-->
            <androidx.compose.ui.platform.ComposeView
                android:id="@+id/compose_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

        </androidx.core.widget.NestedScrollView>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            style="@style/Widget.MaterialComponents.FloatingActionButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin"
            android:onClick="@{() -> callback.add(viewModel.plant)}"
            android:tint="@android:color/white"
            app:isGone="@{viewModel.isPlanted}"
            app:layout_anchor="@id/appbar"
            app:layout_anchorGravity="bottom|end"
            app:shapeAppearance="@style/ShapeAppearance.Sunflower.FAB"
            app:srcCompat="@drawable/ic_plus" />

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

在该布局对应的逻辑代码 PlantDetailFragment 中,由于使用了 DataBinding 可以直接通过 id 拿到这个 ComposeView,用于展示其内容:

	override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            viewModel = plantDetailViewModel
            lifecycleOwner = viewLifecycleOwner
            callback = object : Callback {
                override fun add(plant: Plant?) {
                    plant?.let {
                        hideAppBarFab(fab)
                        plantDetailViewModel.addPlantToGarden()
                        Snackbar.make(root, R.string.added_plant_to_garden, Snackbar.LENGTH_LONG)
                            .show()
                    }
                }
            }

            var isToolbarShown = false

            // scroll change listener begins at Y = 0 when image is fully collapsed
            plantDetailScrollview.setOnScrollChangeListener(
                NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ ->

                    // User scrolled past image to height of toolbar and the title text is
                    // underneath the toolbar, so the toolbar should be shown.
                    val shouldShowToolbar = scrollY > toolbar.height

                    // The new state of the toolbar differs from the previous state; update
                    // appbar and toolbar attributes.
                    if (isToolbarShown != shouldShowToolbar) {
                        isToolbarShown = shouldShowToolbar

                        // Use shadow animator to add elevation if toolbar is shown
                        appbar.isActivated = shouldShowToolbar

                        // Show the plant name if toolbar is shown
                        toolbarLayout.isTitleEnabled = shouldShowToolbar
                    }
                }
            )

            toolbar.setNavigationOnClickListener { view ->
                view.findNavController().navigateUp()
            }

            toolbar.setOnMenuItemClickListener { item ->
                when (item.itemId) {
                    R.id.action_share -> {
                        createShareIntent()
                        true
                    }

                    else -> false
                }
            }

            // 展示 ComposeView 的内容
            composeView.setContent {
                PlantDetailDescription()
            }
        }
        setHasOptionsMenu(true)

        return binding.root
    }

PlantDetailDescription() 是 Codelab 已经准备好的可组合项,目前是初始内容:

@Composable
fun PlantDetailDescription() {
    Surface {
        Text("Hello Compose")
    }
}

下面来扩展 ComposeView 要展示的内容。

首先,详情信息需要知道你是哪一个植物才能展示其信息,该信息实际山可以通过 PlantDetailFragment 中定义的 PlantDetailViewModel 获取:

class PlantDetailFragment : Fragment() {

    private val args: PlantDetailFragmentArgs by navArgs()

    private val plantDetailViewModel: PlantDetailViewModel by viewModels {
        InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
    }
}

因此 PlantDetailDescription() 需要使用 PlantDetailViewModel 作为参数:

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // 使用 = plant 的类型是 State<Plant?>
//    val plant = plantDetailViewModel.plant.observeAsState()
    
    // 使用 by plant 的类型是 Plant
    val plant by plantDetailViewModel.plant.observeAsState()

    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(id = R.dimen.margin_small))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

// 标题信息
@Composable
fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(id = R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

// 浇水信息
@Composable
fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(id = R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(id = R.dimen.margin_normal)

        Text(
            text = stringResource(id = R.string.watering_needs_prefix),
            color = MaterialTheme.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val waterIntervalText = LocalContext.current.resources.getQuantityString(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )

        Text(
            text = waterIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

效果如图:

2024-9-30.View集成Compose效果缩小

2.2 Compose 中使用 View

Compose 暂不支持 Spanned 类,也不显示 HTML 格式文本。因此,需要在 Compose 代码中使用 View 系统的 TextView 来绕过此限制。

由于植物的描述信息中有介绍该植物的超链接,因此需要借助原生的 TextView 来实现点击超链接跳转的功能:

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(id = R.dimen.margin_small))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
            // 增加详情
            PlantDescription(plant.description)
        }
    }
}

@Composable
fun PlantDescription(description: String) {
    // 使用 remember 进行优化,description 作为 key,在没有发生变化时不会进行重组,也就避免
    // 了括号内的重复计算,从而降低性能开销
    val htmlDescription = remember(description) {
        // 使用兼容模式将 HTML 转换为 Spanned,因为 SDK 小于 24 不支持一些 FLAG 标记
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    AndroidView(
        // factory 是创建 View 用于转换为 Compose 的代码块,返回 TextView
        factory = { context ->
            TextView(context).apply {
                // 设置 TextView 上的超链接,使其点击后可以打开链接
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        // update 是在 layout 被填充后的回调函数
        update = {
            it.text = htmlDescription
        }
    )
}

效果如下:

2024-9-30.Compose使用View

2.3 共用主题

在早期的迁移阶段,可能希望 Compose 继承 View 系统中可用的主题,而不是从头开始在 Compose 中重写自己的 Material 主题。Material 主题与 Compose 附带的所有 Material Design 组件完美配合。

只需要在使用 Compose 的根可组合项的位置使用 MdcTheme 即可使用 View 系统中定义的主题:

	override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            viewModel = plantDetailViewModel
            lifecycleOwner = viewLifecycleOwner
            callback = object : Callback {
                override fun add(plant: Plant?) {
                    plant?.let {
                        hideAppBarFab(fab)
                        plantDetailViewModel.addPlantToGarden()
                        Snackbar.make(root, R.string.added_plant_to_garden, Snackbar.LENGTH_LONG)
                            .show()
                    }
                }
            }

            ...

            composeView.setContent {
                // com.google.android.material:compose-theme-adapter
                MdcTheme {
                    PlantDetailDescription(plantDetailViewModel)
                }
            }
        }
        setHasOptionsMenu(true)

        return binding.root
    }

MdcTheme 是 com.google.android.material:compose-theme-adapter 库中的函数,可以很方便的应用 View 体系下的主题。

3、Compose 导航

本节介绍如何在 Compose 中使用 Navigation 进行导航的相关知识,使用的是 Jetpack Compose Navigation 这个 Codelab,运行的效果如下:

2024-10-1.Compose Navigation Codelab效果

我们先来简单看下代码结构:

class RallyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RallyApp()
        }
    }
}

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    onTabSelected = { screen -> currentScreen = screen },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            Box(Modifier.padding(innerPadding)) {
                currentScreen.screen()
            }
        }
    }
}

RallyActivity 显示 RallyApp 的内容,Scaffold 的 topBar 是一个 RallyTabRow,由它来记录当前正在显示哪一个 Screen,某一个 Tab 被选中时如何处理。

详细点看,当前正在显示的 Screen 用 currentScreen 这个状态表示,类型是 RallyDestination 接口的实现类 Overview:

// RallyDestinations.kt:
interface RallyDestination {
    val icon: ImageVector
    val route: String
    val screen: @Composable () -> Unit
}

/**
 * Rally app navigation destinations
 */
object Overview : RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
    // Slots API,用于展示 Tab 对应的具体页面内容
    override val screen: @Composable () -> Unit = { OverviewScreen() }
}

RallyTabRow 的第一个参数 allScreens 传的是 rallyTabRowScreens,这是一个 List,包含了 3 个 RallyDestination 的实现类:

/**
 * Rally app navigation destinations
 */
object Overview : RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
    override val screen: @Composable () -> Unit = { OverviewScreen() }
}

object Accounts : RallyDestination {
    override val icon = Icons.Filled.AttachMoney
    override val route = "accounts"
    override val screen: @Composable () -> Unit = { AccountsScreen() }
}

object Bills : RallyDestination {
    override val icon = Icons.Filled.MoneyOff
    override val route = "bills"
    override val screen: @Composable () -> Unit = { BillsScreen() }
}

object SingleAccount : RallyDestination {
    // Added for simplicity, this icon will not in fact be used, as SingleAccount isn't
    // part of the RallyTabRow selection
    override val icon = Icons.Filled.Money
    override val route = "single_account"
    override val screen: @Composable () -> Unit = { SingleAccountScreen() }
    const val accountTypeArg = "account_type"
}

// Screens to be displayed in the top RallyTabRow
val rallyTabRowScreens = listOf(Overview, Accounts, Bills)

然后我们再看 RallyTabRow 的实现:

@Composable
fun RallyTabRow(
    allScreens: List<RallyDestination>,
    onTabSelected: (RallyDestination) -> Unit,
    currentScreen: RallyDestination
) {
    Surface(
        Modifier
            .height(TabHeight)
            .fillMaxWidth()
    ) {
        Row(Modifier.selectableGroup()) {
            allScreens.forEach { screen ->
                RallyTab(
                    text = screen.route,
                    icon = screen.icon,
                    // 被选中时调用方会在 onTabSelected 内更新当前正在展示的 screen
                    onSelected = { onTabSelected(screen) },
                    selected = currentScreen == screen
                )
            }
        }
    }
}

大致就是这样,详细代码去参考 GitHub 上的代码。

3.1 使用 Navigation 导航

使用 Navigation 进行导航,与 View 体系下的内容有很多类似之处,首先要构建一个 NavHost,里面构建导航图:

@Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier) {
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        // 构建导航图
        composable(
            route = Overview.route
        ) {
            OverviewScreen()
        }

        composable(
            route = Accounts.route
        ) {
            AccountsScreen()
        }

        composable(
            route = Bills.route
        ) {
            BillsScreen()
        }
    }
}

然后修改 RallyApp Scaffold 的 content 内容,不再直接展示布局内容,而是使用 RallyNavHost 通过 NavHostController 控制路由:

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        // 声明 NavHostController 用于控制导航
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    // 选中之后通过路由进行导航
                    onTabSelected = { screen -> navController.navigate(screen.route) },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding),
            )
            /*Box(Modifier.padding(innerPadding)) {
                currentScreen.screen()
            }*/
        }
    }
}

现在这样可以实现跳转:

2024-10-1.Compose Navigation Codelab效果1

但是仔细观察左上角,会发现不论点击哪个 Tab 都显示第一个,需要添加代码:

@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()
        val currentBackStackEntry = navController.currentBackStackEntryAsState()
        // 修改获取当前 RallyDestination 的逻辑
        var currentScreen = Overview.fromRoute(
            currentBackStackEntry.value?.destination?.route
        )

        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    onTabSelected = { screen -> navController.navigate(screen.route) },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding),
            )
        }
    }
}

fromRoute() 是一个新增的工具方法,将 route 字符串转换为 RallyDestination:

object Overview : RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
    override val screen: @Composable () -> Unit = { OverviewScreen() }

    fun fromRoute(route: String?): RallyDestination =
        when (route?.substringBefore("/")) {
            Accounts.route -> Accounts
            Bills.route -> Bills
            Overview.route -> Overview
            null -> Overview
            else -> throw IllegalArgumentException("Route $route is not recognized!")
        }
}

这样处理后上面 Tab 的行为就正常了,最后还有一项,就是在 Overview 页面点击 Accounts 和 Bills 的 SEE ALL 会跳转到对应页面。这个功能在负责路由的 RallyNavHost 构建 OverviewScreen 的路由是添加相应的事件:

@Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier) {
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        // 构建导航图
        composable(
            route = Overview.route
        ) {
            OverviewScreen(
                // 添加跳转到另外两个页面的事件处理,进行导航
                onClickSeeAllAccounts = { navController.navigate(Accounts.route) },
                onClickSeeAllBills = { navController.navigate(Bills.route) }
            )
        }
        ...
    }
}

效果图:

2024-10-1.Compose Navigation Codelab效果2

3.2 Navigation 传参

导航参数可以通过将一个或多个参数传递到路由并调整参数类型或默认值来使路由行为动态化。单击某个账户并进入一个界面,显示给定账户的数据:

2024-10-1.Compose Navigation Codelab效果3

首先我们增加单个账户的路由:

@Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier) {
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        // 构建导航图
        composable(
            route = Overview.route
        ) {
            OverviewScreen(
                onClickSeeAllAccounts = { navController.navigate(Accounts.route) },
                onClickSeeAllBills = { navController.navigate(Bills.route) },
                // 增加跳转到单个账户页面的事件处理
                onAccountClick = { name -> navigateToSingleAccount(navController, name) }
            )
        }
        ...

        composable(
            route = "${Accounts.route}/{name}",
            arguments = listOf(
                navArgument("name") {
                    type = NavType.StringType
                }
            )
        ) { entry ->
            val accountName = entry.arguments?.getString("name")
            val account = UserData.getAccount(accountName)
            SingleAccountScreen(accountType = account.name)
        }
    }
}

然后增加一个跳转到单个账户页面的方法:

fun navigateToSingleAccount(navController: NavHostController, accountName: String) {
    navController.navigate("${Accounts.route}/$accountName")
}

3.3 深层链接

2024-10-1.深层连接知识

代码实现,以跳转到单个账户页面为例,在路由中为其添加 deepLinks 属性:

@Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier) {
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        ...

        composable(
            route = "${Accounts.route}/{name}",
            arguments = listOf(
                navArgument("name") {
                    type = NavType.StringType
                }
            ),
            // 添加深层连接
            deepLinks = listOf(
                navDeepLink {
                    uriPattern = "rally://${Accounts.route}/{name}"
                }
            )
        ) { entry ->
            val accountName = entry.arguments?.getString("name")
            val account = UserData.getAccount(accountName)
            SingleAccountScreen(accountType = account.name)
        }
    }
}

在清单文件中为 Activity 添加深层链接的 intent-filter:

	<activity
            android:name=".RallyActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <!-- 配置深层链接 -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:host="accounts"
                    android:scheme="rally" />
            </intent-filter>
        </activity>

最后在退出应用后,通过 adb 命令执行深层链接:

User>adb shell am start -d "rally://accounts/Checking" -a android.intent.action.VIEW
Starting: Intent { act=android.intent.action.VIEW dat=rally://accounts/... }

效果图:

2024-10-1.Compose Navigation Codelab效果4

3.4 Compose 与 View 的关系

Compose 是通过 setContent 来展示 Compose 的内容:

// ComponentActivity 的扩展函数
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    // android.R.id.content 的第一个子 View 是否为 ComposeView
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

填充 android.R.id.content 的最大的子 View 如果是 ComposeView,就使用 setContent 展示内容,否则还是使用 setContentView 展示。

ComposeView 继承自 AbstractComposeView,后者继承自 ViewGroup,说明 Compose 的顶层 View 实际上还是几个 View 体系下的 ViewGroup,但是小的控件,像 Text 这些,就都是通过 Canvas 封装一层一层自己画的,与传统 View 没有关系了。当然,传统的 View 也是通过 Canvas 画的,只不过体系内部与 Compose 不同了。但正是因为 Canvas 这一点,使得二者可以集成在一起。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值