上次介绍了compose中大多数的标准组件,此外还有两个重要的组件:列表LazyColumn
和LazyRow
,以及约束布局ConstraintLayout
,在使用它们之前,先来认识Modifier
修饰符Modifier
Modifier
之前已经运用过,它能做的事情很多,不仅仅是改变组件的样式,还能够改变组件的位置,以及自定义交互事件,关于Modifier
的所有用法,可以查看官方文档:https://developer.android.google.cn/jetpack/compose/modifiers-list,这边只介绍常用的
一、Modifier顺序
首先我们必须要知道的是:Modifier
的设置是有顺序的,下面的代码分别在设置padding
之前和之后为Box
设置点击事件:
@Preview
@Composable
fun MyModifier1() {
Row(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.size(100.dp)
.background(
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.medium
)
.clickable {
}
.padding(40.dp)
)
Box(
modifier = Modifier
.size(100.dp)
.background(
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.medium
)
.padding(20.dp)
.clickable {
}
)
}
}
效果如下,左边为padding
之前,padding
之后,可以看到之后再设置点击事件,整个组件的点击范围变小了:
二、操作
对组件的操作有很多,如点击、长按、双击、拖拽、选中等
1.clickable-点击
clickable
之前就使用过了,除了点击外,还有一些其他属性和提供无障碍操作(残疾人)使用:
fun Modifier.clickable(
interactionSource: MutableInteractionSource,// 只有第一次按下才会发送,并更新状态
indication: Indication?,// 按下效果 如水波纹
enabled: Boolean = true,
onClickLabel: String? = null,//无障碍访问标签
role: Role? = null,//为无障碍访问描述元素的类型
onClick: () -> Unit
)
2.combinedClickable-点击、长按、双击
combinedClickable
组合了点击、长按、双击:
@ExperimentalFoundationApi
fun Modifier.combinedClickable(
interactionSource: MutableInteractionSource,
indication: Indication?,
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit
)
例子:
代码语言:javascript复制@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
fun MyCombineClick() {
val snackbarState by remember { mutableStateOf(SnackbarHostState()) }
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "点我",
color = Color.White,
modifier = Modifier
.combinedClickable(
onClick = {
scope.launch {
snackbarState.showSnackbar(
"onClick",
duration = SnackbarDuration.Short
)
}
},
onDoubleClick = {
scope.launch {
snackbarState.showSnackbar(
"onDoubleClick",
duration = SnackbarDuration.Short
)
}
},
onLongClick = {
scope.launch {
snackbarState.showSnackbar(
"onLongClick",
duration = SnackbarDuration.Short
)
}
}
)
.background(MaterialTheme.colorScheme.secondary, MaterialTheme.shapes.small)
.padding(10.dp)
)
SnackbarHost(hostState = snackbarState, modifier = Modifier.align(Alignment.BottomCenter))
}
}
效果:
3.draggable-拖拽
draggable
让组件可以响应拖动:
fun Modifier.draggable(
state: DraggableState,
orientation: Orientation,// 水平还是竖直方向
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,//是否立即拖动,防止其他手势检测器对“向下”事件做出反应
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},//拖动开始
onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},//拖动结束
reverseDirection: Boolean = false//是否反转方向
)
例子:
代码语言:javascript复制@Preview
@Composable
fun MyDraggable() {
var offset by remember { mutableStateOf(0f) }
val state = rememberDraggableState(onDelta = { delta ->
offset = delta
})
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "拽我",
modifier = Modifier
.offset { IntOffset(offset.roundToInt(), 0) }//偏移组件
.draggable(
state = state,
orientation = Orientation.Horizontal,// 水平拖拽
)
.background(Color.Cyan, RoundedCornerShape(5.dp))
.padding(10.dp)
)
}
}
效果:
4.swipeable-滑动
swipeable
类似于Switch
的效果,可以定义多个锚点,以及判定切换锚点滑动的阈值:
@ExperimentalMaterialApi
fun <T> Modifier.swipeable(
state: SwipeableState<T>,
anchors: Map<Float, T>,// 锚点集合
orientation: Orientation,// 可滑动的方向
enabled: Boolean = true,
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null,
thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },// 滑动阈值,超于多少就滑动到下个锚点,反之则滑回来
resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
// 滑动手指放开时,距离阈值没达到,但加速度达到阈值,则切换到下个锚点
velocityThreshold: Dp = VelocityThreshold
)
例子:
代码语言:javascript复制@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun MySwipeable() {
// 表示运行到哪个锚点的状态
val swipeableState = rememberSwipeableState(initialValue = 0)
val width = 100.dp
Box(
modifier = Modifier
.width(200.dp)
.border(
1.dp,
Color.Red,
RoundedCornerShape(5.dp)
)
.swipeable(
state = swipeableState,
anchors = mapOf(//锚点集合,表示每个锚点滑动的距离
0f to 0,
with(LocalDensity.current) {//dp->px
width.toPx()
} to 1),
orientation = Orientation.Horizontal//水平滑动
),
contentAlignment = Alignment.TopStart
) {
Button(
onClick = { /*TODO*/ },
modifier = Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }// 根据滑动状态进行偏移操作
.width(width) // 按钮宽度也设置为100dp
) {
Text(text = "滑我", color = Color.White)
}
}
}
效果:
三、对齐方式
除了前面介绍布局时每个布局自带属性的对齐方式外,Modifier
也为各个不同的布局作用域(BoxScope
、RowScope
、ColumnScope
)设置了单独的布局方式,在这些作用域中,我们可以使用下面的对齐方式
1.BoxScope
align
:将内容元素拉取到 Box
中的特定 Alignment
例子:
代码语言:javascript复制@Preview
@Composable
fun MyBoxScope() {
Box(modifier = Modifier.fillMaxSize()) {
Button(
onClick = { /*TODO*/ },
modifier = Modifier.align(Alignment.Center)//将组件位于Box的中央
) {
Text(text = "hi")
}
}
}
效果:
2.RowScope
2.1 align
align
:设置元素在Row
中的垂直对齐方式:Top
顶部、CenterHorizontally
垂直居中、End
底部
例子:
代码语言:javascript复制@Preview
@Composable
fun MyRowScope() {
Row(modifier = Modifier.fillMaxSize()) {
Text(
text = "align-CenterVertically",
modifier = Modifier
.align(Alignment.CenterVertically)
.background(Color.LightGray, RoundedCornerShape(3.dp))
.padding(10.dp)
)
}
}
效果:
2.2 alignBy
alignBy
:使其 alignmentLine
与同样配置为 alignBy
的同级元素对齐。
例子:
代码语言:javascript复制@Preview
@Composable
fun MyRowScope2() {
Row(modifier = Modifier.fillMaxSize()) {
Text(
text = "align-line1",
modifier = Modifier
.alignBy(FirstBaseline)
.background(Color.LightGray, RoundedCornerShape(3.dp))
.padding(10.dp)
)
Text(
text = "align-line2",
fontSize = 20.sp,
modifier = Modifier
.alignBy(FirstBaseline)
.background(Color.LightGray, RoundedCornerShape(3.dp))
.padding(10.dp)
)
}
}
效果:
ColumnScope
也是差不多的使用方式,这边就不做多介绍了
四、动画
1.animateItemPlacement
animateItemPlacement
是作用于列表组件下的作用域,可为列表Item元素添加动画效果
2.animateEnterExit
animateEnterExit
:在AnimatedVisibilityScope
动画可见作用域中自定义进入和出去的动画效果
例子:
代码语言:javascript复制@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun MyAnimeScope() {
var visible by remember { mutableStateOf(false) }
Row(modifier = Modifier.fillMaxSize()) {
Button(onClick = { visible = !visible }) {
Text(text = "点我")
}
// 带动画的效果组件
AnimatedVisibility(visible = visible) {
Icon(
Icons.Default.Info, contentDescription = null,
// 自己指定进入和出去的动画
modifier = Modifier.animateEnterExit(enter = scaleIn(), exit = scaleOut())
)
}
}
}
效果:
五、边框
border
:可以为组件加上一个边框,需要指定Shape
背景形状,还支持Brush
(Shader(点击跳转详情))
例子:
代码语言:javascript复制@Preview
@Composable
fun MyBorder() {
Box(
modifier = Modifier
.size(100.dp)
.padding(start = 10.dp, top = 10.dp)
.border(
1.dp,// 边框粗细1dp
Brush.linearGradient(
0f to Color.Cyan.copy(alpha = 0.5f),
1f to Color.Magenta
),// 线性渲染
RoundedCornerShape(10.dp)// 形状带圆角
)
)
}
预览效果:
六、绘图
1.alpha
alpha
直接改变该组件的透明度:
@Preview
@Composable
fun MyAlpha() {
Row {
Box(
modifier = Modifier
.background(Color.Red)
.size(50.dp)
) {
}
Box(
modifier = Modifier
.alpha(0.2f)
.background(Color.Red)
.size(50.dp)
) {
}
}
}
预览效果:
2.drawBehind
drawBehind
提供一个画布,用于在绘制在内容后方:
@Preview
@Composable
fun MyDrawBehind() {
Text(
text = "hi",
modifier = Modifier.drawBehind {
// 画个圆形背景
drawCircle(color = Color.Cyan, 10f)
}
)
}
预览效果:
3.clip
clip
是将组件内容显示的画布进行裁剪,不可与background
同时使用:
@Preview
@Composable
fun MyClip() {
Box(modifier = Modifier.clip(CircleShape)) {
Box(
Modifier
.size(50.dp)
.background(color = Color.Cyan))
}
}
预览效果:
4.drawWithContent
drawWithContent
允许开发者在布局内容前后进行绘制,通过drawContent()
方法绘制内容:
@Preview
@Composable
fun MyDrawWithContent() {
Text(
text = "hi",
modifier = Modifier.drawWithContent {
// 先绘制内容
this.drawContent()
// 画个圆形背景
drawCircle(color = Color.Cyan, 10f)
}
)
}
预览效果:
5.indication
indication
为交互设置效果,如水波纹,该效果在前面clickable
等操作中也可以设置,pointerInput
在后续指针中:
@Preview
@Composable
fun MyIndication() {
val interactionSource = remember { MutableInteractionSource() }
Text(
text = "hi",
modifier = Modifier
.indication(
interactionSource = interactionSource,
indication = rememberRipple(color = Color.Red) //红色水波纹
)
// 添加手势
.pointerInput(interactionSource, true) {
this.detectTapGestures(
// 按下事件
onPress = { offset ->
val pressInteraction = PressInteraction.Press(offset)
// 触发水波纹
interactionSource.emit(pressInteraction)
}
)
}
.size(100.dp),
textAlign = TextAlign.Center
)
}
效果:
6.paint
paint
允许传入一个painter
画笔,来对整个组件进行渲染:
fun Modifier.paint(
painter: Painter,
sizeToIntrinsics: Boolean = true,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Inside,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
)
例子:
代码语言:javascript复制@Preview
@Composable
fun MyPaint() {
// 红色画笔
val painter = ColorPainter(Color.Red)
Box(modifier = Modifier.size(100.dp).paint(painter = painter)) {
Text("hi")
}
}
预览效果:
7.shadow
shadow
为组件设置一个阴影:
@Stable
fun Modifier.shadow(
elevation: Dp,// 阴影大小
shape: Shape = RectangleShape, //形状
clip: Boolean = elevation > 0.dp,
ambientColor: Color = DefaultShadowColor,
spotColor: Color = DefaultShadowColor,
)
例子:
代码语言:javascript复制@Preview
@Composable
fun MyShadow() {
Box(
modifier = Modifier
.size(100.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(50.dp)
.shadow(2.dp)
) {
}
}
}
预览效果:
七、焦点
1.onFocusChanged
onFocusChanged
可以监听组件焦点的变化,需要和focusRequester
、focusable
配合使用:
@Preview
@Composable
fun MyFocus() {
var focused by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.focusRequester(focusRequester)//绑定焦点请求者
.onFocusChanged { focusStat ->
focused = focusStat.isFocused
}
.focusable()
.size(50.dp)
.background(if (focused) Color.Cyan else Color.Red)
) {
}
Button(
onClick = { focusRequester.requestFocus() },//点击触发焦点获取
modifier = Modifier.align(Alignment.BottomCenter)
) {
Text("click")
}
}
}
效果:
八、布局
1.layout
使用layout
摆放组件,和传统的自定义控件一样,layout
是用于摆放位置的,下面小程序实现功能为基于baseLine
进行一个偏移,最后通过重新
fun Modifier.baseLineToTop(
dp: Dp
) = this.then(//当前Modifier进行组合
layout { measurable, constraints ->
// 预先测量下组件
val placeable = measurable.measure(constraints)
// 获取baseline,为baseline到组件顶部的距离
val baseLine = placeable[FirstBaseline]
// 偏移后,组件的高度 = 原有高度 偏移量-baseline
val height = placeable.height dp.roundToPx() - baseLine
// 重新定义组件的整体宽高
layout(placeable.width, height) {
//重新摆放组件,y轴进行偏移
placeable.placeRelative(0, dp.roundToPx() - baseLine)
}
}
)
@Preview
@Composable
fun MyCustomLayoutModifier() {
Row {
Text(
"hi",
modifier = Modifier.baseLineToTop(24.dp)
)
Spacer(modifier = Modifier.width(20.dp))
Text(
"hi"
)
}
}
预览效果:
九、内边距
1.absolutePadding
absolutePadding
和padding
的区别在于,absolutePadding
总认为是从左往右,从上往下摆放控件的,CompositionLocal
会在后续进行介绍:
@Preview
@Composable
fun MyPadding() {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {// 从右往左摆放组件
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Box(
modifier = Modifier
.size(100.dp)
.padding(10.dp, 10.dp, 20.dp, 10.dp)
.background(Color.Red)
) {
Text("hi")
}
Divider(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
)
Box(
modifier = Modifier
.size(100.dp)
.absolutePadding(10.dp, 10.dp, 20.dp, 10.dp)
.background(Color.Blue)
) {
Text("hi")
}
}
}
}
预览效果,由于进行了反转,蓝色才是使用了absolutePadding
:
十、指针
除了操作中介绍的几种改变组件交互效果外,还可以使用pointerInput
来自定义更自由的操作,它包含了一切触摸事件的监听,并且后续的效果由你自己定义
1.pointerInput
pointerInput
就是处理触摸事件的一个修饰,官方推荐传入一个key
,来确定何时取消上次的处理
@Preview
@Composable
fun MyPointer() {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(modifier = Modifier.fillMaxSize()) {
Text(
text = "hi",
color = Color.White,
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(
MaterialTheme.colorScheme.primary,
MaterialTheme.shapes.medium
)
.padding(10.dp)
.pointerInput(Unit) {
detectTransformGestures { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
pan.apply {
offsetX = x
offsetY = y
}
}
},
style = MaterialTheme.typography.labelMedium
)
}
}
效果:
十一、变换
变换的效果包含:旋转,缩放,以及上面使用过的平移
1.rotate
rotate
传入一个角度,以旋转组件:
@Preview
@Composable
fun MyTrans() {
var rotationState by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.rotate(rotationState)
.padding(10.dp)
.size(300.dp)
.background(Color.Red)
.pointerInput(Unit) {
detectTransformGestures { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
rotationState = rotation
}
}
)
}
效果:
2.rotate
rotate
可以将组件进行缩放
@Preview
@Composable
fun MyTrans2() {
var scaleState by remember { mutableStateOf(1f) }
Box(
modifier = Modifier
.scale(scaleState)
.padding(10.dp)
.size(300.dp)
.background(Color.Red)
.pointerInput(Unit) {
detectTransformGestures { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
scaleState *= zoom
}
}
)
}
效果:
十一、图形
graphicsLayer
就是有关组件显示的一切状态,包含了所有变换效果、透明度、背景shape、阴影,配合transformable
能够对变换进行快速的处理:
fun Modifier.graphicsLayer(
scaleX: Float,
scaleY: Float,
alpha: Float,
translationX: Float,
translationY: Float,
shadowElevation: Float,
rotationX: Float,
rotationY: Float,
rotationZ: Float,
cameraDistance: Float,
transformOrigin: TransformOrigin,
shape: Shape,
clip: Boolean,
renderEffect: RenderEffect?,
ambientShadowColor: Color,
spotShadowColor: Color
)
例子:
代码语言:javascript复制@Preview
@Composable
fun MyGraphicsLayer() {
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val transformState =
rememberTransformableState { zoomChange: Float, panChange: Offset, rotationChange: Float ->
scale *= zoomChange
rotation = rotationChange
offset = panChange
}
Box(
modifier = Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
rotationZ = rotation
translationX = offset.x
translationY = offset.y
}
.padding(10.dp)
.size(300.dp)
.background(Color.Red)
.transformable(transformState)
)
}
效果:
十二、滚动
Modifier
还能为组件添加可以滚动的支持,以及内嵌滚动、根据滚动状态显示或隐藏组件的支持
1.verticalScroll
verticalScroll
可以让组件支持竖直滑动:
@Preview
@Composable
fun MyScrollable() {
val state = rememberScrollState()
Column(
modifier = Modifier
.padding(10.dp)
.size(300.dp)
.background(MaterialTheme.colorScheme.primary)
.verticalScroll(state)
) {
repeat(10) { index ->
Text(text = "hi${index}", modifier = Modifier.height(50.dp))
}
}
}
效果:
2.overscroll
overscroll
就是给组件加上滚动到边缘的效果:
@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
fun MyScrollable2() {
val state = rememberScrollState()
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.padding(10.dp)
.size(300.dp)
.verticalScroll(state)
.overscroll(ScrollableDefaults.overscrollEffect())//滑动到顶部和底部的涟漪效果
) {
repeat(50) { index ->
Text(text = "hi${index}", modifier = Modifier.height(50.dp))
}
}
ExtendedFloatingActionButton(
text = { Text(text = "点我") },
icon = { Icon(Icons.Default.Close, contentDescription = null) },
onClick = { /*TODO*/ },
expanded = state.isScrollInProgress,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
效果:
3.nestedScroll
nestedScroll
将内容组件的滑动事件进行分享,以达到联动的效果:
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScrollable3() {
val state = rememberScrollState()
val toolbarHeight = 48.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
//y方向的偏移量
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value delta
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
return Offset.Zero
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
Column(
modifier = Modifier
.padding(10.dp)
.size(300.dp)
.verticalScroll(state)
.overscroll(ScrollableDefaults.overscrollEffect())//滑动到顶部和底部的涟漪效果
) {
repeat(50) { index ->
Text(text = "hi${index}", modifier = Modifier.height(50.dp))
}
}
TopAppBar(
modifier = Modifier
.height(toolbarHeight)
.offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) },
title = {
Text(
stringResource(id = R.string.app_name)
)
},
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primary)
)
}
}
效果:
十三、其他
其他再列举一些常用的修饰
1.blur
blur
实现模糊滤镜效果,效果参考传统安卓处理方式:Android滤镜--Alpha值滤镜处理之MaskFilter
@Preview
@Composable
fun MyBlur() {
Row(horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth()) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.blur(50.dp)
)
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null
)
}
}
预览效果:
2.pullRefresh
pullRefresh
让组件支持下拉刷新,配合PullRefreshState
来显示PullRefreshIndicator
刷新指示器:
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Preview
@Composable
fun MyPullRefresh() {
val state = rememberScrollState()
var refreshing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val refreshState = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = {
scope.launch {
refreshing = true
delay(1500)
refreshing = false
}
}
)
Box(
modifier = Modifier.pullRefresh(refreshState) //下拉刷新
) {
Column(
modifier = Modifier
.padding(10.dp)
.size(300.dp)
.verticalScroll(state)
.overscroll(ScrollableDefaults.overscrollEffect())//滑动到顶部和底部的涟漪效果
) {
repeat(50) { index ->
Text(text = "hi${index}", modifier = Modifier.height(50.dp))
}
}
PullRefreshIndicator(// 刷新指示器
refreshing,
refreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
效果: