通过前面内置组件和修饰符Modifier
的使用,结合Stat
状态,相信对于一般的开发需求已经没有问题了,接下来对CompositionLocal
进行学习,以及对列表组件LazyColumn
&LazyRow
和约束布局的完善ConstraintLayout
一、CompositionLocal
CompositionLocal
可以创建以树为作用域的具名对象,简单来说就是可组合函数的作用域内,其所有的内容组件都可以隐式的拿到和修改CompositionLocal
中的内容,针对组件的颜色、样式等属性值,他们往往按照一套风格来设计,使用隐式调用更加合适
1.MaterialTheme主题
之前我们在使用一些Shape
、Color
、TextStyle
时,用到了MaterialTheme
的shapes
、colors
、typography
获取,这些都是CompositionLocal
对象
创建项目时,也会自动帮助我们创建一个主题:
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun MyComposeApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
官方也推荐使用md风格,也就是使用预设的几种颜色,尺寸等对组件进行样式的选择,并且整体APP遵循md风格进行设计
在项目中,直接使用定义的Theme
主题包含compose
组件,即可获取md风格的样式,以及深色与浅色主题的切换:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyComposeApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MyScaffold()
}
}
}
}
}
效果:
2.CompositionLocalProvider
CompositionLocalProvider
可以临时改变CompositionLocal
的属性值,如果你想要对某些组件的样式进行特殊处理,推荐使用CompositionLocalProvider
,此改变只争对该作用域内的组件:
@Preview
@Composable
fun MyCompositionLocalProvider() {
MyComposeApplicationTheme { // MaterialTheme sets ContentAlpha.high as default
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)) {
Text("Medium value provided for LocalContentAlpha")
Text("This Text also uses the medium value")
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)) {
DescendantExample()
}
}
}
}
}
@Composable
fun DescendantExample() {
// CompositionLocalProviders also work across composable functions
Text("This Text uses the disabled alpha now")
}
效果:
3.自定义CompositionLocal
我们也可以为组件自定义CompositionLocal
,方式有两种:
3.1 compositionLocalOf
在重组期间只有读取current
的组件才会发生重组:
// 定义数据类
data class TextColor(var color: Color)
// 定义CompositionLocal
val LocalColors = compositionLocalOf { TextColor(Color.Black) }
object Colors {
val Blue: TextColor
get() = TextColor(color = Color.Blue)
val Red: TextColor
get() = TextColor(color = Color.Red)
}
@Composable
fun MyTextColor(
text: String,
color: Color = LocalColors.current.color
) {
Text(text, color = color)
}
@Preview
@Composable
fun MyCompositionLocalProvider2() {
Row {
MyTextColor("hi")
Spacer(modifier = Modifier.width(10.dp))
CompositionLocalProvider(LocalColors provides Colors.Red) {
MyTextColor("hi")
}
}
}
预览效果:
3.2 staticCompositionLocalOf
staticCompositionLocalOf
为更改值会导致提供 CompositionLocal
的整个 content lambda
被重组,如果提供的值发生更改的可能性微乎其微或永远不会更改,使用 staticCompositionLocalOf
可提高性能
二、列表LazyColumn&LazyRow
LazyColumn
和LazyRow
相当于RecyclerView
,内部组件并不会全部一次性加载,而是利用缓存机制,适用于加载大量的数据
1.LazyRow
LazyRow
支持横向滑动:
@Composable
fun LazyRow(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),// 可以获取当前第一个显示的元素索引的状态
contentPadding: PaddingValues = PaddingValues(0.dp),// 为内容组件设置的padding
reverseLayout: Boolean = false,
horizontalArrangement: Arrangement.Horizontal =
if (!reverseLayout) Arrangement.Start else Arrangement.End,//可以通过Arrangement.spacedBy(xx.dp)设置元素的间距
verticalAlignment: Alignment.Vertical = Alignment.Top,// 垂直对齐方式
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
)
例子:
@Preview
@Composable
fun MyLazyRow() {
LazyRow {
items(100) { index ->
Text(
text = "hi${index}",
modifier = Modifier
.width(50.dp)
.padding(top = 10.dp, bottom = 10.dp),
textAlign = TextAlign.Center
)
}
}
}
效果:
2.LazyColumn
LazyColumn
即纵向滑动列表,我们可以配合使用stickyHeader
达到粘性标题:
例子:
@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
fun MyLazyColumn() {
val lazyListState = rememberLazyListState()
LazyColumn(
state = lazyListState,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
stickyHeader {
Text(
text = "index:${lazyListState.firstVisibleItemIndex}",
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(10.dp),
color = MaterialTheme.colorScheme.onSurface
)
}
items(100) { index ->
Text(
text = "hi${index}",
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(top = 10.dp, bottom = 10.dp)
.animateItemPlacement(),// 显示时的动画效果
textAlign = TextAlign.Center
)
}
}
}
效果:
除了LazyRow
和LazyColumn
外,此外还有LazyVerticalGrid 和 LazyHorizontalGrid 可组合项为在网格中显示列表项提供支持,用法上是大致相同的
三、约束布局ConstraintLayout
ConstraintLayout
面对一些复杂布局中,对对齐要求较高时,使用ConstraintLayout
时一个很好的选择,它能够做到不需要嵌套各种Row
、Box
等布局,只用一个约束布局实现内部组件的对齐,可以通过官网介绍进行学习使用:ConstraintLayout
ConstraintLayout
需要导入依赖,版本可以通过官网查看: ConstraintLayout 版本页面
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
1.创建引用,使用约束
ConstraintLayout
作用域内,需要通过createRefs()
或 createRefFor()
为内容组件创建引用,通过约束条件,如linkTo()
对引用的组件进行对齐,约束条件由constrainAs()
修饰符提供
例子:
@Preview
@Composable
fun MyConstraintLayout1() {
ConstraintLayout {
// 创建两个引用
val (txt, btn) = createRefs()
// 对button、text两个组件分别设置引用
Button(
onClick = { /*TODO*/ },
// 为button进行约束
modifier = Modifier.constrainAs(btn) {
// 以父组件顶部,进行一个16dp的margin
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("hi")
}
Text(
"小弟",
// 为text进行约束
modifier = Modifier.constrainAs(txt) {
// 位于btn的下面,并且有一个16dp的margin
top.linkTo(btn.bottom, margin = 16.dp)
})
}
}
预览效果:
2.解构
如果你不想在ConstraintLayout
作用域内定义引用以及约束规则,那么可以将 ConstraintSet
作为参数传递给 ConstraintLayout
,外部通过createRefFor("key")
指定一个字符串key
创建引用,constrain("key")
进行约束条件;内容组件使用修饰符layoutId("key")
进行约束匹配
例子:
@Preview
@Composable
fun MyConstraintPreview2() {
BoxWithConstraints {
val constraints = if (minWidth < 600.dp) {
decoupledConstraints(margin = 16.dp) // Portrait constraints
} else {
decoupledConstraints(margin = 32.dp) // Landscape constraints
}
ConstraintLayout(constraintSet = constraints) {
Button(
onClick = { /* Do something */ },
// 找到引用
modifier = Modifier.layoutId("button")
) {
Text("hi")
}
Text("老弟", Modifier.layoutId("text"))
}
}
}
private fun decoupledConstraints(margin: Dp): ConstraintSet {
return ConstraintSet {
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button) {
top.linkTo(parent.top, margin = margin)
}
constrain(text) {
top.linkTo(button.bottom, margin)
}
}
}
预览效果:
3.Guideline
Guideline
:可以创建一个指示线,指示线可以相较于父组件的top
、bottom
、start
、end
以一个百分比
或dp
进行偏移,以便别的组件可以针对指示线进行约束,Guideline
创建方式有以下4种:
// 较于父组件左边10%位置创建
val startGuideline = createGuidelineFromStart(0.1f)
// 较于父组件右边10%位置创建
val endGuideline = createGuidelineFromEnd(0.1f)
// 较于父组件顶部16dp位置创建
val topGuideline = createGuidelineFromTop(16.dp)
// 较于父组件底部16dp位置创建
val bottomGuideline = createGuidelineFromBottom(16.dp)
例子:
@Preview
@Composable
fun MyGuidelinePreview() {
ConstraintLayout(modifier = Modifier.fillMaxSize()) {
val txt = createRef()
val startGuideline = createGuidelineFromStart(0.5f)
Text(text = "hi", modifier = Modifier.constrainAs(txt) {
start.linkTo(startGuideline)
})
}
}
预览效果:
4.Barrier
Barrier
可以将多个内容组件引用组合成一个屏障,其他的组件就可以以屏障Barrier
来进行约束,创建Barrier
有以下4中方式:
// 以btn,txt进行组合,创建右边的barrier
val barrier = createEndBarrier(btn, txt)
// 以btn,txt进行组合,创建左边的barrier
val barrier = createStartBarrier(btn, txt)
// 以btn,txt进行组合,创建顶部的barrier
val barrier = createTopBarrier(btn, txt)
// 以btn,txt进行组合,创建底部的barrier
val barrier = createBottomBarrier(btn, txt)
例子:
@Preview
@Composable
fun MyBarrierPreview() {
ConstraintLayout {
val (btn, txt) = createRefs()
// 对button、text两个组件分别设置引用
Button(
onClick = { /*TODO*/ },
// 为button进行约束
modifier = Modifier.constrainAs(btn) {
// 以父组件顶部,进行一个16dp的margin
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("hi")
}
Text(
"小弟",
// 为text进行约束
modifier = Modifier.constrainAs(txt) {
// 位于btn的下面,并且有一个16dp的margin
top.linkTo(btn.bottom, margin = 16.dp)
start.linkTo(btn.end)
})
// 创建barrier
val barrier = createEndBarrier(btn, txt)
val txt2 = createRef()
Text(
"老弟",
modifier = Modifier.constrainAs(txt2) {
start.linkTo(barrier)
top.linkTo(btn.top)
}
)
}
}
预览效果:
5.Chain
Chain
用于将多个内容组件引用组合成以个链,并以不同的 ChainStyles
配置链内各个组件的分布,创建方式有两种:
// 创建水平的链
val chain = createHorizontalChain(txt1, txt2, txt3, chainStyle = ChainStyle.SpreadInside)
// 创建垂直的链
val chain = createVerticalChain(txt1, txt2, txt3, chainStyle = ChainStyle.SpreadInside)
ChainStyles
配置:
-
ChainStyle.Spread
:空间会在所有可组合项之间均匀分布,包括第一个可组合项之前和最后一个可组合项之后的可用空间。 -
ChainStyle.SpreadInside
:空间会在所有可组合项之间均匀分布,不包括第一个可组合项之前或最后一个可组合项之后的任何可用空间。 -
ChainStyle.Packed
:空间会分布在第一个可组合项之前和最后一个可组合项之后,各个可组合项之间没有空间,会挤在一起。
例子:
@Preview
@Composable
fun MyChainPreview() {
ConstraintLayout(Modifier.fillMaxSize()) {
val (txt1, txt2, txt3) = createRefs()
val chain = createHorizontalChain(txt1, txt2, txt3, chainStyle = ChainStyle.SpreadInside)
Text("hi1", modifier = Modifier.constrainAs(txt1) {})
Text("hi2", modifier = Modifier.constrainAs(txt2) {})
Text("hi3", modifier = Modifier.constrainAs(txt3) {})
}
}
预览效果: