Jetpack Compose 初体验,flutter 游戏开发
}
layout(constraints.maxWidth,300){//跟踪子 view 的 y 坐标 var yPosition = 0//在父 view 中放置子 viewplaceables.forEach{ placeable ->placeable.place(x=0,y=yPosition)yPosition+=placeable.height}}})}//使用的时候直接如下 MyColumn (Modifier.padding(8.dp).background(Color.Red)){Text("哈哈哈哈第一行")Text("哈哈哈哈第二行")Text("哈哈哈哈第三行")Text("哈哈哈哈第四行")}
除了自定义容器,如果觉得修饰符 Modifier 不够用还可以自定 Modifier,其实就是给 Modifier 添加了一个扩展函数,比如下面的给 text 添加一个从文字的基线到顶部的距离
fun Modifier.firstBaselineToTop(firstBaseLineToTop: Dp)=Modifier.layout{measurable, constraints ->//测量可测量的参数 这里就是指 Textval placeable = measurable.measure(constraints)//检查是否有文本基线 FirstBaselinecheck(placeable[FirstBaseline]!= AlignmentLine.Unspecified)val firstBaseLine = placeable[FirstBaseline]//高度减去 firstBaseLineval placeableY = firstBaseLineToTop.toPx().toInt() - firstBaseLineval height = placeable.height + placeableY//通过 layout 指定可组合项的尺寸 layout(placeable.width,height){placeable.place(0,placeableY)}}
使用的时候直接给 Text 添加 firstBaselineToTop 属性如下,预览就可以看到使用 firstBaselineToTop 的效果会比直接使用 padding 距离顶端的更小一些
@Preview@Composablefun TextWithPaddingToBaselinePreview() {MyApplicationTheme {Text("firstBaselineToTop", Modifier.firstBaselineToTop(32.dp))}}
@Preview@Composablefun TextWithNormalPaddingPreview() {MyApplicationTheme {Text("普通 padding!", Modifier.padding(top = 32.dp))}}
自定义 View
在 Compose 中自定义 View 比之前使用 xml 简单了很多,比如下面的代码直接在 Canvas 里面划线、画圆圈、画矩形等等
@Composablefun 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))}})}
列表
前面尝试了给 Column 添加一个 verticalScroll()就可以让 Column 滚动了。不过这时候它只是相当于我们用 xml 布局时候的 ScrollView,每次会加载所有的内容。如果数据量太大会影响性能。
如果我们想实现 xml 布局的时候的 Recyclerview 的各种缓存功能,Compose 提供了 LazyColumn 和 LazyRow。例如
@Composablefun MessageList(messages:List<String>){LazyColumn{items(messages){ message ->Text(text = mess
age)}}}
LazyColumn 内部可以通过 item()来加载单个列表项,通过 items()来加载多个列表项。还有一个 itemsIndexed 可以实现带索引的列表项。
@Composablefun MessageList(messages:List<String>){LazyColumn{itemsIndexed(messages){index,message ->Text(text = "index")}}}
如果想要给列表添加一个粘性的头部可以使用 stickyHeader 很方便的实现,不过这个目前是实验性的 api,以后有可能会改或者去掉。
@Composablefun MessageList(messages:List<String>){val listState = rememberLazyListState()LazyColumn(contentPadding = PaddingValues(horizontal = 16.dp,vertical = 8.dp),verticalArrangement = Arrangement.spacedBy(4.dp),state = listState){println("滚动:{listState.firstVisibleItemScrollOffset}")stickyHeader(1) {Text(text = "我是头部",Modifier.fillMaxWidth().height(60.dp).background(Color.Green))}itemsIndexed(messages){index,message ->Text(text = "index", Modifier.background(Color.Yellow).height(60.dp))Spacer(modifier = Modifier.fillMaxWidth().height(5.dp).background(Color.Gray))}}}
上面代码中的 listState 可以监听列表滚动时候的状态,比如第一个可见的位置。listState 还提供了控制列表滚动的方法,比如 scrollToItem、animateScrollToItem 等。
网格列表可以通过 LazyVerticalGrid 来实现,这个 api 目前也是实验性的以后可能会变
@ExperimentalFoundationApi@Composablefun GridList(messages:List<String>){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 的从下往上划出页面,从上往下进入界面同时带有淡入淡出效果。多种效果可以直接用+
号连接起来就行。
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 控件大小改变的时候会有过渡效果。
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 的时候会有淡入淡出的效果
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 过渡
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 = "点击")}}
上面的代码,点击按钮控件的背景从绿色过渡到红色
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.dpBoxState.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.BlueBoxState.Expanded -> Color.Yellow}}Row {Box(Modifier.size(size).background(coloranimate)){}Button(onClick = {currentState = if(currentState == BoxState.Collapsed) BoxState.Expandedelse BoxState.Collapsed}) {Text(text = "点击")}}
上面的代码 transition 管理着两个动画,一个是大小从 10 变到 100,一个是颜色从蓝色变到黄色。点击按钮的时候两个动画一块执行
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))}
上面的代码,页面加载完成之后,控件的背景就会在红色和绿色之间不停的切换。
手势
点击操作
直接使用 Modifier 的 clickable 就可以
@Composablefun 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 方法里面按下、长按、双击、单击都有
@Composablefun 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
@Composablefun 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 方法来执行滚动操作。
该操作需要运行在一个协程的作用域中,使用 rememberCoroutineScope 方法可以获得一个协程的作用域
@Composableprivate 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 就可以监听手指滚动的距离了。
@Composablefun ScrollableDemo(){var offset by remember{ mutableStateOf(0f) }Box(Modifier.size(100.dp).scrollable(orientation = Orientation.Vertical,state = rememberScrollableState { delta ->offset += deltadelta}).background(Color.Gray),contentAlignment = Alignment.Center) {Text(text = offset.toString())}}
嵌套滚动 简单的嵌套滚动很简单,哪个控件需要滚动就加上相应的 verticalScroll 或者 horizontalScroll 即可。
compose 会自动处理滑动冲滚动突 子控件先滚动,滚动到边界之后 父控件开始。
下面的例子就是类表里面的每个 item 还是个列表,滑动的时候就可以看到内部先滑动,滑动到边界后外部列表在滑动。
@Composablefun 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))}}}}}
拖动操作可以使用 draggable 修饰符,它可以实现单一方向上的拖动比如横向的或者纵向的拖动。
比如下面的例子在 draggable 中设置拖拽的方向,监听到拖拽的距离之后设置给自身的 offset 方法就实现拖拽滑动了。
@Composablefun 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 })))}
如果想要多个方向上拖动,可以使用 pointerInput 修饰符,比如下面的例子记录 X 方向和 Y 方向上的偏移量,然后设置给自身就可以实现自由拖动了。
@Composablefun 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.xoffsetY += dragAmount.y}})}
评论