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" /><variablename="viewModel"type="com.compose.migration.viewmodels.PlantDetailViewModel" /><variablename="callback"type="com.compose.migration.plantdetail.PlantDetailFragment.Callback" /></data><androidx.coordinatorlayout.widget.CoordinatorLayoutandroid: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.AppBarLayoutandroid: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.CollapsingToolbarLayoutandroid: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"><ImageViewandroid: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.Toolbarandroid: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.NestedScrollViewandroid: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.ComposeViewandroid:id="@+id/compose_view"android:layout_width="match_parent"android:layout_height="wrap_content" /></androidx.core.widget.NestedScrollView><com.google.android.material.floatingactionbutton.FloatingActionButtonandroid: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 = plantDetailViewModellifecycleOwner = viewLifecycleOwnercallback = 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 collapsedplantDetailScrollview.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 shownappbar.isActivated = shouldShowToolbar// Show the plant name if toolbar is showntoolbarLayout.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 的类型是 Plantval 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 的代码块,返回 TextViewfactory = { 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 = plantDetailViewModellifecycleOwner = viewLifecycleOwnercallback = 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-adapterMdcTheme {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: ImageVectorval route: Stringval screen: @Composable () -> Unit
}/*** Rally app navigation destinations*/
object Overview : RallyDestination {override val icon = Icons.Filled.PieChartoverride 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.PieChartoverride val route = "overview"override val screen: @Composable () -> Unit = { OverviewScreen() }
}object Accounts : RallyDestination {override val icon = Icons.Filled.AttachMoneyoverride val route = "accounts"override val screen: @Composable () -> Unit = { AccountsScreen() }
}object Bills : RallyDestination {override val icon = Icons.Filled.MoneyOffoverride 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 selectionoverride val icon = Icons.Filled.Moneyoverride 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 内更新当前正在展示的 screenonSelected = { 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.PieChartoverride val route = "overview"override val screen: @Composable () -> Unit = { OverviewScreen() }fun fromRoute(route: String?): RallyDestination =when (route?.substringBefore("/")) {Accounts.route -> AccountsBills.route -> BillsOverview.route -> Overviewnull -> Overviewelse -> 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:

	<activityandroid: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" /><dataandroid: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 是否为 ComposeViewval existingComposeView = window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as? ComposeViewif (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 attachsetParentCompositionContext(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 presentsetOwners()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 这一点,使得二者可以集成在一起。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯备考:数据结构之栈 和 stack

目录 栈的概念以及栈的实现 STL 的stack 栈和stack的算法题 栈的模板题 栈的算法题之有效的括号 验证栈序列 后缀表达式 括号匹配 栈的概念以及栈的实现 栈是一种只允许在一端进行插入和删除的线性表 空栈&#xff1a;没有任何元素 入栈&#xff1a;插入元素消息 出…

gesp(C++五级)(1)洛谷:B3941:[GESP样题 五级] 小杨的锻炼

gesp(C五级)&#xff08;1&#xff09;洛谷&#xff1a;B3941&#xff1a;[GESP样题 五级] 小杨的锻炼 题目描述 小杨的班级里共有 n n n 名同学&#xff0c;每位同学都有各自的锻炼习惯。具体来说&#xff0c;第 i i i 位同学每隔 a i a_i ai​ 天就会进行一次锻炼&#x…

MIUI显示/隐藏5G开关的方法,信号弱时开启手机Wifi通话方法

5G网速虽快&#xff0c;手机功耗也大。 1.取消MIUI强制的5G&#xff0c;手动设置4G的方法&#xff01; 【小米澎湃OS, Xiaomi HyperOS显示/隐藏5G开关的方法】 1.1.小米MIUI系统升级后&#xff0c;被强制连5G&#xff0c;手动设置开关被隐藏&#xff0c;如下图&#xff1a; 1…

Gateway 网关

1.Spring Cloud Gateway Spring cloud gateway是spring官方基于Spring 5.0、Spring Boot2.0和Project Reactor等技术开发的网关&#xff0c;Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式&#xff0c;Spring Cloud Gateway作为Spring Cloud生态…

python 轮廓 获取环形区域

目录 效果图&#xff1a; 代码&#xff1a; 效果图&#xff1a; 代码&#xff1a; import cv2 import numpy as np# 读取图像 image cv2.imread(rE:\project\jijia\tools_jijia\img_tools\ground_mask.jpg, cv2.IMREAD_GRAYSCALE) # 二值化图像 # 二值化图像 _, binary cv…

MySQL主从复制

文章目录 1.主从复制1.1 概念和原理1.2 案例&#xff1a;一主一从1&#xff09;准备工作2&#xff09;master3&#xff09;slave4&#xff09;测试 1.主从复制 1.1 概念和原理 1.2 案例&#xff1a;一主一从 1&#xff09;准备工作 同步时间 # 安装 ntpdate yum -y install…

网络应用技术 实验七:实现无线局域网

一、实验简介 在 eNSP 中构建无线局域网&#xff0c;并实现全网移动终端互相通信。 二、实验目的 1 、理解无线局域网的工作原理&#xff1b; 2 、熟悉无线局域网的规划与构建过程&#xff1b; 3 、掌握无线局域网的配置方法&#xff1b; 三、实验学时 2 学时 四、实…

51c大模型~合集104

我自己的原文哦~ https://blog.51cto.com/whaosoft/13076849 #Deepfake Detection ACM Computing Surveys | 港大等基于可靠性视角的深度伪造检测综述&#xff0c;覆盖主流基准库、模型 本文作者包括香港大学的王天一、Kam Pui Chow&#xff0c;湖南大学的廖鑫 (共同通讯…

人工智能实验(四)-A*算法求解迷宫寻路问题实验

零、A*算法学习参考资料 1.讲解视频 A*寻路算法详解 #A星 #启发式搜索_哔哩哔哩_bilibili 2.A*算法学习网站 A* 算法简介 一、实验目的 熟悉和掌握A*算法实现迷宫寻路功能&#xff0c;要求掌握启发式函数的编写以及各类启发式函数效果的比较。 二、实验要求 同课本 附录…

Web开发(一)HTML5

Web开发&#xff08;一&#xff09;HTML5 写在前面 参考黑马程序员前端Web教程做的笔记&#xff0c;主要是想后面自己搭建网页玩。 这部分是前端HTML5CSS3移动web视频教程的HTML5部分。主要涉及到HTML的基础语法。 HTML基础 标签定义 HTML定义 HTML(HyperText Markup Lan…

LabVIEW水位监控系统

LabVIEW开发智能水位监控系统通过集成先进的传感技术与控制算法&#xff0c;为工业液体存储提供精确的水位调控&#xff0c;保证了生产过程的连续性与安全性。 项目背景 在化工和饮料生产等行业中&#xff0c;水位控制的准确性对保证生产安全和提高产品质量至关重要。传统的水…

【Rust】结构体定义域实例化

目录 思维导图 1. 结构体的定义与实例化 1.1 结构体的基本概念 1.2 定义结构体 1.3 创建结构体实例 1.4 结构体的定义与实例化示例 2. 访问与修改结构体字段 2.1 访问字段 2.2 修改字段 3. 结构体实例的构造函数 3.1 构造函数的定义 3.2 使用字段初始化简写 4. 结…

013:深度学习之神经网络

本文为合集收录&#xff0c;欢迎查看合集/专栏链接进行全部合集的系统学习。 合集完整版请参考这里。 深度学习是机器学习中重要的一个学科分支&#xff0c;它的特点就在于需要构建多层且“深度”的神经网络。 人们在探索人工智能初期&#xff0c;就曾设想构建一个用数学方式…

Java 将RTF文档转换为Word、PDF、HTML、图片

RTF文档因其跨平台兼容性而广泛使用&#xff0c;但有时在不同的应用场景可能需要特定的文档格式。例如&#xff0c;Word文档适合编辑和协作&#xff0c;PDF文档适合打印和分发&#xff0c;HTML文档适合在线展示&#xff0c;图片格式则适合社交媒体分享。因此我们可能会需要将RT…

【2024年华为OD机试】(C卷,100分)- 攀登者1 (Java JS PythonC/C++)

一、问题描述 题目描述 攀登者喜欢寻找各种地图&#xff0c;并且尝试攀登到最高的山峰。 地图表示为一维数组&#xff0c;数组的索引代表水平位置&#xff0c;数组的元素代表相对海拔高度。其中数组元素0代表地面。 例如&#xff1a;[0,1,2,4,3,1,0,0,1,2,3,1,2,1,0]&…

day06_Spark SQL

文章目录 day06_Spark SQL课程笔记一、今日课程内容二、DataFrame详解&#xff08;掌握&#xff09;5.清洗相关的API6.Spark SQL的Shuffle分区设置7.数据写出操作写出到文件写出到数据库 三、Spark SQL的综合案例&#xff08;掌握&#xff09;1、常见DSL代码整理2、电影分析案例…

Copula算法原理和R语言股市收益率相依性可视化分析

阅读全文&#xff1a;http://tecdat.cn/?p6193 copula是将多变量分布函数与其边缘分布函数耦合的函数&#xff0c;通常称为边缘。在本视频中&#xff0c;我们通过可视化的方式直观地介绍了Copula函数&#xff0c;并通过R软件应用于金融时间序列数据来理解它&#xff08;点击文…

Spring Boot 支持哪些日志框架

Spring Boot 支持多种日志框架&#xff0c;主要包括以下几种&#xff1a; SLF4J (Simple Logging Facade for Java) Logback&#xff08;默认&#xff09;Log4j 2Java Util Logging (JUL) 其中&#xff0c;Spring Boot 默认使用 SLF4J 和 Logback 作为日志框架。如果你需要使…

OpenCV基础:视频的采集、读取与录制

从摄像头采集视频 相关接口 - VideoCapture VideoCapture 用于从视频文件、摄像头或其他视频流设备中读取视频帧。它可以捕捉来自多种源的视频。 主要参数&#xff1a; cv2.VideoCapture(source): source: 这是一个整数或字符串&#xff0c;表示视频的来源。 如果是整数&a…

Uniapp仿ChatGPT Stream流式输出(非Websocket)

Uniapp仿ChatGPT Stream流式输出&#xff08;非Websocket&#xff09; 前言&#xff1a;流式输出可以使用websocket也可以使用stream来实现EventSource是 HTML5 中的一个接口&#xff0c;用于接收服务器发送的事件流&#xff08;Server - Sent Events&#xff0c;SSE&#xff…