安卓 kotlin Jetpack Compose 定西 2024-12-20 2024-12-22 Compose API 设计原则 视图树一旦生成便不可随意改变,视图的刷新依靠Composable函数的反复执行来实现
composable
函数只能在composeable
函数中调用
在Compose的世界中,一切组件都是函数,由于没有类的概念,因此不会有任何继承的层次结构,所有组件都是顶层函数
可以在DSL中直接调用
Composable
作为函数相互没有继承关系 ,有利于促使开发者使用组合的视角去思考问题
基本概念啥的都有点不太一样,和之前学的
常用UI组件 Compose
提供了Column,Row,Box三种布局组件,类似于传统视图中的LinearLayout(Vertical)
,LinearLayout(Horizontal)
,RelativeLayout
Modifier修饰符 Modifier允许我们同诺链式调用的写法来为组件应用一系列的样式设置,如边距,字体,位移等,在Compose中,每个基础的Composable组件都有一个modifier参数,通过传入自定义的Modifier来修改组件的样式
size
设置组件大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Image( painterResource(id = R.drawable.shiguang2), contentDescription = null , modifier = Modifier .size(100. dp) .clip(CircleShape) ) Image( painterResource(id = R.drawable.shiguang2), contentDescription = null , modifier = Modifier .size(width = 200. dp, height = 500. dp) )
background
用来为修饰组件添加背景色,背景色支持设置color的纯色背景也可以使用brush设置渐变色背景,Brush是Compose提供的用来创建线性渐变色的工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Row { Box( Modifier .size(50. dp) .background(color = Color.Red) ) { Text("纯色" , Modifier.align(Alignment.Center)) } Spacer(modifier = Modifier.width(16. dp)) Box( Modifier .size(50. dp) .background(brush = verticalGradientBrush) ) { Text("渐变色" , Modifier.align(Alignment.Center)) } } val verticalGradientBrush = Brush.verticalGradient( colors = listOf( Color.Red, Color.Yellow, Color.Green ) )
传统视图中的View的background属性可以用来设置图片格式的背景,但是这里是不支持的,Compose的background只能设置颜色背景
fillMaxSize
size可以控制组件的大小,而fillMaxSize
可以让组件在高度或者宽度上填满父空间,此时可以用fillMaxSize
1 2 3 4 5 6 7 8 9 10 11 12 13 Row( Modifier.background(Color.Yellow).width(100. dp).height(200. dp) ) { Box( Modifier.fillMaxWidth().height(50. dp).background(Color.Blue) ) }
border&padding
border用来为被修饰组件添加边框,边框可以指定颜色,粗细,以及通过Shape指定形状,比如圆角矩形等,padding用来为被修饰组件增加间隙,可以在border前后各插入一个padding,区分对外和对内的比间距
1 2 3 4 5 6 7 8 9 10 11 12 Box( modifier = Modifier .padding(8. dp) .border(2. dp, Color.Red, shape = RoundedCornerShape(2. dp)) .padding(8. dp) ){ Spacer( Modifier .size(width = 100. dp, height = 10. dp) .background(Color.Red) ) }
相对于传统布局有Margin和Padding之分,Compose中只有padding这一种修饰符,根据在调用链的位置不同发挥不同的作用,概念更加简洁
offset
offset修饰符用来移动被修饰组件的位置,我们在使用时只分别传入水平方向与垂直方向的偏移量
Modifier调用顺序会影响最终UI呈现的效果,这里应使用offset修饰符偏移,再使用background修饰符绘制背景色
1 2 3 4 5 6 7 8 9 10 11 12 Box( Modifier .size(100. dp) .offset { IntOffset( 200. dp.roundToPx(), 150. dp.roundToPx() ) } .background(Color.Red) )
作用域限定Modifier修饰符 某些Modifier修饰符只能在特定作用域中使用,有利于类型安全地调用它们,所谓作用域,在Kotlin中就是一个带有Receiver
的代码,例如Box组件参数中的conent就是一个Receiver
类型为BoxScope的代码块,因此其子组件都处于BoxScope作用域中
matchParentSize
matchParentSize
是只能在BoxScope中使用的作用域限定修饰符,当使用matchParentSize
设置尺寸时,可以保证当前组件的尺寸与父组件相同,而父组件默认的是wrapContent
,这个相当于根据内层组件的大小来确定自己的大小
但是如果使用fillMaxSize
来取代matchParentSize
,那么该组件的尺寸会被设置为父组件所允许的最大尺寸,这样会导致背景铺满整个屏幕
weight
在RowScope与ColumnScope中可以使用专属的weight修饰符来设置尺寸,与size修饰符不同的是,weight修饰符允许组件通过百分比设置尺寸,也就是允许组件可以自适应适配各种屏幕尺寸的移动终端设备
案例:希望让白色方块、蓝色方块和红色方块共享一整块Column空间,其中每种颜色方块高度各占比1/3,使用weight修饰符可以很容易地实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Composable fun WeightModifierDemo () { Column( Modifier .width(300. dp) .height(200. dp) ) { Box( Modifier .weight(1f ) .fillMaxWidth() .background(Color.Green)) Box( Modifier .weight(1f ) .fillMaxWidth() .background(Color.Blue)) Box( Modifier .weight(1f ) .fillMaxWidth() .background(Color.Red)) } }
Modifier实现原理 Modifier调用顺序会影响到最终UI的呈现效果,这是因为Modifier会由于调用顺序不同而产生不同的Modifier链,Compose会按照Modifier链来顺序完成页面测量布局与渲染
Modifier实际上是一个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Suppress("ModifierFactoryExtensionFunction" ) @Stable @JvmDefaultWithCompatibility interface Modifier { fun <R> foldIn (initial: R , operation: (R , Element ) -> R ) : R fun <R> foldOut (initial: R , operation: (Element , R ) -> R ) : R fun any (predicate: (Element ) -> Boolean ) : Boolean }
嗯,这一段,等会用了再看吧,先会用,再理解概念
常用的基础组件
Text文本
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Composable fun Text ( text: String , modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle ? = null , fontWeight: FontWeight ? = null , fontFamily: FontFamily ? = null , letterSpacing: TextUnit = TextUnit.Unspecified, textDecoration: TextDecoration ? = null , textAlign: TextAlign ? = null , lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true , maxLines: Int = Int .MAX_VALUE, minLines: Int = 1 , onTextLayout: ((TextLayoutResult ) -> Unit )? = null , style: TextStyle = LocalTextStyle.current )
最佳实践:Text组件的参数会按照其使用频度排序,并尽量添加默认实现,便于在单元测试或者预览中使用,我们自定义的Composable组件也应该遵循这样的参数设计原则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Composable fun TextDemo () { Column { Spacer(Modifier.size(100. dp)) Text( text = "Hello Android" ) Text( text = "Hello Android" , style = TextStyle( fontSize = 25. sp, fontWeight = FontWeight.Bold, background = Color.Cyan, lineHeight = 35. sp ) ) Text( text = "Hello Android" , style = TextStyle( color = Color.Gray, letterSpacing = 4. sp ) ) Text( text = "Hello Android" , style = TextStyle( textDecoration = TextDecoration.LineThrough ) ) Text( text = "Hello Android" , style = MaterialTheme.typography.headlineLarge.copy(fontStyle = FontStyle.Italic) ) } }
style中的部分参数也可以直接在Text中直接设置,例如字体大小,粗细,且Text参数会覆盖掉style中的样式
AnnotatedString多样式文字
在一段文字中对局部内容应用特别格式一示突出
AnnotatedString
SpanStyle:用于描述在文本中子串的文字样式
ParagraphStyle:用于描述文本中子串额段落格式
Range:确定子串的范围
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontSize = 24. sp)) { append("你现在学习的章节是" ) } withStyle( style = SpanStyle( fontWeight = FontWeight.W900, fontSize = 24. sp ) ) { append("Text" ) } append("\n" ) withStyle(style = ParagraphStyle(lineHeight = 25. sp)) { append("在刚刚讲的内容中,我们学会了如何应用文字样式,以及如何限制文本的行数和处理溢出的视觉效果" ) append("\n" ) append("现在,我们正在学习" ) withStyle( style = SpanStyle( fontWeight = FontWeight.W900, textDecoration = TextDecoration.Underline, color = Color(0xFF59AB69 ) ) ) { append("AnnotatedString" ) } } } )
SpanStyle继承了TextStyle中关于文字样式相关的字段,而ParagraphStyle继承了TextStyle中控制段落的样式,例如textAligh,lineHeight等,某种意义上二者拆分了TextStyle,可以对子串分别进行文字以及段落样式设置
Compose提供了一种可以点击的文本组件ClickedText
,可以响应我们对文字的点击,并返回点击位置
Text自身默认是不能被长按选择的,否则在Button中使用,又会出现”可粘贴的Button”的例子
Compose提供了专门的SelectionContainer
组件,对包裹的Text进行选中
1 2 3 4 SelectionContainer { Text("这是可复制的文字" ) }
TextField输入框
最常用的文本输入框,具有两种风格,一种是默认,也就是filled,另一种是OutlinedTextField
使用var text by remember { mutableStateOf("") }
报错时,可能是没有导入下面两个依赖
1 2 import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue
1 2 3 4 5 6 7 8 9 @Composable fun TextFiledDemo () { var text by remember { mutableStateOf("" ) } TextField(value = text, onValueChange = { text = it }, label = { Text("用户名" ) }) }
就是这个样子的:
为输入框添加修饰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Composable fun TextFiledSample () { var username by remember { mutableStateOf("" ) } var password by remember { mutableStateOf("" ) } Column { TextField( value = username, onValueChange = { username = it }, label = { Text("用户名" ) }, leadingIcon = { Icon( imageVector = Icons.Filled.AccountBox, contentDescription = stringResource(R.string.description) ) }, maxLines = 1 ) OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text("密码" ) }, trailingIcon = { IconButton(onClick = {}) { Icon( painter = painterResource(id = R.drawable.shiguang2), contentDescription = stringResource(R.string.description) ) } }, maxLines = 1 ) } }
两种风格的输入框,都自带动效
需要注意的是,TextField和OutlinedTextField都是遵循Material Desingn准则的,所以无法直接修改输入框的高度,如果尝试修改高度,会看到输入区域被截断
这时就可以使用更基础的BasicTextField
,这种输入框有更多的可自定义的参数
B站风格搜索框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 @Composable fun SearchBar () { var text by remember { mutableStateOf("" ) } Box( Modifier .fillMaxSize() .background(Color(0xFFD3D3D3 )), contentAlignment = Alignment.Center ) { BasicTextField( value = text, maxLines = 1 , onValueChange = { text = it }, decorationBox = { innerTextField -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2. dp, horizontal = 8. dp) ) { Icon( imageVector = Icons.Filled.Search, contentDescription = stringResource(R.string.description) ) Box( modifier = Modifier .padding(horizontal = 10. dp) .weight(1f ), contentAlignment = Alignment.CenterStart, ) { if (text.isEmpty()) { Text( text = "输入点东西看看吧~" , style = TextStyle( color = Color(0 , 0 , 0 , 128 ) ) ) } innerTextField() } if (text.isNotEmpty()) { IconButton( onClick = { text = "" }, modifier = Modifier.size(16. dp) ) { Icon( imageVector = Icons.Filled.Close, contentDescription = stringResource(R.string.description) ) } } } }, modifier = Modifier .padding(horizontal = 10. dp) .background(Color.White, CircleShape) .height(30. dp) .fillMaxWidth() ) } }
说实话,并没有感觉这玩意比前端好写,甚至感觉这写起来比前端麻烦多了,而且结构不清晰
图片组件
Icon图标
Icon组件用于显示一系列小图标,Icon组件支持三种不同类型的图片设置
Icon可以传入Resource中的资源
imageVector
imageBitmap
vectorResource
imageResource
painterResource
可以直接使用Material
包中的图标
1 2 3 4 5 6 7 8 @Composable fun IconSample () { Icon( imageVector = Icons.Filled.Favorite, contentDescription = null , tint = Color.Red ) }
Icon组件还可以加载网络上下载的图标库,google图标库
Image图片
image组件中有一个contentScale
参数用来指定图片在Image组件中的伸缩样式
1 2 3 4 5 6 7 8 9 10 11 12 @Composable fun ImageSample () { Image( painterResource(id = R.drawable.shiguang2), contentDescription = null , modifier = Modifier.size(width = 100. dp, height = 200. dp), contentScale = ContentScale.FillBounds ) }
colorFilter
参数用于设置一个ColorFilter,它可以通过对绘制的图片的每个像素的颜色进行修改,实现不同的图片效果
tint
colorMatrix
lighting 灯光效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var colorMatrix = ColorMatrix().apply { setToSaturation(0f ) } @Composable fun ImageSample () { Image( painterResource(id = R.drawable.shiguang2), contentDescription = null , modifier = Modifier.size(width = 100. dp, height = 200. dp), contentScale = ContentScale.Crop, colorFilter = ColorFilter.lighting( multiply = Color.Red, add = Color.Blue ) ) }