在 2019 年的 Google/IO 大会上,亮相了一个全新的 Android 原生 UI 开发框架 Jetpack Compose。与 IOS 的 SwiftUI 一样,Jetpack Compose 也是一个声明式的 UI 框架,随着 Android 和 IOS 两大移动平台相继推出了自己平台专属的声明式 UI 框架,标志着整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程设计

经过两年多的打磨,到了今年七月底,Google 正式发布了 Jetpack Compose 的 1.0 版本,这是 Compose 的稳定版本,可供开发者在生产环境中使用

引用 Google 官网对 Jetpack Compose 的介绍:Jetpack Compose 是用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,帮助您使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用

其核心功能包括:

  • 互操作性:Compose 可以和既有的应用进行互操作。您可以将 Compose UI 嵌入 View,反之亦然。您可以只在屏幕上添加一个按钮,也把自己创建的自定义视图保留在现在用 Compose 打造的界面中
  • Jetpack 集成:Compose 和大家熟知且喜爱的 Jetpack 开发库天然整合。通过与 Navigation、Paging、LiveData (或 Flow/RxJava)、ViewModel 和 Hilt 的整合,Compose 可以与您现有的架构完美共存
  • Material:Compose 提供了 Material Design 组件和主题的实现,使您能够轻松构建符合您的品牌个性的美观应用。Material 主题系统更容易理解和追踪,再也不需要翻阅多个 XML 文件
  • 列表:Compose 的 Lazy 组件为数据列表的呈现提供了一种简单扼要且功能强大的方式,而且将模版代码精简到了最少
  • 动画:Compose 简明的动画 API 让您可以更轻松地打造出让用户眼前一亮的体验

一、compose_chat

技术的世界总是在不断变化的,新的技术总在不断涌现,我数了一下,现在一名 Android 应用开发工程师需要掌握的最基础技能有以下几个,有点 MMP 的感觉 🤣🤣

需要掌握的 UI 开发框架:

  • 传统的 View 体系
  • 跨平台的 Flutter
  • 最新的 Jetpack Compose
  • xxxxx

需要掌握的语言有:

  • Java
  • Dart
  • Kotlin
  • xxxxx

实践出真理,学不动也要学 🤣🤣 Jetpack Compose 大概率会成为以后 Android 原生应用开发时的首选技术方案,所以我也做了一次实战演练,花了两个月时间断断续续写了一个完全用 Compose 实现的 IM APP,实现了基本的好友聊天功能

整个 APP 采用的是单 Activity 形式,内部通过 navigation 来模拟多屏幕跳转,使用了单向数据流模式,所有业务逻辑均通过 ViewModel 来完成,业务逻辑的处理结果再以可组合函数的入参参数的形式回传给 UI 层,底层通过腾讯云的 IM SDK 来实现通信

本文主要介绍的是 Jetpack Compose 的各种重点概念和功能点,并不会涉及太多 compose_chat 的实际编码内容,但会以 compose_chat 作为讲解时的辅助例子。读者如果想要学习 Jetpack Compose 的话,compose_chat 会是一个很好的入门学习项目,希望对你有所帮助 🤣🤣

二、命令式与声明式

长期以来,Android 的视图层次结构都可以表示为一个视图树,若干个 View 和 ViewGroup 以嵌套的关系组成整个视图树,开发者通过 XML 来声明整个视图树的层次结构,再通过 findViewById()来拿到每个控件的引用。当状态发生变化需要刷新 UI 时,就通过主动调用控件的特定方法来更新 UI

整个过程就类似如下所示。通过 TextView 来显示用户名,在用户名发生变化时需要通过主动调用 TextView.setText 方法来刷新 UI,开发者直接持有并维护着每个视图结点,想要更新视图都需要开发者直接向控件下发“指令”,这整个过程的复杂度和出错概率随着需要维护的控件数量的增加而增加。这种方式就属于命令式

    val tvUserName = findViewById<TextView>(R.id.tvUserName)fun onUserNameChanged(userName: String) {tvUserName.text = userName}onUserNameChanged("业志陈")
复制代码

Compose 则是通过声明一系列不包含返回值的可组合(Composable)函数来构建界面的。可组合函数只负责描述所需的屏幕状态,且不提供任何视图控件的引用给到开发者。可组合函数可以包含入参参数,参数就用来参与描述屏幕状态,当需要改变屏幕状态时,也是通过生成新的入参参数并再次调用可组合函数来实现 UI 刷新的

整个过程就类似如下所示。Compose 中对应 TextView 的 "控件" 就是 Text() 函数,Greeting 函数在拿到用户名 name 后,就通过 Text() 函数来进行显示,其接收一个 String 参数用于在屏幕上显示文本信息。在这整个过程中,开发者不持有任何视图节点的引用,而是以描述的方式来声明视图应该如何呈现,且视图并不直接持有状态,而是依赖状态来生成自身。这种方式就属于声明式

声明式 UI 的一个很显著的特点就是:视图是否存在是根据有没有被声明过来决定的

以 compose_chat 为例,主界面包含一个悬浮按钮 floatingActionButton

该按钮只会在 Friendship 页面出现,HomeScreen 依靠if (screenSelected == ViewScreen.Friendship)语句来决定其出现时机,只有 if 语句为 true,FloatingActionButton() 函数才会被执行,此时才会显示悬浮按钮

这和原生的 View 体系有很大区别,按 View 体系的做法来实现的话,FloatingActionButton 会对应一个控件对象且对象是一直存在的,只是我们选择性地通过 View.Gone() 来将其隐藏。而按 Compose 的做法,只有 FloatingActionButton() 函数被执行了悬浮按钮才会存在,否则对于当前屏幕来说,悬浮按钮就相当于从来没有出现过

@Composable
fun HomeScreen(navController: NavHostController,screenSelected: ViewScreen,onTabSelected: (ViewScreen) -> Unit
) {ChatTheme(appTheme = appTheme) {ProvideWindowInsets {ModalBottomSheetLayout() {Scaffold(floatingActionButton = {if (screenSelected == ViewScreen.Friendship) {FloatingActionButton(backgroundColor = MaterialTheme.colors.primary,onClick = {sheetContentAnimateTo(targetValue = ModalBottomSheetValue.Expanded)}) {Icon(imageVector = Icons.Filled.Favorite,tint = Color.White,contentDescription = null,)}}},)}}}
}
复制代码

三、可组合函数

带有 @Composable注解的函数即可组合函数,该注解就用于告知 Compose 编译器:此函数旨在将数据转换为界面。例如,Compose 的Text() 函数就提供了 TextView 的所有功能,开箱即用,方法签名如下所示

@Composable
fun Text(text: String,modifier: Modifier = Modifier,color: Color = Color.Unspecified,fontSize: TextUnit = TextUnit.Unspecified,fontStyle: FontStyle? = null,fontWeight: FontWeight? = null,fontFamily: FontFamily? = null,letterSpacing: TextUnit = TextUnit.Unspecified,textDecoration: TextDecoration? = null,textAlign: TextAlign? = null,lineHeight: TextUnit = TextUnit.Unspecified,overflow: TextOverflow = TextOverflow.Clip,softWrap: Boolean = true,maxLines: Int = Int.MAX_VALUE,onTextLayout: (TextLayoutResult) -> Unit = {},style: TextStyle = LocalTextStyle.current
)
复制代码

当中,最需要关注的当属 Modifier 这个入参参数了,Compose 提供了一系列开箱即用的“控件”函数,例如,对应 FrameLayout 的 Box()、对应 ImageView 的 Image()、对应 RecyclerView 的 LazyColumn() 等,这些控件都包含一个 Modifier 入参参数。Modifier 功能十分强大,每个控件的宽高大小、位置、方向、对齐、裁切、间距、背景色、点击、甚至手势识别等功能都需要通过它来完成,每个功能都通过扩展函数的方式来声明,以链式调用的方式进行使用

	Text(modifier = Modifier.weight(weight = messageWidthWeight, fill = false).padding(start = messageStartPadding,top = messageTopPadding,end = messageEndPadding).clip(shape = messageShape).background(color = friendMsgBgColor).pointerInput(key1 = Unit) {detectTapGestures(onLongPress = {onLongPressMessage(textMessage)},)}.padding(start = messageInnerPadding,top = messageInnerPadding,end = messageInnerPadding,bottom = messageInnerPadding),text = textMessage.msg,style = MaterialTheme.typography.subtitle1,textAlign = TextAlign.Left,)
复制代码

Compose 提供的一系列“控件”函数基本已经能够满足我们的日常开发需求了,我们在开发 Compose 应用时,就可以通过嵌套组合官方提供的控件来实现各种自定义需求

以 compose_chat 为例,好友发送的每一条消息都通过 FriendTextMessageItem() 来显示,包括好友头像和文本消息,当中便嵌套使用到了多个官方控件

四、状态

应用的状态(State)是指可以随时间变化的任何值,其定义十分宽泛,从函数的入参参数到应用的背景色都包括在内

对于 Android 传统的 View 视图结构来说,控件会直接持有着 State。例如,EditText 的内部就包含一个 CharSequence 类型的全局变量 mText,用于存储 EditText 当前显示的文本。当想要改变文本内容时,就需要通过手动调用 EditText.setText 方法来改变 mText,EditText 也随之刷新,mText 即 EditText 持有的 State

而 Compose 通过组合多个可组合函数来描述整个屏幕状态并以此来绘制屏幕,更新视图的唯一途径就是生成新的入参参数并再次调用可组合函数,新的入参参数就代表想要的屏幕状态,每当 State 更新时就会触发可组合函数进行重组,从而实现 UI 刷新。在这整个过程中,可组合函数并不直接持有 State,而是通过读取 State 来确定自身应该如何显示

Compose 中的 OutlinedTextField 函数在功能上就类似于 EditText,但 OutlinedTextField 想要实现和 EditText 相同的效果就会显得稍微麻烦一点

看以下示例,当用户不断向 OutlinedTextField 输入信息时,由于其 value 一直为空字符串,所以 OutlinedTextField 呈现出来的也只会一直是空空如也,因为 OutlinedTextField 只会在 value 值发生变化的时候才进行 UI 刷新

@Composable
fun HelloContent() {OutlinedTextField(value = "",onValueChange = {})
}
复制代码

想要让 OutlinedTextField 实现和 EditText 相同的效果,就需要有一个中间值对其状态进行描述,也即以下的 name。mutableStateOf 函数返回的是与 Compose 运行时集成的可观察类型,当 mutableState 值发生变化时就会触发所有读取该值的可组合函数进行重组。而 remember 的作用就是让 Compose 在初始组合期间将由其保存的值存储在组合中,并在重组期间返回存储的值,从而使得值可以跨越重组被保留下来,当然这个保留期限也只限于可组合函数的单次生命周期内

@Composable
fun HelloContent() {var name by remember { mutableStateOf("") }OutlinedTextField(value = name,onValueChange = {name = it})
}
复制代码

以 compose_chat 为例,其主界面如下所示

主界面由 HomeScreen 函数声明,内部一共包含三个子界面,对应 ConversationScreen、FriendshipScreen、UserProfileScreen 三个函数。HomeScreen 就通过 screenSelected 来控制当前需要显示哪个子界面,当 screenSelected 发生变化时,就会触发 HomeScreen 函数重组,使得 when 表达式再次执行,从而显示新的子界面

@Composable
fun HomeScreen(navController: NavHostController,screenSelected: HomeScreenTab,onTabSelected: (HomeScreenTab) -> Unit,backDispatcher: OnBackPressedDispatcher,
) {ModalBottomSheetLayout() {Scaffold() {when (screenSelected) {HomeScreenTab.Conversation -> {ConversationScreen()}HomeScreenTab.Friendship -> {FriendshipScreen()}HomeScreenTab.UserProfile -> {UserProfileScreen()}}}}
}
复制代码

五、状态提升

看以下例子。name 的存在,相当于使得 HelloContent 这个可组合函数包含了一个内部状态,该状态对于调用方来说是完全不可见的。如果调用方不需要控制状态的变化或者是接收状态变化的通知,那么隐藏内部状态就变得十分有用,因为这样就降低了调用方的调用成本。但是,具有内部状态的可组合项往往不易重复使用,也更难测试

@Composable
fun HelloContent() {var name by remember { mutableStateOf("") }OutlinedTextField(value = name,onValueChange = {name = it})
}
复制代码

如果想要将 HelloContent 改造为无状态模式的话,就需要进行状态提升,状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。仿照 OutlinedTextField 的方式,将状态均交由外部提供,这样 name 这个状态值就交由调用方 HelloContentOwner 来进行维护并持有了

@Composable
fun HelloContent(name: String, onValueChange: (String) -> Unit) {OutlinedTextField(value = name,onValueChange = onValueChange)
}@Composable
fun HelloContentOwner() {var name by remember { mutableStateOf("") }HelloContent(name = name) {name = it}
}
复制代码

以 compose_chat 为例。HomeScreen 的子界面是通过点击底部的 BottomBar 来进行切换的,即切换子界面的请求是从 BottomBar 发起的。BottomBar 需要拿到 screenSelected 才能决定应该选中哪个 tab,HomeScreen 也需要拿到 screenSelected 才能知道当前应该显示哪个子界面,因此 BottomBar 就不应该直接持有 screenSelected 这个状态,而是应该交由调用方来提供,这里就使用到了状态提升这个概念

BottomBar 不直接持有 screenSelected,而是由 HomeScreen 来提供。当用户点击 BottomBar 时,该点击事件也会从 BottomBar 向上传递给 HomeScreen(HomeScreen 再将该事件回调给更上层的调用方),由最上层的调用方来负责改变 screenSelected 的当前值,以此触发 BottomBar 和 HomeScreen 进行重组

在这整个过程中,BottomBar 并不包含任何内部状态,而只负责将用户的点击事件传递给调用方,其本身是无状态的。而 onTabSelected 这个回调函数就相当于应用的业务逻辑,其负责对用户的点击事件进行响应并改变 screenSelected 的值,screenSelected 就相当于应用的状态。当 screenSelected 发生变化时,Compose 就会将最新的状态值传给可组合函数,以此触发屏幕重新绘制。可组合函数由于状态发生变化导致再次执行的过程就称为重组

@Composable
fun HomeScreen(navController: NavHostController,screenSelected: HomeScreenTab,onTabSelected: (HomeScreenTab) -> Unit,backDispatcher: OnBackPressedDispatcher,
) {ChatTheme() {ProvideWindowInsets {ModalBottomSheetLayout {Scaffold(bottomBar = {HomeScreenBottomBar(screenList = ViewScreen.values().toList(),screenSelected = screenSelected,onTabSelected = onTabSelected)},) {when (screenSelected) {HomeScreenTab.Conversation -> {ConversationScreen()}HomeScreenTab.Friendship -> {FriendshipScreen()}HomeScreenTab.UserProfile -> {UserProfileScreen()}}}}}}
}
复制代码

在这整个过程中,状态是向下传递的,而事件是向上传递的。状态下降、事件上升的这种模式称为“单向数据流”,通过遵循单向数据流,可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。因此,应用的界面状态应该都交由可组合函数的入参参数来定义,而应用的业务逻辑应该交由 ViewModel 来容纳并处理,业务逻辑的处理结果再以新的入参参数的形式传递给可组合函数,以此对用户进行响应和界面更新。遵循这种规则后,UI 层就有了一个统一且单一的数据源,这样应用才更不容易出错

提升状态时,有三条规则可帮助开发者弄清楚状态应去向何处:

  1. 状态应至少提升到使用该状态(读取)的所有可组合项的最低共同父项
  2. 状态应至少提升到它可以发生变化(写入)的最高级别
  3. 如果两种状态发生变化以响应相同的事件,它们应一起提升

我认为,遵循状态提升的理念并非一定要做到所有可组合项均无状态。虽然 BottomBar 做到了无状态,但最终 screenSelected 也需要转交给上一级的调用方进行持有,调用方是有状态的。虽然可以将 screenSelected 再次提升到 ViewModel 中进行持有,但像这种不依赖外部环境(例如,网络请求,系统配置等),仅依靠用户事件进行变化的状态,我觉得将其提升到使用该状态(读取)的所有可组合项的最低共同父项即可,不必强行做到所有可组合项均无状态

六、纯函数

在很多讲解关于程序设计最佳实践的文章或者书籍里,都会推荐一个编码原则:尽可能使用 val、不可变对象及纯函数来设计程序。这个原则在 Compose 中也同样需要遵守,因为一个合格的可组合函数就应该属于纯函数,幂等且没有副作用

何谓纯函数?假如一个函数使用相同的入参参数重复执行时,总是能获得相同的运行结果,且运行结果不会影响任何外部状态,也不用担心重复执行会对外部造成改变,那么这个函数就称为纯函数

纯函数不具备副作用,具有引用透明性。副作用就是指修改了某处的某些东西,比如:

  • 引用或修改了外部变量的值
  • 执行了 IO 操作,比如读写了 SharedPreferences
  • 执行了 UI 操作,比如修改了一个按钮的可操作状态

以下例子就不属于纯函数,由于受到外部变量的影响,使用相同入参参数多次执行 count 函数的结果并不全部相同,且每次执行都会影响到外部环境(使用到了 println 函数),这些都属于副作用

var count = 1fun count(x: Int): Int {count += xprintln(count)return count
}
复制代码

使用 Compose 时需要注意:

  • 可组合函数可以按任何顺序执行
  • 可组合函数可以并行执行
  • 重组会跳过尽可能多的可组合函数和 lambda
  • 重组是乐观的操作,可能会被取消
  • 可组合函数可能会像动画的每一帧一样非常频繁地运行

看以下例子。按照我们的直观理解,ButtonRow 中的三个函数是会顺序执行的,且三个函数会得到一个依次递增的 count 值,但这在 Compose 中是没有保障的。为了提升运行效率,Compose 可能会在后台线程中以并行方式来执行多个可组合函数,这也意味着可组合函数之间的先后顺序是不确定的。而且由于存在智能重组,Compose 会自动识别出哪些可组合函数并没有发生更改从而跳过重组,而仅仅重新执行发生变化的部分函数,从而仅更新屏幕的某一部分

Compose 的这些举措都是为了提升程序的运行效率还有屏幕的绘制效率,但这也导致可组合函数在读写外部变量时是完全没有保障的,我们无法假设在执行 MiddleScreen 前 StartScreen 就已经修改了 count,两者的顺序可能完全相反甚至根本没有被执行

@Composable
fun ButtonRow() {StartScreen()MiddleScreen()EndScreen()
}var count = 0@Composable
fun StartScreen() {Text(text = count.toString())count += 1
}@Composable
fun MiddleScreen() {Text(text = count.toString())count += 1
}@Composable
fun EndScreen() {Text(text = count.toString())count += 1
}
复制代码

此外,由于可组合函数可能会像每一帧一样频繁地重新执行,例如在执行动画时,可组合函数会快速地执行,以避免在播放动画期间出现卡顿,这个过程中可组合函数就会不断地在被重复调用。如果可组合函数存在副作用,例如内部存在读写 SharedPreferences 的操作,可能一秒钟就会被调用几百次,这就会严重影响到屏幕的绘制效率,从而导致页面卡顿了,所以对于这类执行成本高昂的操作,需要放到后台协程中执行,并将执行结果作为参数传递给可组合函数

所以说,可组合函数需要做到无副作用才能得到正确的期望结果。此外,对于相同的入参参数,可组合函数应该一直呈现相同的表现形式。多个可组合函数之间应该保持状态独立,不能具有互相依赖性。对于共享的状态,应该以入参参数的形式进行传递,并且将状态放在最顶级函数或者 ViewModel 中进行维护

七、副作用

在某些情况下,可组合函数可能无法做到完全无副作用,例如,我们在切换应用主题的时候希望系统状态栏也能跟着一起改变背景色,此时就说可组合函数产生了副作用。为了处理这种情况,Compose 也提供了 Effect API,以便以可预测的方式执行这些附带效应

以 compose_chat 为例。设置系统状态栏的颜色是通过 SetSystemBarsColor() 函数来实现的。我们希望在应用刚启动时,以及应用主题发生变化时,SetSystemBarsColor() 都能够被执行一次,这可以通过 DisposableEffect 来实现

当 DisposableEffect 进入组合时,或者是 key 发生变化时,DisposableEffect 内的代码就会被执行,从而改变系统状态栏,除此之外每次界面重组时都不会再次执行,从而避免了无意义的调用

@Composable
fun SetSystemBarsColor(key: Any = Unit,statusBarColor: Color = MaterialTheme.colors.background,navigationBarColor: Color = MaterialTheme.colors.background
) {val systemUiController = rememberSystemUiController()val isLight = MaterialTheme.colors.isLightDisposableEffect(key1 = key) {systemUiController.setStatusBarColor(color = statusBarColor,darkIcons = isLight)systemUiController.setNavigationBarColor(color = navigationBarColor,darkIcons = isLight)systemUiController.systemBarsDarkContentEnabled = isLightonDispose {}}
}
复制代码

HomeScreen 传给 SetSystemBarsColor 的 Key 也即 appTheme,因此每次切换主题后都能保证 DisposableEffect 会再次执行

@Composable
fun HomeScreen(navController: NavHostController,screenSelected: HomeScreenTab,onTabSelected: (HomeScreenTab) -> Unit
) {val homeViewModel = viewModel<HomeViewModel>()val appTheme by homeViewModel.appTheme.collectAsState()ChatTheme(appTheme = appTheme) {ProvideWindowInsets {SetSystemBarsColor(key = appTheme,statusBarColor = Color.Transparent,navigationBarColor = MaterialTheme.colors.primaryVariant)}}
}
复制代码

更多关于 Effect API 的介绍请看这里:Compose 中的附带效应

八、布局

布局是每个 UI 框架都必须的功能,不管是 View、Flutter、还是 Compose,都必须提供一些开箱即用的布局控件,这里就介绍下 compose_chat 中使用得比较多的 ConstraintLayout 和 LazyColumn

ConstraintLayout

在以往的 Android View 视图体系下,我们都会尽量避免在布局中进行多层嵌套,因为嵌套层次越深,在测量 View 时需要的次数和时间就会越多,这就会严重影响到应用的运行性能了,因此 Google 官方也建议开发者尽量使用 ConstraintLayout 进行布局,实现扁平化布局。但 Compose 不同,由于 Compose 可以避免多次测量,因此开发者可以根据需要进行深层次嵌套,而不用担心会影响性能。同时 Compose 也提供了自己专属的 constraintlayout-compose,用于实现约束定位

以 compose_chat 为例。每个好友列表 Item 就对应 FriendshipItem() 函数,其内部就使用到了 ConstraintLayout 进行布局,其使用思路和 View 版本的差不多。首先需要通过 createRefs() 来声明“控件”的引用,然后通过 constrainAs 将引用和“控件”绑定在一起,类似于为“控件”声明 ID,之后每个“控件”之间就可以通过 linkTo 方法进行关联定位了,当中 parent 即指 ConstraintLayout 自身

LazyColumn

在以往的 Android View 视图体系下,我们在加载长列表时一般是通过 RecyclerView 来实现的,以便能够缓存复用 Item,RecyclerView 在滑动性能上还是很优越的,能有效地避免由于数据量过大导致滑动卡顿的情况,缺点就是需要由开发者来声明各种 Adapter 和 ViewHolder,这一点比较麻烦

Compose 中对应 RecyclerView 的是 LazyColumn() 函数,从名字就可以猜出该函数是个竖向滑动列表,且实现了懒加载。实际上 LazyColumn 也的确实现了 Item 的缓存复用机制,重点在使用上要比 RecyclerView 简单很多,因为我们再也不用声明各种繁琐的 Adapter 和 ViewHolder 了

以 compose_chat 为例。在拿到好友列表 friendList 后,通过 for 循环,每个 PersonProfile 就对应 LazyColumn 的一个列表项 item(),在 item() 函数中声明的 FriendshipItem() 即每个列表项要呈现的视图。通过循环调用 item() 函数,就可以完成整个滑动列表数据项的声明了,而且即使存在多种样式的 Item,也通过相同方式来声明即可,就像以下例子中我为列表添加了一个底部间距 Spacer

@Composable
fun FriendshipScreen(paddingValues: PaddingValues,friendList: List<PersonProfile>,onClickFriend: (PersonProfile) -> Unit
) {Scaffold(modifier = Modifier.padding(bottom = paddingValues.calculateBottomPadding()).fillMaxSize()) {if (friendList.isEmpty()) {EmptyView()} else {LazyColumn {friendList.forEach {item(key = it.userId) {FriendshipItem(personProfile = it, onClickFriend = onClickFriend)}}item {Spacer(modifier = Modifier.height(40.dp))}}}}
}
复制代码

九、动画 & 手势操作

Jetpack Compose 提供了一些功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。动画在现代移动应用中至关重要,其目的是实现自然流畅、易于理解的用户体验。许多 Jetpack Compose 动画 API 可以提供可组合函数,就像布局和其他界面元素一样;它们由使用 Kotlin 协程挂起函数构建的较低级别 API 提供支持

此外,Compose 也提供了多种 API 用于检测用户互动生成的手势。API 涵盖各种用例:

  • 其中一些级别较高,旨在覆盖最常用的手势。例如,clickable修饰符可用于轻松检测点击,此外它还提供无障碍功能,并在点按时显示视觉指示(例如涟漪)
  • 还有一些不太常用的手势检测器,它们在较低级别提供更大的灵活性,例如 PointerInputScope.detectTapGestures 或 PointerInputScope.detectDragGestures,但不提供额外功能

以 compose_chat 为例。个人资料页 ProfileScreen 就同时应用到了动画和手势操作:背景图同时包含了 裁切 + 缩放 + 旋转 三种动画,用户可以拖拽头像进行移动,当松手时头像也会通过弹簧动画自动移回原位

动画

前文很多地方都有讲到:更新视图的唯一途径就是生成新的入参参数并再次调用可组合函数,动画也是如此。想要让视图以一种连贯且自然的方式进行变换,那么意思也就是说需要有一个值生成器来连贯地改变可组合函数的参数值

ProfileScreen 使用rememberInfiniteTransition()来实现这种效果。InfiniteTransition 通过 animateFloat、animateValue、animateColor 等方式来保存子动画,这些动画一进入组合阶段就开始运行,除非被移除,否则不会停止。再为 InfiniteTransition 指定一个初始值和一个结束值,并指定动画以反转的形式来回运行,animateValue 就会在这两个值之间不断地连贯变化,之后将 animateValue 应用到背景图的布局参数上即可实现动画效果

        val animateValue by rememberInfiniteTransition().animateFloat(initialValue = 1.3f, targetValue = 1.9f,animationSpec = infiniteRepeatable(animation = tween(durationMillis = 1800, easing = FastOutSlowInEasing),repeatMode = RepeatMode.Reverse,),)NetworkImage(data = userFaceUrl,modifier = Modifier.constrainAs(ref = background) {}.fillMaxWidth().aspectRatio(ratio = 5f / 4f).scrim(colors = listOf(Color(0x40000000), Color(0x40F4F4F4))).clip(shape = BezierShape(padding = animateValue * 100)) //裁切.scale(scale = animateValue) //缩放.rotate(degrees = animateValue * 10.3f) //旋转)
复制代码

手势操作

Compose 中的 Modifier 十分强大,不仅仅是用于进行布局,像点击事件、手势操作等一样需要依靠其来完成,pointerInput 函数就用于识别用户的手势操作,Modifier 同时提供了 offset 函数用于控制控件的偏移量,通过结合 pointerInput 和 offset 两个函数来动态改变控件的偏移量,就可以实现拖拽用户头像了

拖拽 OutlinedAvatar 的过程中系统会不断回调 onDrag 函数,在回调里通过用户的拖拽值不断改变 offsetX 和 offsetY 两个值,就可以不断触发 OutlinedAvatar 进行重组,以此实现拖拽效果。当用户松手时,onDragEnd 函数会被回调,再通过 Animatable 将 OutlinedAvatar 的偏移量重置为零,这样就可以实现自动移回原位的效果了

        val coroutineScope = rememberCoroutineScope()var offsetX by remember { mutableStateOf(0f) }var offsetY by remember { mutableStateOf(0f) }OutlinedAvatar(data = userFaceUrl,modifier = Modifier.offset {IntOffset(x = offsetX.roundToInt(),y = offsetY.roundToInt())}.pointerInput(Unit) {detectDragGestures(onDragStart = {},onDragCancel = {},onDragEnd = {coroutineScope.launch {Animatable(initialValue = Offset(offsetX, offsetY),typeConverter = Offset.VectorConverter).animateTo(targetValue = Offset(x = 0f, y = 0f),animationSpec = SpringSpec(dampingRatio = Spring.DampingRatioHighBouncy),block = {offsetX = value.xoffsetY = value.y})}},onDrag = { change, dragAmount ->change.consumeAllChanges()offsetX += dragAmount.xoffsetY += dragAmount.y},)})
复制代码

十、主题

在以前,Android 应用实现多主题 Theme 切换时都需要声明多个 XML 文件,例如在实现夜间模式时就需要两套 colors.xml 和 styles.xml,这种机制在性能上不能说低,但在易用性上的确不高

Compose 的 Theme 就比较优秀了,完全基于 Kotlin 语言来实现,避免了原生实现方式的那种割裂感,相对原生的实现方式在性能和易用性上都提升了很多。Compose 提供了 MaterialTheme 这一种基于 Material Design 风格样式的主题,androidx.compose.material 包内提供的所有控件都遵循 MaterialTheme 进行设计,保证了整个应用统一的风格样式

当使用 Android Studio 创建一个 Compose 工程时,会自动在 ui.theme 包目录下创建以下四个文件,当中就默认提供了 Dark 和 Light 两种主题样式

DarkColorPalette 和 LightColorPalette 分别定义了在夜间模式和日间模式下使用的颜色值,通过选取不同的 colors 对象传给 MaterialTheme 就可以实现不同的主题样式

private val DarkColorPalette = darkColors(primary = Purple200,primaryVariant = Purple700,secondary = Teal200
)private val LightColorPalette = lightColors(primary = Purple500,primaryVariant = Purple700,secondary = Teal200
)@Composable
fun MyApplicationTheme(darkTheme: Boolean = isSystemInDarkTheme(),content: @Composable() () -> Unit
) {val colors = if (darkTheme) {DarkColorPalette} else {LightColorPalette}MaterialTheme(colors = colors,typography = Typography,shapes = Shapes,content = content)
}
复制代码

MaterialTheme 由颜色 colors、排版 typography、形状 shapes 共同组成,当自定义这些属性后,所做的更改会自动反映在所有用来构建应用的组件中。MaterialTheme 会将不同的配置项映射保存为应用的环境变量,例如我们传入的 colors 就保存为了静态常量 LocalColors

internal val LocalColors = staticCompositionLocalOf { lightColors() }@Composable
fun MaterialTheme(colors: Colors = MaterialTheme.colors,typography: Typography = MaterialTheme.typography,shapes: Shapes = MaterialTheme.shapes,content: @Composable () -> Unit
) {val rememberedColors = remember {// Explicitly creating a new object here so we don't mutate the initial [colors]// provided, and overwrite the values set in it.colors.copy()}.apply { updateColorsFrom(colors) }val rippleIndication = rememberRipple()val selectionColors = rememberTextSelectionColors(rememberedColors)CompositionLocalProvider(LocalColors provides rememberedColors,LocalContentAlpha provides ContentAlpha.high,LocalIndication provides rippleIndication,LocalRippleTheme provides MaterialRippleTheme,LocalShapes provides shapes,LocalTextSelectionColors provides selectionColors,LocalTypography provides typography) {ProvideTextStyle(value = typography.body1, content = content)}
}
复制代码

Compose 中提供的各种“控件”函数默认都会来读取 LocalColors 中的颜色值来绘制自身,例如 Surface 默认就以 MaterialTheme.colors.surface作为背景色

@Composable
fun Surface(modifier: Modifier = Modifier,shape: Shape = RectangleShape,color: Color = MaterialTheme.colors.surface,contentColor: Color = contentColorFor(color),border: BorderStroke? = null,elevation: Dp = 0.dp,content: @Composable () -> Unit
)
复制代码

以 compose_chat 为例,一共提供了三套主题:Light、Dark、Pink

三种主题类型就对应枚举类 AppTheme 和三套颜色值,根据当前选中的主题类型传给 MaterialTheme 不同的颜色值即可

private val LightColorPalette = lightColors(background = BackgroundColorLight,primary = PrimaryColorLight,primaryVariant = PrimaryVariantColorLight,surface = SurfaceColorLight,secondary = DivideColorLight,
)private val DarkColorPalette = darkColors(background = BackgroundColorDark,primary = PrimaryColorDark,primaryVariant = PrimaryVariantColorDark,surface = SurfaceColorDark,secondary = DivideColorDark,
)private val PinkColorPalette = lightColors(background = BackgroundColorPink,primary = PrimaryColorPink,primaryVariant = PrimaryVariantColorPink,surface = SurfaceColorPink,secondary = DivideColorPink,
)@Composable
fun ChatTheme(appTheme: AppTheme = AppThemeHolder.currentTheme,content: @Composable () -> Unit
) {val colors = when (appTheme) {AppTheme.Light -> {LightColorPalette}AppTheme.Dark -> {DarkColorPalette}AppTheme.Pink -> {PinkColorPalette}}val typography = if (appTheme.isDarkTheme()) {DarkTypography} else {LightTypography}MaterialTheme(colors = colors,typography = typography,shapes = AppShapes,content = content)
}
复制代码

Compose 的主题切换也依赖于可组合函数的重组操作。以 compose_chat 为例,HomeScreen 内部整体是包裹在 ChatTheme 中的,当切换了应用主题,即改变了 appTheme 时,就会触发 ChatTheme 和 HomeScreen 进行重组,重组过程就会读取到最新的主题配置,从而实现了主题切换

@Composable
fun HomeScreen(navController: NavHostController,screenSelected: ViewScreen,onTabSelected: (ViewScreen) -> Unit
) {val homeViewModel = viewModel<HomeViewModel>()val appTheme by homeViewModel.appTheme.collectAsState()ChatTheme(appTheme = appTheme) {}
}
复制代码

可以看出来,Compose 的主题切换是完全依赖于内存读写的,避免了原生 Android 方式还需要通过 IO 流去读取 XML 文件的情况,在执行效率上相对会高很多,而且在定义主题时也十分方便,仅需要多声明一种 Colors 对象即可实现,类型安全且有助于减少代码量

十一、结尾

关于 Jetpack Compose 的大部分知识点都讲完了,自我感觉 compose_chat 能很好的帮助读者入门,最后当然也少不了源码了

项目地址:compose_chat

APK 下载尝鲜:compose_chat

由于腾讯云 IM SDK 免费版最多只能注册一百个账号,因此读者如果发现注册不了的话,可以使用以下几个我预先注册好的账号,但多设备同时登陆的话会互相挤掉线 ~~

  • Google
  • Android
  • Compose
  • Flutter
  • Java
  • Kotlin
  • Dart
  • Jetpack
  • ViewModel
  • LiveData


作者:业志陈
链接:https://juejin.cn/post/6991429231821684773
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

相关文章

  1. Pulsar基础(三)—多租户模式介绍

    多租户模式介绍 什么是多租户&#xff1f; 多租户是一种架构&#xff0c;目的是为了让多用户环境下使用同一套程序&#xff0c;且保证用户间数据隔离简单讲&#xff1a;在一台服务器上运行单个应用实例&#xff0c;它为多个租户&#xff08;客户&#xff09;提供服务。多租户…...

    2024/4/24 14:15:17
  2. 机器学习:神经网络正向传播与反向传播的向量化推导

    文章目录向量化终极技巧神经网络架构正向传播反向传播代价函数误差传播计算梯度虽然吴恩达推荐在第一次实现神经网络的时候用 for循环挨个挨个传入样本正向传播反向传播&#xff0c;每次累加误差与隐藏层的成绩来计算梯度&#xff0c;但我不满意呀&#xff0c;妥妥能向量化的东…...

    2024/4/14 5:34:11
  3. 《C#零基础入门之百识百例》(六十七)枚举常用方法 -- 使用示例

    C#零基础入门结构体和枚举 枚举常用方法 -- 使用示例 前言一,CompareTo方法二,Equals方法三,GetName/GetNames方法四,GetValues方法五,IsDefined方法六,ToString方法前言 本文属于C#零基础入门之百识百例系列文章。此系列文章旨在为学习C#语言的童鞋提供一套系统的学习路…...

    2024/4/27 11:47:31
  4. python元组(tuple)

    tuple用( )定义&#xff0c;元素之间用逗号分隔支持索引、切片tuple不可变&#xff0c;没有增、删、改、查需要改变可转list 访问元组 tuple索引、切片与list一样 从前索引 1 2 3 tuple ("red","green","blue&…...

    2024/4/14 5:33:51
  5. R语言实战应用精讲50篇(二十九)-数据可视化应用案例(附R语言代码)

    条形图、饼图、treemap及其局部调整 # 条形图、饼图、树形图及其局部调整 library(tidyverse) mtcars %>%mutate(cyl = as_factor(cyl)) %>%ggplot(aes(x = cyl)) +geom_bar()mtcars %>%mutate(cyl = as_factor(cyl)) %>%ggplot(aes(y = cyl)) +geom_bar()library…...

    2024/5/2 9:29:51
  6. MATLAB绘图应用案例100篇(二)-画个灯笼,过年的气氛搞起来

    MATLAB源码 clc;clear;close all;axes(Color,k,DataA,[1,1,1])hold ona=315;b=(0:.01:pi);[x,y,z]=sphere(a-1);v=(abs(sin(20*b))+3)/4;v(1:60)=nan;A=ones(a).*v.*cat(3,1,0,0).*sin(b);B=ones(a,a,3).*cat(3,1,1,0);surf(x.*v,y.*v,z,A);surf(x/2,y/2,z/2,B);shading flatr…...

    2024/4/7 16:49:59
  7. 哥德巴赫猜想的证明(李扩继)

    哥德巴赫猜想是一个纯数学命题&#xff0c;是偶数的分类问题。偶数奇合数奇合数奇合数素数素数素数。本来没有问题的事&#xff0c;可在数学领域里&#xff0c;总要把每一个命题是真的还是假的要判断清楚&#xff0c;德国的一个中学数学教师哥德巴赫(1690-1764)想证明“偶数素数…...

    2024/4/18 18:08:30
  8. MySQL Learning Note 005 -- Adding and deleting

    Add data to tables INSERT INTO table_name VALUES(col_1, col_2, col_3, ...); // 按顺序填入所有fields&#xff0c;字符串要加引号, 每个field之间要加逗号&#xff0c;结尾要加分号 Use null when the field is auto-generated override auto value by manually providi…...

    2024/4/14 23:08:37
  9. PM的FAQ1项目管理中如何做好费用监控、资源共享?

    PM的FAQ1项目管理中如何做好费用监控、资源共享&#xff1f; 随着信息技术的发展&#xff0c;如何有效地管理项目已经成为了项目管理人员重点关心的问题与工作职责。很多企业采用传统的手工管理&#xff0c;这种方式不但复杂、而且数据常常不准确&#xff0c;尤其是项目费用与…...

    2024/4/20 14:41:51
  10. maven 打包将依赖打进jar包

    最近在做JAVA 的SDK 工具&#xff0c;由于SDK 依赖了其他的一些开源工具包&#xff0c;打包时少了依赖工具包&#xff0c;这样其他项目想要用SDK 就需要自己额外增加响应依赖&#xff0c;所以想要把依赖打进SDK。 其实这也很简单&#xff0c;只需要更改maven 配置即可&#xff…...

    2024/4/14 5:34:57
  11. Python的线程12 简易限流器

    正式的Python专栏第49篇&#xff0c;同学站住&#xff0c;别错过这个从0开始的文章&#xff01; 前篇学委展示了Semaphore信号量&#xff0c;这个工具可以让开发者设置阀值&#xff0c;简单的控制并发的数量。 不知道读者还记得前篇设置信号量为1的时候&#xff0c;三个运动员…...

    2024/4/14 5:34:57
  12. 函数的四种特性——有界性 单调性 奇偶性 周期性

    目录 1、有界性 2、单调性 3、奇偶性 4、周期性 5、重要结论✬✬✬&#xff08;必背&#xff09; 1、有界性 设f(x)的定义城为D,数集I⊂D.如果存在某个正数M,使对任一x∈I,有|f(x)|≤M,则称f(x)在I上有界:如果这样的M不存在&#xff0c;则称f(.x)在I上无界。 [注] (1)从…...

    2024/4/14 5:35:02
  13. 84Linux yum85小结

    文章目录yumyum...

    2024/4/14 5:34:42
  14. 函数的四种特性——1、有界性2、单调性3、奇偶性4、周期性5、重要结论✬✬✬(必背)

    目录 1、有界性 2、单调性 3、奇偶性 4、周期性 5、重要结论✬✬✬&#xff08;必背&#xff09; 1、有界性 设f(x)的定义城为D,数集I⊂D.如果存在某个正数M,使对任一x∈I,有|f(x)|≤M,则称f(x)在I上有界:如果这样的M不存在&#xff0c;则称f(.x)在I上无界。 [注] (1)从…...

    2024/4/14 5:35:07
  15. 接水问题蓝桥杯

    学校里有一个水房&#xff0c;水房里一共装有 mm 个龙头可供同学们打开水&#xff0c;每个龙头每秒钟的供水量相等&#xff0c;均为 1。 现在有 nn 名同学准备接水&#xff0c;他们的初始接水顺序已经确定。将这些同学按接水顺序从 1 到 nn编号&#xff0c;ii 号同学的接水量为…...

    2024/4/14 5:34:57
  16. 【10章Java集合】几张脑图带你进入Java集合的头脑风暴

    ❤写在前面 ❤博客主页&#xff1a;努力的小鳴人 ❤系列专栏&#xff1a;Java基础学习&#x1f60b; ❤欢迎小伙伴们&#xff0c;点赞&#x1f44d;关注&#x1f50e;收藏&#x1f354;一起学习&#xff01; ❤如有错误的地方&#xff0c;还请小伙伴们指正&#xff01;&#x1…...

    2024/4/14 5:34:52
  17. 44 WM配置-作业-库存盘点-定义差异和凭证限制

    业务背景&#xff1a;定义盘点凭证的项目数量设置&#xff0c;以及711盘亏和712盘盈的移动类型设置。 事务码&#xff1a;SPRO SPRO路径&#xff1a;SPRO->后勤执行->仓库管理->作业->库存盘点->定义差异和凭证限制 第1步&#xff0c;SPRO进入 第2步&#xf…...

    2024/4/14 16:40:41
  18. Qt学习之事件过滤器的使用(实现QLineEdit点击效果)

    在做模仿qq登录界面的时候遇到了一个问题&#xff0c;就是如何让编辑框也拥有类似于按钮那样的点击之后完成某件事情的效果。 方法一&#xff1a;可以自定义信号。 方法二&#xff1a;可以使用事件过滤器来实现编辑框的点击事件。 方法三&#xff1a;使用编辑框自己的信号&…...

    2024/4/20 5:40:44
  19. 在cirfa10数据集上实现一个文艺复兴期间的CNN网络VGG

    在cirfa10数据集上实现一个文艺复兴期间的CNN网络VGG &#x1f64b;‍♂️ 张同学&#xff0c;zhangruiyuanzju.edu.cn 有问题请联系我~ 这里是目录呀~在cirfa10数据集上实现一个文艺复兴期间的CNN网络VGG〇、背景介绍一、只使用torch.nn.XXX来构建网络结构二、使用Pytorch官方…...

    2024/4/15 17:00:16
  20. 详解“位段”

    序言 我们学过结构体的大小是怎么计算的&#xff0c;我们会有内存对齐的概念&#xff0c;今天基于结构体的知识来进行位段的相关知识 位段 在C语言中&#xff0c;位段的声明和结构&#xff08;struct&#xff09;类似&#xff0c;但它的成员是一个或多个位的字段&#xff0c;这…...

    2024/4/14 5:35:02

最新文章

  1. STM32单片机通过串口控制DDSM210 直驱伺服电机

    1 电机介绍 官方资料&#xff1a;https://www.waveshare.net/wiki/DDSM210 DDSM210 直驱伺服电机是基于一体化开发理念&#xff0c;集外转子无刷电机、编码器、伺服驱动于一体的高可靠性永磁同步电动机&#xff0c;其结构紧凑&#xff0c;安装方便&#xff0c;运行稳定&#x…...

    2024/5/2 18:32:19
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 【Ubuntu】在 Windows 和 Ubuntu 之间传输文件

    在 Ubuntu 上安装 Samba&#xff1a; sudo apt-get update sudo apt-get install samba在 Ubuntu 上创建一个共享文件夹并设置权限&#xff1a; mkdir /home/your_username/shared sudo chown nobody:nogroup /home/your_username/shared sudo chmod 0777 /home/your_username/…...

    2024/5/2 5:54:29
  4. 【干货】零售商的商品规划策略

    商品规划&#xff0c;无疑是零售业的生命之源&#xff0c;是推动业务腾飞的强大引擎。一个精心策划的商品规划策略&#xff0c;不仅能帮助零售商在激烈的市场竞争中稳固立足&#xff0c;更能精准捕捉客户需求&#xff0c;实现利润最大化。以下&#xff0c;我们将深入探讨零售商…...

    2024/5/1 13:01:46
  5. 开启 Keep-Alive 可能会导致http 请求偶发失败

    大家好&#xff0c;我是蓝胖子&#xff0c;说起提高http的传输效率&#xff0c;很多人会开启http的Keep-Alive选项&#xff0c;这会http请求能够复用tcp连接&#xff0c;节省了握手的开销。但开启Keep-Alive真的没有问题吗&#xff1f;我们来细细分析下。 最大空闲时间造成请求…...

    2024/5/1 13:02:34
  6. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  7. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/2 16:16:39
  8. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  9. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/2 9:28:15
  10. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  11. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  12. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  13. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  14. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  15. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/2 15:04:34
  16. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  17. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  18. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  19. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  20. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  21. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  22. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  23. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  24. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/2 9:07:46
  25. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57