Compose是一个由 Google Android 团队官方推出的声明式UI框架,对标 我们之前使用的 View体系(命令式UI)。
命令式:需要从头开始,先创建View,然后拿到View,再来更新View。
声明式:事先声明好了UI布局,通过维护UI的状态来更新控件的状态。框架内部帮我们哪些命令式的操作。
- 通过XML 来写布局。
LayoutInflater
读取 XML 并解析,然后创建对应的View。- 将View关联到 Window上,这里可能是 Activity、Dialog等,我们会 使用 Java 或者 Kotlin 来开发。
存在问题:
系统需要读取解析XML,在转为View,存在性能损耗。当然我们也可以直接使用代码的方式来布局并创建View,只不过写法相对繁琐。
- 是一种声明式UI框架,可以方便的 使用 Kotlin 直接以纯代码的方式来写布局。也算是顺应了时代的潮流。
- 通过修改控件的状态来刷新UI。
存在的问题:
每个状态的变更都会需要去刷新界面,这里会依赖 声明式UI框架的优化策略。跳过状态没有变化的控件,只更新状态变化的控件。
可组合函数用于描述所需的界面状态,并不是结构界面组件。
Compose 在渲染时并不会转化成
View
,它的布局与渲染还是在LayoutNode
上完成的
我们通过添加 @Composable
注解,即可定义一个可组合函数,这个注释会告诉 Compose 编译器:这个函数是将数据转换为界面。
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
- 只有 Composable 函数内能调用 Composable 函数。
- 可组合函数可能会像动画的每一帧一样非常频繁地运行,所以应避免副作用(Effect)。
- 可组合函数可以按任何顺序执行,可组合函数可以并行运行。
**输入更改时会再次调用可组合函数,这个过程叫做重组。**Compose 的重组是其声明式 UI 运转的基础,每当状态更新时,都会发生重组,不过会跳过尽可能多的可组合函数和 lambda,仅重组需要更新的部分。
同时重组是乐观操作,Compose 会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改,Compose 可能会取消重组,并使用新参数重新开始。(但是 Effect 依旧会执行,所以可能会导致异常)。
但并不是说数据没变就不会重组,当调用点发生变化时也会触发重组。同时 不稳定类型也不能跳过重组。
调用点:调用可组合项的源代码位置。会影响其在组合中的位置,因此会影响界面树。
不稳定类型:例如一个有 var 成员的 data class。https://developer.android.com/develop/ui/compose/performance/stability
稳定类型:不可变对象(val String等)、仅有 val 成员的 data class 。稳定类型的成员必须也是稳定类型。
-
每个调用都有唯一的调用点和源位置,编译器将使用它们对调用进行唯一识别。
-
当从同一个调用点多次调用某个可组合项时,除了调用点之外,还会使用执行顺序来区分实例。
所以左侧图例中 列表下方增加数据时,已存在部分将会被重复使用。但是在上方增加、移除或者数据重排时,将会导致参数变化的位置发生重组。
而右侧图例中通过使用 key
指定唯一性 来避免重组。
重组策略 | 说明 | 使用场景 |
---|---|---|
DisposeOnDetachedFromWindowOrReleasedFromPool | 默认策略。当组合依赖的ComposeView 从 Window 分离或不在容器池时,组合将被释放。 | |
DisposeOnLifecycleDestroyed | ComposeView对应的Lifecycle 被销毁时,组合将被释放 | |
DisposeOnViewTreeLifecycleDestroyed | 当ViewTreeLifecycleOwner.Lifecycle 被销毁时,组合将被释放。即Activity.view 或者 Fragment.view 被销毁时 |
Fragment 中使用ComposeView时 |
添加 @Preview
注解后,就能在 Android Stuido 中预览布局。
不建议在正式函数中使用,应单独定义一个预览专用的函数。
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyComposeTheme {
Greeting("Android")
}
}
Compose的默认布局是重叠布局,同 FrameLayout的效果。
Compose中父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整。整个过程仅测量一次子项
首先,系统会要求每个节点对自身进行测量,然后以递归方式完成所有子节点的测量,并将尺寸约束条件沿着树向下传递给子节点。再后,确定叶节点的尺寸和放置位置,并将经过解析的尺寸和放置指令沿着树向上回传。
- 节点:measure -> 递归处理子节点(无子节点则跳过) -> size and place
帧渲染主要有三个阶段:
- 组合:界面显示哪些内容。运行可组合函数构建界面说明。
- 布局:测量并放置元素。
- 绘制:界面元素绘制到画布(屏幕)。
使用 layout
修饰符来修改元素的测量和布局方式。
包含2个参数:measurable(测量的元素)、constraints(来自父的约束条件)。
- 测量:
measurable.measure(constraints)
- 指定尺寸:
layout(placeable.width, height){}
- 放置到屏幕上:
placeable.placeRelative(0, placeableY)
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable, constraints ->
// Measure the composable:测量
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
// 指定可组合项的尺寸
layout(placeable.width, height) {
// Where the composable gets placed:放置到屏幕的位置
placeable.placeRelative(0, placeableY)
}
}
Layout
可组合项可以手动测量和布局子项,实现自定义布局。
measurables:需要测量的子项列表。
constraints:来自父的约束条件
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
// 测量所有子项
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
// 自定尺寸
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
// 放置到屏幕上
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
height(IntrinsicSize.Min)
可将其子项的高度强行调整为最小固有高度。
该修饰符具有递归性,它将查询 Row
及其子项 minIntrinsicHeight
。
@Composable
fun TwoTexts(
text1: String,
text2: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(
color = Color.Black,
modifier = Modifier.fillMaxHeight().width(1.dp)
)
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
自定义布局可以重写
MeasurePolicy
相关方法。
@Composable
fun MyCustomComposable(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
return object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
// Measure and layout here
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = {
// Logic here
}
// Other intrinsics related methods have a default value,
// you can override only the methods that you need.
}
}
clickable:检测对元素的点击。
@Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// content that you want to make clickable
Text(
text = count.value.toString(),
modifier = Modifier.clickable(..配置参数) { count.value += 1 }
)
}
// clickable() 包含很多配置参数
// 例如 去除水波纹
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
)
pointerInput:提供更详细的事件。
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}
- verticalScroll:垂直滚动
- horizontalScroll:水平滚动
@Composable
fun ScrollBoxes() {
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.verticalScroll(rememberScrollState()) // rememberScrollState() 获取或更改滚动状态
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
}
scrollable:滚动监听,不会真的滚动元素。
@Composable
fun ScrollableSample() {
// actual composable state
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState { delta -> // delta 单次滚动间隔的偏移量
offset += delta
delta
}
)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
默认为从子级传到父级,当子级无法滚动时,将由父级处理。
提供了 Modifier.nestedScroll()
自定义协调滚动。
val scrollState = rememberLazyListState()
val topBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState)
Modifier.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
InteractionSource
提供了多种方法来获取各种互动状态。
- collectIsPressedAsState():按下
- collectIsFocusedAsState():焦点
- collectIsDraggedAsState():拖动
- collectIsHoveredAsState():悬浮在上方
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState() // 是否按下的状态
Button(
onClick = { /* do something */ },
interactionSource = interactionSource) {
Text(if (isPressed) "Pressed!" else "Not pressed")
}
// 获取状态处理
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> {
interactions.add(interaction)
}
is DragInteraction.Start -> {
interactions.add(interaction)
}
}
}
}
动画 | Jetpack Compose | Android Developers
AnimatedVisibility(
visible = visible,
enter = fadeIn(), // 进入动画
exit = fadeOut() // 退出动画
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit( // 子项进入/退出动画
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
// Content of the notification…
}
}
}
相关API 有 animateDpAsState
、animateColorAsState
等。
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
Modifier.fillMaxSize()
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)
animationSpec :spring(弹簧)、tween、repeatable等
- 修改可组合项的大小、布局、行为和外观。
- 顺序会影响最终结果。如
clickable()
和padding()
,padding()
在后面时,内边距也可点击,反之则不可点击。
设置元素如何放置,支持链式调用
Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 8.dp)
函数 | ||
---|---|---|
fillMaxWidth() |
填充至其父的最大可用宽度 | 会使父布局也填充满最大可以用的空间。 |
fillMaxHeight() |
填充至其父的最大可用高度 | |
fillMaxSize() |
填充至其父的最大可用尺寸 | |
width() |
设置宽度 | |
height() |
设置高度 | IntrinsicSize.Min 强行调整为最小固有高度 |
widthIn(min, max) |
设置最小最大宽度 | |
heightIn(min,max) |
设置最小最大高度 | |
padding() |
设置内边距 | 没有外边距修饰符。 |
paddingFromBaseline() |
在文本基线上方添加内边距 | 到基线保持特定距离 |
offset() |
设置x,y的偏移量 | padding 和 offset 之间的区别在于,可组合项添加 offset 不会改变其测量结果。需要注意在 LTR 和 RTL 这两种不同的布局方式中,它的表现将不同。对于正偏移值,在LTR中右移,RTL中左移。 |
absoluteOffset() |
设置x,y的偏移量 | 正偏移值一律会将元素向右移。即 LTR 中的 offset() |
size(width = 10.dp, height = 10.dp) |
设置宽高尺寸。 | |
indication() |
水波纹 |
特殊场景函数 | ||
---|---|---|
matchParentSize() |
仅 Box 中可用 | 子布局与父项 Box 尺寸相同,并且不影响 Box 的尺寸。和 fillMaxSize 的不同在于,它不会影响到父布局的尺寸。 |
weight |
Row 和 Column | 权重 |
- Modifier.drawWithContent:选择绘制顺序
- Modifier.drawBehind:在可组合项后面绘制
- Modifier.drawWithCache:绘制和缓存绘制对象。只要绘制区域的大小不变,或者读取的任何状态对象都未发生变化,对象就会被缓存
var pointerOffset by remember {
mutableStateOf(Offset(0f, 0f))
}
Column(
modifier = Modifier
.fillMaxSize()
.pointerInput("dragging") {
detectDragGestures { change, dragAmount ->
pointerOffset += dragAmount
}
}
.onSizeChanged {
pointerOffset = Offset(it.width / 2f, it.height / 2f)
}
.drawWithContent {
drawContent()
// draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
drawRect(
Brush.radialGradient(
listOf(Color.Transparent, Color.Black),
center = pointerOffset,
radius = 100.dp.toPx(),
)
)
}
) {
// Your composables here
}
Modifier.graphicsLayer:提供 缩放、平移、旋转、裁剪等变换功能
// 缩放 scaleX
Image(
painter = painterResource(id = R.drawable.sunset),
contentDescription = "Sunset",
modifier = Modifier
.graphicsLayer {
this.scaleX = 1.2f
this.scaleY = 0.8f
}
)
// 平移 translationX
Image(
painter = painterResource(id = R.drawable.sunset),
contentDescription = "Sunset",
modifier = Modifier
.graphicsLayer {
this.translationX = 100.dp.toPx()
this.translationY = 10.dp.toPx()
}
)
// 旋转 rotationX
Image(
painter = painterResource(id = R.drawable.sunset),
contentDescription = "Sunset",
modifier = Modifier
.graphicsLayer {
// TransformOrigin 指定旋转的原点。默认为 (0.5f,0.5f)
this.transformOrigin = TransformOrigin(0f, 0f)
this.rotationX = 90f
this.rotationY = 275f
this.rotationZ = 180f
}
)
// 裁剪clip:graphicsLayer的裁剪功能会绘制到边界之外。
Box(
modifier = Modifier
.clip(RectangleShape) // 保证 graphicsLayer 不会绘制到 边界之外
.size(200.dp)
.border(2.dp, Color.Black)
.graphicsLayer {
clip = true
shape = CircleShape
translationY = 50.dp.toPx()
}
.background(Color(0xFFF06292))
) {
Text(
"Hello Compose",
style = TextStyle(color = Color.Black, fontSize = 46.sp),
modifier = Modifier.align(Alignment.Center)
)
}
// 透明度 alpha
Image(
painter = painterResource(id = R.drawable.sunset),
contentDescription = "clock",
modifier = Modifier
.graphicsLayer {
this.alpha = 0.5f
}
)
// 设置合成策略,
Image(
painter = painterResource(id = R.drawable.sunset),
contentDescription = "clock",
modifier = Modifier.graphicsLayer {
// 使用屏幕外缓冲区绘制,不设置时涉及 alpha的BlendMode 不设置时将无法正常工作。
// 如 BlendMode.Clear:合成时会将所有像素清楚,导致 Image 显示黑色或者透明显示其他图层内容。
compositingStrategy = CompositingStrategy.Offscreen
// CompositingStrategy.Auto 与 CompositingStrategy.Offscreen策略 完成的所有绘制都会被裁剪至绘制区域 Canvas 的大小。内部绘制的内容超过部分将不显示
}
)
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = stringResource(id = R.string.dog_content_description),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(200.dp)
.clip(CircleShape) // 圆形
// .clip(RoundedCornerShape(16.dp)) // 圆角
)
// 自定义 Shape
class SquashedOval : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val path = Path().apply {
// We create an Oval that starts at ¼ of the width, and ends at ¾ of the width of the container.
addOval(
Rect(
left = size.width / 4f,
top = 0f,
right = size.width * 3 / 4f,
bottom = size.height
)
)
}
return Outline.Generic(path = path)
}
}
Compose 提供了 来和 原先的 View 体系进行结合
ComposeView 源码,它实际就是一个 ViewGroup, 提供了一个 setContent()
函数切换到Compose环境 添加 Composeable.
class ComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
@Suppress("RedundantVisibilityModifier")
protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@Composable
override fun Content() {
content.value?.invoke()
}
override fun getAccessibilityClassName(): CharSequence {
return javaClass.name
}
/**
* Set the Jetpack Compose UI content for this view.
* Initial composition will occur when the view becomes attached to a window or when
* [createComposition] is called, whichever comes first.
*/
fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}
}
abstract class AbstractComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {}
val composeView = ComposeView(requireContext()).apply {
// 设置重组策略,和 fragment.view 关联
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
// 这里添加 Composeable
setContent { // 这里已经是Compose环境了
MyApp()
}
}
class MainActivity : AbsActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ComponentActivity的扩展函数,是对ComposeView 对封装
setContent {
MyApp()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MyApp() {
....
}
class MainFragment : AbsFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
lifecycle
viewLifecycleOwner.lifecycle
return ComposeView(requireContext()).apply {
// 设置重组策略,和 fragment.view 关联
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MyApp()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MyApp() {
....
}
Flutter:强调的是所有平台上业务和UI的一致。最终都是在Flutter的Skia引擎上处理的,而不是对应平台的操作系统。可移植性好,性能略差。
Kotlin Multiplatform + Compose Multiplatform:Compose实现多端的UI, KMP 则是会编译成指定平台的二进制文件,调用的是原生API。侧重于复用,可移植性差些,性能好。
Compose | Flutter | |
---|---|---|
树形结构界面 | 树形结构界面 | 一般尽量仅更新修改的部分 |
@Composable | Widget | 都是元素的配置,用于描述应用的界面。而并非是真正的控件。且两者提供的常用组件的命名也十分类似 |
CompositionLocal | Provider | 一种数据共享的方式,同时限制了作用域。数据可以在界面树中传递 |
Jetpack Compose | Android 开发者 | Android Developers (google.cn)
Jetpack Compose 界面应用开发工具包 - Android 开发者 | Android Developers (google.cn)