Jetpack Compose初体验

74 阅读1分钟

MyApplicationTheme { Surface(color = MaterialTheme.colors.background) { Column { HelloScreen(viewModel) } } } } } @Composable fun HelloScreen(viewModel: HelloViewModel){ val name:String by viewModel.name.observeAsState("") HelloContent(name = name, onNmeChange = { viewModel.onNameChanged(it) }) }

@Composable fun HelloScreen(){ var name = remember{ mutableStateOf("")} HelloContent(name = name.value, onNmeChange = { name.value = it }) }

@Composable fun HelloContent(name: String,onNmeChange:(String)->Unit){ Column(modifier = Modifier.padding(16.dp)) { if(name.isNotEmpty()){ Text(text = "Hello,${name}", modifier = Modifier.padding(8.dp), style = MaterialTheme.typography.h5) } OutlinedTextField(value = name, onValueChange = onNmeChange,label = {Text("Name")}) } }

上面代码中 observeAsState可以观察LiveData,并返回State ,State Jetpack Compose 可以直接使用的可观察类型。前面说了observeAsState内部也是封装了remember函数。使用的时候需要引入下面的依赖

implementation "androidx.compose.runtime:runtime-livedata:1.0.0-beta01"

Jetpack Compose是通过各个组件的组合来描述一个UI界面,当应用的状态发生变化的时候,Jetpack Compose 会安排重组,重组就是重新执行可能因状态改变而发生变化的组件。重组是更新界面的唯一方式。

也就是说一个组合就可以代表一个界面,其内部的可组合项的生命周期就是:进入组合、执行0次或者多次重组、退出组合。

Compose中的重组一般是由State<T>接口触发,Compose会跟踪使用State<T>数据的可组合项,Compose在重组的时候只会更改发生变化的部分。

下面来了解一下常用的布局、列表、动画、手势等操作在Compose中的使用。

布局

Compose中的布局最常用的有三个:Column、Row和Box

  • Column 相当于纵向的LinearLayout
  • Row 相当于横向的LinearLayout
  • Box 相当于FrameLayout

@Composable fun LayoutTest(){ Column() { Row(modifier = Modifier.padding(10.dp),verticalAlignment = Alignment.CenterVertically) { Image(painter = painterResource(id = R.drawable.ic_launcher_background), contentDescription = "头像", Modifier .width(60.dp) .height(60.dp) .clip(RoundedCornerShape(50))) Column(modifier = Modifier.padding(10.dp)) { Text(text = "名字") Text(text = "2 minute ago") } } } }

上面的代码轻松实现了左边头像右边竖向排列的两个文本的效果。

compose_2.png

Modifier是修饰符,专门用来控制视图的各种属性如padding、offset、background、圆角、大小、外观、行为、互动等,属性有好多,用的时候直接查文档或者点进去看看源码

注意:modifier的顺序对结果也有影响,比如下面的代码中,padding在clickable前面,那么padding部分是不能点击的padding如果在clickable后面,那么整个Column布局都可以点击

@Composable fun ArtistCard(name: String,onClick: () -> Unit){ Column(modifier = Modifier .padding(16.dp) //设置16dp的padding .clickable(onClick = onClick) //让改控件拥有点击属性和点击水波纹效果 .fillMaxWidth() //宽度填充父控件 ) { Row(verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.size(20.dp)) //设置一个20dp的空间占位 宽高都是20的正方形 Card(elevation = 4.dp) { Image(painter = painterResource(id = R.drawable.img_bg_2), contentDescription = "pic") } } } }

如果想要Column或者Row可以滚动,直接添加下面的属性就可以了。Modifier.verticalScroll(rememberScrollState())或者horizontalScroll(rememberScrollState())

Modifier.size 可以设置一个容器的宽高 其子布局默认不能超出他的范围,如果想要子布局可以超出它的范围,子布局可以使用Modifier.requiredSize方法就可以了

Box( Modifier .size(90.dp, 150.dp) .background(Color.Green)) { Box( Modifier .requiredSize(100.dp, 100.dp) .background(Color.Red)) { } }

也可以使用weight属性按比例分布局的大小入下

@Composable fun FlexibleComposable() { Row(Modifier.fillMaxWidth()) { Box( Modifier .weight(2f) .height(50.dp) .background(Color.Blue)) Box( Modifier .weight(1f) .height(50.dp) .background(Color.Red)) } }

compose_3.png

除了前面的三个布局容器,如果想要在Compose中的使用相对布局,可以使用ConstraintLayout,在实现对其方式比较复杂的布局时比较有用。

虽然在用xml布局的时候为了更好的减少布局层级,推荐优先使用ConstraintLayout布局,不过在Compose中不用担心布局层级的问题,所以创建布局时还是首选Column、Row、Box

使用constraintlayout的时候需要单独引入

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha03"

下面的代码实现了一个text在button的下面,距离button底部16dp

@Composable fun ConstraintLayoutContent() { ConstraintLayout { // 给可组合对象创建引用 val (button, text) = createRefs() Button( onClick = { }, //将button约束到布局距离顶部16dp的位置 modifier = Modifier.constrainAs(button) { top.linkTo(parent.top, margin = 16.dp) } ) { Text("Button") } //将text约束到距离button底部16dp的地方 Text("Text", Modifier.constrainAs(text) { top.linkTo(button.bottom, margin = 16.dp) }) } }

自定义布局

自定义布局,在使用xml布局的时候,我们需要继承ViewGroup并重写onMeasure和onLayout,在Compose中,只需要使用Layout可组合项编写一个函数就可以了

比如下面自定义实现一个类似Column的效果

@Composable fun MyColumn(modifier: Modifier = Modifier,content:@Composable() ()->Unit){ Layout(content = content,modifier = modifier, measurePolicy = MeasurePolicy{ measurables, constraints -> // val placeables = measurables.map { measurable -> //测量每个子view measurable.measure(constraints.copy(minWidth = 0)) }

layout(constraints.maxWidth,300){ //跟踪子view的y坐标 var yPosition = 0 //在父view中放置子view placeables.forEach{ placeable -> placeable.place(x=0,y=yPosition) yPosition+=placeable.height } } }) } //使用的时候直接如下 MyColumn (Modifier .padding(8.dp) .background(Color.Red)){ Text("哈哈哈哈第一行") Text("哈哈哈哈第二行") Text("哈哈哈哈第三行") Text("哈哈哈哈第四行") }

compose_4.png

除了自定义容器,如果觉得修饰符Modifier不够用还可以自定Modifier,其实就是给Modifier添加了一个扩展函数,比如下面的给text添加一个从文字的基线到顶部的距离

fun Modifier.firstBaselineToTop(firstBaseLineToTop: Dp)=Modifier.layout{ measurable, constraints -> //测量可测量的参数 这里就是指Text val placeable = measurable.measure(constraints) //检查是否有文本基线FirstBaseline check(placeable[FirstBaseline]!= AlignmentLine.Unspecified) val firstBaseLine = placeable[FirstBaseline] //高度减去firstBaseLine val placeableY = firstBaseLineToTop.toPx().toInt() - firstBaseLine val height = placeable.height + placeableY //通过layout指定可组合项的尺寸 layout(placeable.width,height){ placeable.place(0,placeableY) } }

使用的时候直接给Text添加firstBaselineToTop属性如下,预览就可以看到使用firstBaselineToTop的效果会比直接使用padding距离顶端的更小一些

@Preview @Composable fun TextWithPaddingToBaselinePreview() { MyApplicationTheme { Text("firstBaselineToTop", Modifier.firstBaselineToTop(32.dp)) } }

@Preview @Composable fun TextWithNormalPaddingPreview() { MyApplicationTheme { Text("普通padding!", Modifier.padding(top = 32.dp)) } }

自定义View

在Compose中自定义View比之前使用xml简单了很多,比如下面的代码直接在Canvas里面划线、画圆圈、画矩形等等

@Composable fun CanvasTest() { Canvas(modifier = Modifier.fillMaxSize(), onDraw = { drawLine( start = Offset(0f, 0f), end = Offset(size.width, size.height), color = Color.Blue, strokeWidth = 5f ) rotate(degrees = 45f){ drawRect( color = Color.Green, size = size/4f, topLeft = Offset(size.width/3f,size.height/3f) ) } drawCircle( color = Color.Blue, center = Offset(size.width/2,size.height/2), radius = 50f ) //多个状态组合 旋转和平移 withTransform({ translate(left = size.width/5f) rotate(degrees = 45f) }){ drawRect( color = Color.Yellow, size = size/5f, topLeft = Offset(size.width/3f,size.height/3f) ) } }) }

compose_5.png

列表

前面尝试了给Column添加一个verticalScroll()就可以让Column滚动了。不过这时候它只是相当于我们用xml布局时候的ScrollView,每次会加载所有的内容。如果数据量太大会影响性能。

如果我们想实现xml布局的时候的Recyclerview的各种缓存功能,Compose提供了LazyColumn和LazyRow。例如

@Composable fun MessageList(messages:List){ LazyColumn{ items(messages){ message -> Text(text = message) } } }

LazyColumn 内部可以通过item()来加载单个列表项,通过items()来加载多个列表项。还有一个itemsIndexed可以实现带索引的列表项。

@Composable fun MessageList(messages:List){ LazyColumn{ itemsIndexed(messages){index,message -> Text(text = "message===message===index") } } }

如果想要给列表添加一个粘性的头部可以使用stickyHeader很方便的实现,不过这个目前是实验性的api,以后有可能会改或者去掉。

@Composable fun MessageList(messages:List){ val listState = rememberLazyListState() LazyColumn(contentPadding = PaddingValues(horizontal = 16.dp,vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp),state = listState){ println("滚动:listState.firstVisibleItemIndex=={listState.firstVisibleItemIndex}=={listState.firstVisibleItemScrollOffset}") stickyHeader(1) { Text(text = "我是头部",Modifier.fillMaxWidth().height(60.dp).background(Color.Green)) } itemsIndexed(messages){index,message -> Text(text = "message===message===index", Modifier.background(Color.Yellow).height(60.dp)) Spacer(modifier = Modifier .fillMaxWidth() .height(5.dp) .background(Color.Gray)) } } }

compose_6.png

上面代码中的listState可以监听列表滚动时候的状态,比如第一个可见的位置。listState还提供了控制列表滚动的方法,比如scrollToItem、animateScrollToItem等。

网格列表可以通过LazyVerticalGrid来实现,这个api目前也是实验性的以后可能会变

@ExperimentalFoundationApi @Composable fun GridList(messages:List){ LazyVerticalGrid(cells = GridCells.Adaptive(minSize = 128.dp), content = { items(messages){ message -> Text(text = message,Modifier .background(Color.Yellow) .height(60.dp)) } }) }

动画

AnimatedVisibility

这个api可以很方便的组合多种动画,目前这个api目前是实验性的,未来有可能改变或者删除

Row{ AnimatedVisibility( visible = visible, enter = slideInVertically(initialOffsetY = {-40}) + expandVertically(expandFrom = Alignment.Top) + fadeIn(initialAlpha = 0.3f), exit = slideOutVertically()+ shrinkVertically()+ fadeOut()) { Text(text = "text",fontSize =30.sp) } Spacer(modifier = Modifier.size(20.dp)) Button(onClick = { visible = !visible }) { Text(text = "点击") }

}

上面的代码点击按钮,可以控制一个text的从下往上划出页面,从上往下进入界面同时带有淡入淡出效果。多种效果可以直接用+号连接起来就行。

copmose_7.gif

animateContentSize

如果要给一个控件改变大小的时候添加动画,就使用animateContentSize,非常方便。

var message by remember { mutableStateOf("Hello") } Row { Box( modifier = Modifier .background(Color.Blue) .animateContentSize() ) { Text(text = message) } Button(onClick = { message += message }) { Text(text = "点击") } }

上面的代码点击的时候,增加Text的文本,可以看到Text控件大小改变的时候会有过渡效果。

copmose_8.gif

Crossfade

直接使用Crossfade包裹控件就能很方便的实现切换的时候淡入淡出的效果

var currentPage by remember { mutableStateOf("A") } Row { Crossfade(targetState = currentPage) { screen -> when(screen){ "A" -> Text(text = "A",Modifier.background(Color.Green),fontSize = 30.sp) "B" -> Text(text = "B",Modifier.background(Color.Blue),fontSize = 30.sp) } } Spacer(modifier = Modifier.size(20.dp)) Button(onClick = { if(currentPage=="A") currentPage="B" else currentPage="A" }) { Text(text = "点击") } }

上面的代码,点击按钮,Text切换A、B的时候会有淡入淡出的效果

copmose_9.gif

animate*AsState

*号代表多种数据类型,比如animateFloatAsState、animateDpAsState、animateSizeAsState、animateOffsetAsState、animateIntAsState等

var enabled by remember{mutableStateOf(true)} val alpha = animateFloatAsState(targetValue = if (enabled) 1f else 0.5f) Row { Box ( Modifier .width(50.dp) .height(50.dp) .graphicsLayer(alpha = alpha.value) .background(Color.Red)) Spacer(modifier = Modifier.size(20.dp)) Button(onClick = { enabled = !enabled }) { Text(text = "点击") } }

上面的代码,点击按钮的时候,让控件的背景的透明度从1到0.5过渡

copmose_10.gif

Animatable

Animatable是一个容器,可以通过animateTo方法给动画添加效果,Animatable的很多功能是以挂起函数的形式提供的,所以一般运行在一个协程的作用域内,可以使用LaunchedEffect创建一个协程的作用域

var ok by remember{mutableStateOf(true)} val color = remember{ Animatable(Color.Gray)} LaunchedEffect(ok){ color.animateTo(if (ok) Color.Green else Color.Red) } Row { Box ( Modifier .width(50.dp) .height(50.dp) .background(color.value)) Spacer(modifier = Modifier.size(20.dp)) Button(onClick = { ok = !ok }) { Text(text = "点击") } }

上面的代码,点击按钮控件的背景从绿色过渡到红色

copmose_11.gif

updateTransition

updateTransition是一个方法,返回一个Transition对象,Transition可以管理多个动画,并同时运行这些动画。

var currentState by remember{mutableStateOf(BoxState.Collapsed)} val transition = updateTransition(targetState = currentState) val size by transition.animateDp { state -> when (state) { BoxState.Collapsed -> 10.dp BoxState.Expanded -> 100.dp } } val coloranimate by transition.animateColor( transitionSpec = { when { BoxState.Expanded isTransitioningTo BoxState.Collapsed -> //spring 可以创建基于物理特性的动画比如先快后慢、回弹、匀速等 spring(stiffness = 50f) else -> tween(durationMillis = 500) } } ) { state -> when (state) { BoxState.Collapsed -> Color.Blue BoxState.Expanded -> Color.Yellow } } Row { Box( Modifier .size(size) .background(coloranimate)){ } Button(onClick = { currentState = if(currentState == BoxState.Collapsed) BoxState.Expanded else BoxState.Collapsed }) { Text(text = "点击") } }

上面的代码transition管理着两个动画,一个是大小从10变到100,一个是颜色从蓝色变到黄色。点击按钮的时候两个动画一块执行

copmose_12.gif

InfiniteTransition

InfiniteTransition 也可以保存多个动画,跟前面不同的是 它的这些动画是布局的时候就立即运行

val infiniteTransition = rememberInfiniteTransition() val colorTran by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) Row { Box( Modifier .width(60.dp) .height(60.dp) .background(colorTran)) }

上面的代码,页面加载完成之后,控件的背景就会在红色和绿色之间不停的切换。

copmose_13.gif

手势

点击操作

直接使用Modifier的clickable就可以

@Composable fun ClickableSample() { val count = remember { mutableStateOf(0) } Text( text = count.value.toString(), modifier = Modifier .width(30.dp) .height(30.dp) .background(Color.Gray) .wrapContentSize(Alignment.Center) .clickable { count.value += 1 }, textAlign = TextAlign.Center ) }

如果想要更精细的点击可以使用 pointerInput 方法里面按下、长按、双击、单击都有

@Composable fun PointerInputSample() { val count = remember { mutableStateOf(0) } Text( text = count.value.toString(), modifier = Modifier .width(30.dp) .height(30.dp) .background(Color.Gray) .wrapContentSize(Alignment.Center) .pointerInput (Unit){ detectTapGestures ( onPress = {/按下操作/}, onLongPress = {/长按操作/}, onDoubleTap = {/双击/}, onTap = {/单击/} ) }, textAlign = TextAlign.Center ) }

滚动操作

只需给一个页面元素添加verticalScroll或者horizontalScroll就可以实现竖向和横向的滚动了,类似我们之前使用xml布局时的ScrollView

@Composable fun ScrollBoxes() { Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .verticalScroll(rememberScrollState()) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }

如果想要滚动到指定的位置,比如下面的代码点击按钮滚动到200的位置,可以使用rememberScrollState的scrollTo方法来执行滚动操作。

copmose_14.gif

该操作需要运行在一个协程的作用域中,使用rememberCoroutineScope方法可以获得一个协程的作用域

@Composable private fun ScrollBoxesSmooth() { val scrollState = rememberScrollState() val scope = rememberCoroutineScope() Column { Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .padding(horizontal = 8.dp) .verticalScroll(scrollState) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } Button(onClick = { scope.launch { scrollState.scrollTo(200) } }) { Text(text = "点击") } } }

如果想要记录手指在屏幕上滑动的位置,可以使用scrollable修饰符来记录。比如下面的代码中scrollable中的state就可以监听手指滚动的距离了。

@Composable fun ScrollableDemo(){ var offset by remember{ mutableStateOf(0f) } Box( Modifier .size(100.dp) .scrollable( orientation = Orientation.Vertical, state = rememberScrollableState { delta -> offset += delta delta } ) .background(Color.Gray), contentAlignment = Alignment.Center ) { Text(text = offset.toString()) } }

copmose_15.gif

嵌套滚动 简单的嵌套滚动很简单,哪个控件需要滚动就加上相应的verticalScroll或者horizontalScroll即可。

compose会自动处理滑动冲滚动突 子控件先滚动,滚动到边界之后 父控件开始。

下面的例子就是类表里面的每个item还是个列表,滑动的时候就可以看到内部先滑动,滑动到边界后外部列表在滑动。

@Composable fun nestScrollDemo1(){ Column( modifier = Modifier .background(Color.LightGray) .width(100.dp) .height(200.dp) .verticalScroll(rememberScrollState()) ) { repeat(10) { Column( Modifier .border(6.dp, Color.Blue) .background(Color.Green) .padding(15.dp) .height(150.dp) .verticalScroll(rememberScrollState())) { repeat(20){ Text("Item $it", modifier = Modifier.padding(2.dp)) } } } } }

copmose_16.gif

拖动操作可以使用draggable修饰符,它可以实现单一方向上的拖动比如横向的或者纵向的拖动。

比如下面的例子在draggable中设置拖拽的方向,监听到拖拽的距离之后设置给自身的offset方法就实现拖拽滑动了。

@Composable fun draggableDemo(){ var offsetX by remember{ mutableStateOf(0f) } Text(text = "横着拖拽我", modifier = Modifier .background(Color.Green) .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState(onDelta = { offsetX += it }) )) }

copmose_17.gif

如果想要多个方向上拖动,可以使用pointerInput修饰符,比如下面的例子记录X方向和Y方向上的偏移量,然后设置给自身就可以实现自由拖动了。

@Composable fun draggableDemo1(){ Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consumeAllChanges() offsetX += dragAmount.x offsetY += dragAmount.y } } ) } }

copmose_18.gif

swipeable 可以在拖动控件松手之后 控件会朝着自定义的方向自动滑动,比如常见的滑动开关的效果