【技术前沿】 整理下JetPackCompose 的常用api

1,214 阅读11分钟

Jetpack Compose前瞻

  • 就在不久前官方推出的JetpackCompose1.0.0-beta01版本,让广大Android开发都加入了学习的路途,并且Google开办的Android开发挑战赛更是涌入了大量学习JetPack Compose 的开发者大佬们,一度成为了当下火热的UI工具包。

  • Jetpack Compose是作为构建本机AndroidUI的现代工具包,使用更少的代码,强大的工具和直观的KotlinAPI简化并加速了Android上的UI开发。

欢迎各位同学fork以下GitHub仓库,同时也是挑战赛官方提供的模板project

官方模板User this templete

本文demo代码运行环境

  • Android studio版本为:Android Studio Arctic Fox >>> 2020.3.1 Canary 9
  • JetpackCompose版本为: 1.0.0-beta01
  • JDK版本为11.0.9 ps:
  1. 想要正常运行模板项目,jdk大于11,且Android studio版本在4.3以上,也就是Canary 9 Arctic Fox
  2. 另外想问下有大佬在解决在现在Android studio beta版本中成功运行模板project的么?

43B5552F-A725-444f-A5AD-42904B3ADF41.png

学习指南

以下我大概从Foundation(基础),Layout(布局),Material(材料),Animation(动画)

Foundation

  • Image
  1. 图像用于显示图像。它类似于经典Android View系统中的ImageView,可以使用painterResource加载资源文件
@Composable
fun ImageResource() {
    val image: Painter = painterResource(R.drawable.lagotto_romagnolo)
    Image(painter = image, contentDescription = null)
}

2.实际效果如下

image.png

  • Text
  1. 使用Text可以显示文本。可以使用style参数定义诸如textdecoration或fontfamily之类的东西。
示例代码
@Composable
@Preview
fun TextExample(){
    Column {
        Text("Jacky Tallow")
        Text("Text with cursive font", style = TextStyle(fontFamily = FontFamily.Cursive))
        Text(
            text = "Text with LineThrough",
            style = TextStyle(textDecoration = TextDecoration.LineThrough)
        )
        Text(
            text = "Text with underline",
            style = TextStyle(textDecoration = TextDecoration.Underline)
        )
        Text(
            text = "Text with underline, linethrough and bold",
            style = TextStyle(
                textDecoration = TextDecoration.combine(
                    listOf(
                        TextDecoration.Underline,
                        TextDecoration.LineThrough
                    )
                ), fontWeight = FontWeight.Bold
            )
        )
    }
}
  1. 普通文字
 Text("Jacky Tallow")
  1. 草书文字
 Text("Text with cursive font", style = TextStyle(fontFamily = FontFamily.Cursive))
  1. 带有LineThrough的文字
Text(
            text = "Text with LineThrough",
            style = TextStyle(textDecoration = TextDecoration.LineThrough)
        )
  1. 带有下划线的文字
        Text(
            text = "Text with underline",
            style = TextStyle(textDecoration = TextDecoration.Underline)
        )
  1. 带有下划线,粗体和直行的文字
  Text(
            text = "Text with underline, linethrough and bold",
            style = TextStyle(
                textDecoration = TextDecoration.combine(
                    listOf(
                        TextDecoration.Underline,
                        TextDecoration.LineThrough
                    )
                ), fontWeight = FontWeight.Bold
            )
        )
  • BaseTextFiled BaseTextField可用于插入文本。
@Composable
fun BaseTextFieldDemo() {
    var textState by remember { mutableStateOf(TextFieldValue("h")) }

    Column {
        TextField(value = textState, onValueChange = {
            textState = it
        })
        Text("The textfield has this text: " + textState.text)
    }

}
  • Canvas 不用多说,作为画布可以进行绘制各种各样的图形去美化界面,下面是该代码实例
@Composable
@Preview(showBackground = true)
fun CanvasDemo() {

    Canvas(modifier = Modifier.fillMaxSize()) {
        //绘制矩形
        drawRect(Color.Blue, topLeft = Offset(0f, 0f), size = Size(this.size.width, 55f))
        //绘制圆形
        drawCircle(Color.Red, center = Offset(50f, 200f), radius = 40f)
        //绘制直线
        drawLine(
            Color.Green, Offset(20f, 0f),
            Offset(200f, 200f), strokeWidth = 5f
        )

        //绘制饼状
        drawArc(
            Color.Black,
            0f,
            60f,
            useCenter = true,
            size = Size(300f, 300f),
            topLeft = Offset(60f, 60f)
        )
    }
}
  1. 分别可以使用drawRect绘制矩形,drawCircle绘制圆形,drawLine绘制直线,drawArc绘制饼状

  2. 实际效果如下

canvas.png

  • Shape

Shape故名思义,可以用来绘制特定形状的Composable

  1. 下面使用了个例子绘制了圆形形状,圆角形状,矩形形状,带切角的圆形形状
@Composable
@Preview
fun PreviewShapeDemo() {

    Column(modifier = Modifier
        .fillMaxWidth()
        .wrapContentSize(Alignment.Center)) {
        //矩形形状
        ExampleBox(shape = RectangleShape)
        //圆形形状
        ExampleBox(shape = CircleShape)
        //带圆角的矩形形状
        ExampleBox(shape = RoundedCornerShape(8.dp))
        //带有切角的矩形形状
        ExampleBox(shape = CutCornerShape(10.dp))
    }
}


//形状
@Composable
fun ExampleBox(shape: Shape) {
    Box(modifier = Modifier
        .size(100.dp)
        .clip(shape)
        .background(Color.Red))
}

  1. 那么如何绘制自定义形状呢? 一般情况下, 有通用形状和扩展形状界面
  • 通用形状 这里用GenericShape。让我们看看三角形是如何绘制的。
private val TriangleShape = GenericShape { size ->
    // 1)
    moveTo(size.width / 2f, 0f)

    // 2)
    lineTo(size.width, size.height)

    // 3)
    lineTo(0f, size.height)
}
  • 在GenericShape内部,您可以绘制自定义形状。您可以访问size object。这是应用形状的Composable的大小。你可以用高度size.height与宽度size.width
  • 最初,画家将从父对象composable(0x,0y)的左上方开始。使用moveTo()可以设置绘画者的坐标。在这里,坐标将设置为父布局的一半宽度,并且坐标为0y。
  • 这将从在1)中设置的画家坐标绘制一条线到父布局的右下角。然后将画家坐标自动设置到此角。
  • 这将在左下角画一条线。GenericShape将隐式执行close() -函数。close()将从最后一个绘制器坐标到第一个定义的坐标绘制一条线。
  • 扩展形状界面 看一看一个例子,有这个example来解释一下
/**
 * Defines a generic shape.
 */
interface Shape {
    /**
     /**
     * Creates [Outline] of this shape for the given [size].
     *
     * @param size the size of the shape boundary.
     * @param density the current density of the screen.
     *
     * @return [Outline] of this shape for the given [size].
     */
    fun createOutline(size: Size, density: Density): Outline
}

您可以扩展Shape接口以创建自己的Shape实现。在createOutline内部,您可以获取可组合对象的大小(将形状应用到该对象)以及屏幕的密度。您必须返回Outline的实例。Outline是具有以下子类的密封类:

  • 矩形(val rect:Rect)
  • 四舍五入(val rrect:RRect)
  • 通用(val path:Path)
class CustomShape : Shape {
    override fun createOutline(size: Size, density: Density): Outline {
        val path = Path().apply {
            moveTo(size.width / 2f, 0f)
            lineTo(size.width, size.height)
            lineTo(0f, size.height)
            close()
        }
        return Outline.Generic(path)
    }
}
  • LazyColumn LazyColumn是作为垂直滚动列表,只有组成,并勾画出当前可见的项目,它类似于经典Android View系统中的Recyclerview
  1. 下面是例子,简单使用了LazyColumn
@Composable
fun LazyColumnDemo() {
    val list = listOf(
        "A", "B", "C", "D"
    ) + ((0..100).map { it.toString() })
    LazyColumn(modifier = Modifier.fillMaxHeight()) {
        items(items = list, itemContent = { item ->
            Log.d("COMPOSE", "This get rendered $item")
            when (item) {
                "A" -> {
                    Text(text = item, style = TextStyle(fontSize = 80.sp))
                }
                "B" -> {
                    Button(onClick = {}) {
                        Text(text = item, style = TextStyle(fontSize = 80.sp))
                    }
                }
                "C" -> {
                    //Do Nothing
                }
                "D" -> {
                    Text(text = item)
                }
                else -> {
                    Text(text = item, style = TextStyle(fontSize = 80.sp))
                }
            }
        })
    }
}
  • LazyRow 一个LazyRow是一个水平滚动列表,只组成,并勾画出当前可见的项目。它类似于经典Android View系统中的卧式Recyclerview。
  1. 下面是一个例子,简单使用了LazyRow
@Composable
fun LazyRowDemo() {
    val list = listOf(
        "A", "B", "C", "D"
    ) + ((0..100).map { it.toString() })
    LazyRow(modifier = Modifier.fillMaxHeight()) {
        items(items = list, itemContent = { item ->
            Log.d("COMPOSE", "This get rendered $item")
            when (item) {
                "A" -> {
                    Text(text = item, style = TextStyle(fontSize = 80.sp))
                }
                "B" -> {
                    Button(onClick = {}) {
                        Text(text = item, style = TextStyle(fontSize = 80.sp))
                    }
                }
                "C" -> {
                    //Do Nothing
                }
                "D" -> {
                    Text(text = item)
                }
                else -> {
                    Text(text = item, style = TextStyle(fontSize = 80.sp))
                }
            }
        })
    }
}

Layout

  • Box Box作为盒子布局,将相互堆叠,可以使用align修饰符指定应在何处进行绘制可组合的对象
  1. 可以看看下面的例子
fun BoxExample() {
    Box(Modifier.fillMaxSize()) {
        Text("This text is drawn first", modifier = Modifier.align(Alignment.TopCenter))
        Box(
            Modifier.align(Alignment.TopCenter).fillMaxHeight().width(
                50.dp
            ).background( Color.Blue)
        )
        Text("This text is drawn last", modifier = Modifier.align(Alignment.Center))
        FloatingActionButton(
            modifier = Modifier.align(Alignment.BottomEnd).padding(12.dp),
            onClick = {}
        ) {
            Text("+")
        }
    }
}
  • Column Column将显示每个孩子在先前的孩子之下,它类似于具有垂直方向的LinearLayout。
  1. 看看下面的例子
fun ColumnDemo() {
    Column {
        Text(text = "Hello World")
        Text(text = "Hello World2")
    }
}
  • Row Row将显示每个孩子在前一个孩子旁边,它类似于具有水平方向的LinearLayout。
  1. 看看下面的例子
@Composable
@Preview
fun RowDemo() {

    Row {
       Text(text = "Hello World")
       Text(text = "Hello World2") 
    }
}
  • ConstraintLayout Compose中的ConstraintLayout与经典的Android View System中的ConstraintLayout相似
  1. 看看下面的例子
@Composable
fun ConstraintLayoutDemo() {
    ConstraintSet {
        val text1 = createRefFor("text1")
        val text2 = createRefFor("text2")
        val text3 = createRefFor("text3")

        constrain(text1) {
            start.linkTo(text2.end)
        }
        constrain(text2) {
            top.linkTo(text1.bottom)
        }

        constrain(text3) {
            start.linkTo(text2.end)
            top.linkTo(text2.bottom)
        }

    }
    ConstraintLayout {
        Text("Text1", Modifier.layoutId("text1"))
        Text("Text2", Modifier.layoutId("text2"))
        Text("This is a very long text", Modifier.layoutId("text3"))
    }
}

Meterial

  • TextFiled TextField可用于插入文本。这等效于Android View系统中的EditText。
  1. 简单看看下面的例子
//文本域
@Composable
@Preview
fun TextFieldDemo() {

    Column(Modifier.padding(16.dp)) {
        val textState = remember { mutableStateOf(TextFieldValue()) }
        TextField(
            value = textState.value,
            onValueChange = { textState.value = it }
        )
        Text("The textfield has this text: " + textState.value.text)
    }
}
  • Button 一个按钮有一个的onClick用功能,可以添加Text-Composable或任何其他composables作为Button的子元素。
  1. 简单看看下面的例子,简单加入了onclick,colors参数
@Composable
fun ButtonExample() {
    Button(
        onClick = { /*TODO*/ },
        colors = ButtonDefaults.textButtonColors(backgroundColor = Color.Red)
    ) {
        Text("Button")
    }

}
  • Switch 用作compose中的switch开关,下面是简单的一个例子
@Composable
@Preview
fun SwitchDemo() {

    //持久化状态
    val checkedState = remember { mutableStateOf(true) }

    Switch(checked = checkedState.value, onCheckedChange = { checkedState.value = it })
}

  • Card Card与Compose中的CardView等效,用作卡片布局
  1. 简单使用下Card
@Composable
@Preview
fun CardDemo() {
    Card(
        Modifier
            .fillMaxSize()
            .padding(8.dp),elevation = 8.dp) {
        Text(text = "This is a Card")

    }
}
  • AlertDialog 实现Compose自带的对话框,和Android View中的AlertDialog警告对话框基本样式一致
  1. 简单通过例子学习下,设置一个点击按钮,onClick事件为true的时候展示对话框
fun AlertDialogSample() {
    MaterialTheme {
        Column {
            val openDialog = remember { mutableStateOf(false) }

            Button(onClick = {
                openDialog.value = true
            }) {
                Text("Click me")
            }

            if (openDialog.value) {

                AlertDialog(
                    onDismissRequest = {
                        openDialog.value = false
                    },
                    title = {
                        Text(text = "Dialog Title")
                    },
                    text = {
                        Text("Here is a text ")
                    },
                    confirmButton = {
                        Button(
                            onClick = {
                                openDialog.value = false
                            }) {
                            Text("This is the Confirm Button")
                        }
                    },
                    dismissButton = {
                        Button(
                            onClick = {
                                openDialog.value = false
                            }) {
                            Text("This is the dismiss Button")
                        }
                    }
                )
            }
        }

    }
}
  • CircularProgressIndicator 顾名思义,CircularProgressIndicator可以用于显示圆形进度,大致可以分为以下两种
  1. 不设置参数,它会一直进行下去
CircularProgressIndicator()
  1. 设置参数后,当您为progress参数设置一个值时,指示符将与该进度一起显示。例如,进度为0.5f会将其填满一半。
CircularProgressIndicator(progress = 0.5f)
  • 下面来看看一个例子来学习
@Composable
@Preview
fun CircularProgressIndicatorSample() {
    var progress by remember { mutableStateOf(0.1f) }
    val animatedProgress = animateFloatAsState(
        targetValue = progress,
        animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
    ).value

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Spacer(Modifier.height(30.dp))
        Text("CircularProgressIndicator with undefined progress")
        //无参
        CircularProgressIndicator()
        Spacer(Modifier.height(30.dp))
        Text("CircularProgressIndicator with progress set by buttons")
        //带参数
        CircularProgressIndicator(progress = animatedProgress)
        Spacer(Modifier.height(30.dp))
        OutlinedButton(
            onClick = {
                if (progress < 1f) progress += 0.1f
            }
        ) {
            Text("Increase")
        }

        OutlinedButton(
            onClick = {
                if (progress > 0f) progress -= 0.1f
            }
        ) {
            Text("Decrease")
        }
    }
}
  • LinearProgressIndicator

LinearProgressIndicator可以用于显示直线上的进度,也称为进度条。可以分为两种:

  1. 不定,使用不带progress参数的LinearProgressIndicator时,它将永远运行。
LinearProgressIndicator()
  1. 确定,当您为progress参数设置一个值时,指示符将与该进度一起显示。例如,进度为0.5f会将其填满一半。
LinearProgressIndicator(progress = 0.5f)
  • 下面可以看看例子来结合学习
@Composable
fun LinearProgressIndicatorSample() {
    var progress by remember { mutableStateOf(0.1f) }
    val animatedProgress = animateFloatAsState(
        targetValue = progress,
        animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
    ).value

    Column(horizontalAlignment = Alignment.CenterHorizontally) {

        Spacer(Modifier.height(30.dp))
        Text("LinearProgressIndicator with undefined progress")
        LinearProgressIndicator()
        Spacer(Modifier.height(30.dp))
        Text("LinearProgressIndicator with progress set by buttons")
        LinearProgressIndicator(progress = animatedProgress)
        Spacer(Modifier.height(30.dp))
        OutlinedButton(
                onClick = {
                    if (progress < 1f) progress += 0.1f
                }
        ) {
            Text("Increase")
        }

        OutlinedButton(
                onClick = {
                    if (progress > 0f) progress -= 0.1f
                }
        ) {
            Text("Decrease")
        }
    }
}
  • DropdownMenu DropdownMenu Composable可用于创建DropdownMenu,也就是下拉菜单
fun DropdownMenu(
    expanded: Boolean,
    onDismissRequest: () -> Unit,
    modifier: Modifier = Modifier,
    offset: DpOffset = DpOffset(0.dp, 0.dp),
    properties: PopupProperties = PopupProperties(focusable = true),
    content: @Composable ColumnScope.() -> Unit
)
  • 当前如果为true的话,与dropdownContent的弹出菜单将显示
  • CheckBox 作为选择框,可以进行是否选择选项操作
  1. 简单看看下面的例子,一样传入当前状态判断是否选中
@Composable
fun CheckBoxDemo() {
    val checkedState = remember { mutableStateOf(true) }
    Checkbox(
        checked = checkedState.value,
        onCheckedChange = { checkedState.value = it }
    )
}
  • FloatingActionButton 悬浮按钮,和Android View中一样分为FloatActionButton和ExtendedFloatingActionButton(可扩展的)
  1. FloatActionButton
fun FloatingActionButtonDemo() {
    FloatingActionButton(onClick = { /*do something*/}) {
        Text("FloatingActionButton")
    }
}
  1. ExtendedFloatingActionButton
fun FloatingActionButtonDemo() {
    FloatingActionButton(onClick = { /*do something*/}) {
        Text("FloatingActionButton")
    }
}
  • ModalDrawerLayout 使用ModalDrawerLayout可以创建抽屉
  1. 看看下面的例子,点击按钮展开抽屉
@Composable
@Preview
fun ModalDrawerSample() {
    val drawerState = rememberDrawerState(DrawerValue.Closed)
    val scope = rememberCoroutineScope()


    ModalDrawer(
        drawerState = drawerState,
        drawerContent = {
            Column {
                Text("Text in Drawer")
                Button(onClick = {
                    scope.launch {
                        drawerState.close()
                    }
                }) {
                    Text("Close Drawer")
                }
            }
        }, content = {
            Column {
                Text(text = "Text in BodyContext")
                Button(onClick = {
                    scope.launch { 
                        drawerState.open()
                    }
                }) {
                    Text(text = "Click to open")
                }
            }
        })
}

  • RadioButton 简单来说,该api可以实现单选按钮进行单选操作
  1. 可以看看官方的例子
fun RadioButtonSample() {
    val radioOptions = listOf("A", "B", "C")
    val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[1]) }
    Column {
        radioOptions.forEach { text ->
            Row(
                Modifier
                    .fillMaxWidth()
                    .selectable(
                        selected = (text == selectedOption),
                        onClick = {
                            onOptionSelected
                            (text)
                        }
                    )
                    .padding(horizontal = 16.dp)
            ) {
                RadioButton(
                    selected = (text == selectedOption),
                    onClick = { onOptionSelected(text) }
                )
                Text(
                    text = text,
                    style = MaterialTheme.typography.body1.merge(),
                    modifier = Modifier.padding(start = 16.dp)
                )
            }
        }
    }
}
  • Sacffold 脚手架是一种实现基本材料设计布局结构的布局。可以添加诸如TopBar,BottomBar,FAB或Drawer之类的东西。
  1. 可以看看下面的例子
@Composable
@Preview
fun ScaffoldDemo() {
    val materialBlue700 = Color(0xFF1976D2)
    val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Open))
    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            //topBar
            TopAppBar(
                title = { Text("TopAppBar") },
                backgroundColor = materialBlue700
            )
        },
        //悬浮按钮位置
        floatingActionButtonPosition = FabPosition.End,
        //悬浮按钮
        floatingActionButton = {
            FloatingActionButton(onClick = {}) {
                Text("X")
            }
        },
        //抽屉内容
        drawerContent = { Text(text = "drawerContent") },
       //内容
        content = { Text("BodyContent") },
        //bottomBar
        bottomBar = { BottomAppBar(backgroundColor = materialBlue700) { Text("BottomAppBar") } }
    )
}
  • scaffoldState: 使用scaffoldState可以设置抽屉的打开状态(DrawerState.Opened或DrawerState.Closed)
  • floatActionButton: 在这里可以添加FloatingActionButton,可以设置任何Composable,但是已经为该用例设置了FloatingActionButton
  • floatActionButtonPosition : 添加FAB后,可以使用此指定它的位置。默认位置在布局的末尾。
  • drawerContent: 可以在此处设置抽屉的内容。
  • content: 这里是支架的主要内容
  • bottombar:可以将布局的一部分设置在屏幕底部,可以设置任何Composable,但是已经为该用例创建了BottomAppBar。

Animation

  • CrossFade 淡入淡出可用于使用淡入淡出动画在可组合对象之间切换。
  1. 下面通过例子来学习以下CrossFade吧,每次单击按钮时,屏幕内容将随着3秒的动画持续时间而变化。
@Composable
@Preview
fun CrossFadeDemo() {
    var currentColor by remember { mutableStateOf(MyColor.Red) }

    Column {
        Row {
            MyColor.values().forEach { myColors ->
                Button(
                    onClick = { currentColor = myColors },
                    Modifier
                        .weight(1f, true)
                        .height(48.dp)
                        .background(myColors.color),
                    colors = ButtonDefaults.buttonColors(backgroundColor = myColors.color)
                ) {
                    Text(text = myColors.name)
                }
            }
        }
        Crossfade(targetState = currentColor) {
            Crossfade(targetState = currentColor, animationSpec = tween(3000)) { selectedColor ->
                Box(modifier = Modifier.fillMaxSize().background(selectedColor.color))
            }
        }
    }
}

enum class MyColor(val color: Color) {
    Red(Color.Red),
    Green(Color.Green),
    Blue(Color.Blue)
}

  1. 思考,这个淡入淡出是如何工作的呢?
  • 该例子包含一个屏幕,该屏幕的顶部有3个按钮。单击按钮后,下面的屏幕将更改为所选颜色。
Crossfade(targetState = currentColor, animationSpec = tween(3000)) { selectedColor ->
   Box(modifier = Modifier.fillMaxSize().background(selectedColor.color))
}
  • Crossfade希望某种状态可以检测何时重新组合。在这个例子中,这是currentColor,它是具有选定颜色枚举的状态。此状态将设置为当前参数。

  • 使用animationSpec参数,可以设置应使用哪个动画在可组合对象之间进行切换。此处的默认参数是补间动画。您可以在实现AnimationSpec的任何类之间进行选择。此示例中的3000是补间动画将具有的持续时间。

  • 最后一个参数是Crossfade Composable的主体。在这里,您必须创建要显示的UI。selectedColor是currentColor的当前值。在此示例中,我使用Box来显示颜色。每次单击三个按钮之一时,currentColor的值将更改,并且Crossfade将使用新旧UI之间的动画进行重构。

demo地址

总结

  • 简单来说,Jetpack Compose是新的下一代UI工具包。它使用基于声明性组件的范例来轻松,快速地构建UI,它完全用Kotlin编写,并包含Kotlin语言的风格和人体工程学。
  • 通过示例撰写 Jetpack Compose是一个新的声明性UI工具包,旨在满足创建现代用户界面的需求。通过检查我们使用它创建的具体UI,开始学习Compose,并了解构成该工具包的新API和Material组件。本文同学我通过主题,动画,布局等示例,演示如何自定义和组合组件以构建真实的UI,简化自己的开发经验并带来新的可能性。
  • Jetpack Compose现在已经正式发布了beta版本,其中的api将不会做很大的改动,正是广大开发者学习交流的时刻,之后可能开始将其添加到现有实际应用程序中
  • 这几天的学习Compose中,也让我思考与工具的紧密集成如何使开发经验变得更好,继续学习Compose以及它如何帮助自己构建更好的应用程序吧!