Android Compose 分享 2021-04-14

  • Post author:
  • Post category:其他




Android Compose 分享



目标

  • 认识 Compose
  • 学习 Compose 的基本布局
  • Compose 的状态管理
  • 自定义 Compose 的 View



Compose 的 Column、Row、Box 基本布局

这个三种基本布局的示意如图

在这里插入图片描述

代码示范



Column

@Preview("column")
@Composable
fun ColumnLayout() {
   Column(
       modifier = Modifier
           .background(Color(0xFF063142))
           .padding(16.dp)
           .size(150.dp)
   ) {
       Text(
           text = "", modifier = Modifier
               .fillMaxWidth()
               .height(50.dp)
               .background(Color(0xFFFFFF00))
       )
       Text(
           text = "", modifier = Modifier
               .fillMaxWidth()
               .weight(1f, true)
               .background(Color(0xFF4385F4))
       )
       Text(
           text = "", modifier = Modifier
               .fillMaxWidth()
               .weight(1f, true)
               .background(Color(0xFF3CDB85))
       )
   }
}

  • 16.dp

    是之前布局中的dp,这个的转换必须在有context的内容下才能进行转换为px等



Row

@Preview("row")
@Composable
fun RowLayout() {
    Row(
        modifier = Modifier
            .size(200.dp)
            .background(Color(0xFF063142))
    ) {
        Text(
            text = "", modifier = Modifier
                .fillMaxHeight()
                .width(50.dp)
                .background(Color(0xFFFFFF00))
        )
        Text(
            text = "", modifier = Modifier
                .fillMaxHeight()
                .weight(1f, true)
                .background(Color(0xFF4385F4))
        )
        Text(
            text = "", modifier = Modifier
                .fillMaxHeight()
                .width(50.dp)
                .background(Color(0xFF3CDB85))
        )
    }
}



Box

@Preview("box")
@Composable
fun BoxLayout() {
    Box(modifier = Modifier.size(200.dp)) {
        Text(
            text = "", modifier = Modifier
                .background(Color(0xFF063142))
                .fillMaxSize()
        )
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = "",
                modifier = Modifier
                    .size(
                        100.dp, 100.dp
                    )
                    .background(Color.Yellow)
            )
        }

    }
}



Modifier 的使用

Compose的一个重要的样式属性:修饰符。类似于Style,常用的属性大小,布局,行为和外观。因为Compose是函数式的编程语言,所以这个属性非常灵活。需要注意的

  • 有序
  • 手势(点击、滚动、拖动,滑动,多点触控,旋转、缩放)



列表

列表我们这边来实现一个简易的需求,一个通讯录的个人信息展示。

在这里插入图片描述



实现 Item 布局

@Preview
@Composable
fun ContactListItem(
    @PreviewParameter(ContactDefault::class) contact: Contact
) {
    val context = LocalContext.current

    Row(
        modifier = Modifier
            .background(Color(0xFFF6F6F6))
            .padding(10.dp)
            .height(60.dp)
            .fillMaxWidth()
            .background(Color.White, RoundedCornerShape(10.dp))
            .clickable {
                Toast
                    .makeText(context, "contact:${contact.toString()}", Toast.LENGTH_SHORT)
                    .show()
            }
            .padding(6.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {

        Text(
            text = "",
            modifier = Modifier
                .size(40.dp, 40.dp)
                .background(color = contact.userIcon, shape = CircleShape),
        )

        Column(Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp)) {
            Text(text = contact.userName, fontSize = 16.sp)
            Text(text = contact.email, fontSize = 10.sp, color = Color(0x52030303))
        }
    }
}



拼接 Item 成为 List

@Preview
@Composable
fun ContactList() {
    val dataSource = mockData()
    MyComposeTheme() {
        LazyColumn() {
            dataSource.forEach { contact ->
                item {
                    ContactListItem(contact)
                }
            }
        }
    }
}



总结 List 的实现

通过上面两步发现整体的编码过程只考虑了 Item 和 DataSource 进行遍历即可。LazyColumn进行懒加载和复用,很明显比 RecycleView 和 ListView 高效明了,如果需要加载不同的 Item 只需要在遍历进行判断,加载不一样的 Item 即可。需要注意的是

@PreviewParameter(ContactDefault::class)

对于预览函数的使用,这个主要是结合 AndroidStudio 进行预览的方法,这个方法的接口为

PreviewParameterProvider<T>

,重写values方法,返回mock的数据。



Compose 的状态管理

在 Android 的以往的 xml 的布局中,ViewModel 是使用 Observable 对数据进行封装,根据 xml 生产监听的代码。在 Compose 中使用

remember {mutableStateOf()}

的值,当值变化后,就会执行触发改变。例子

@Preview("state_manager")
@Composable
fun StateManagerView() {

    var name by rememberSaveable { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(text = "Hello")
        OutlinedTextField(value = name,
            onValueChange = { name = it },
            label = { Text(text = "name") }
        )
    }
}



Compose 自定义 View



实现一个手动划线的 DrawView

需求:实现一个划线的View,支持撤销,手指按下后,进行滑动,画出线段,抬起手结束。

@SuppressLint("UnrememberedMutableState")
@Preview("drawView2")
@Composable
fun DrawView2() {

    var pathSave: SnapshotStateList<Pair<Boolean, Pair<Float, Float>>?> = mutableStateListOf(null)

    Box(modifier = Modifier.fillMaxSize()) {

        Canvas(
            modifier = Modifier
                .fillMaxSize()
                .pointerInteropFilter {
                    when (it.action) {
                        MotionEvent.ACTION_DOWN -> {
                            pathSave.add(Pair(true, Pair(it.x, it.y)))
                        }
                        MotionEvent.ACTION_MOVE -> {
                            pathSave.add(Pair(false, Pair(it.x, it.y)))
                        }
                        MotionEvent.ACTION_UP -> {
                            pathSave.add(Pair(false, Pair(it.x, it.y)))
                        }
                        else -> false
                    }
                    true
                }
        ) {
            drawPath(
                path = pathSave.toPath(),
                color = Color.Yellow,
                alpha = 1f,
                style = Stroke(4.dp.toPx())
            )
        }

        Text(
            modifier = Modifier
                .padding(20.dp)
                .background(Color(0xFF00ffff), RoundedCornerShape(12.dp))
                .padding(10.dp)
                .clickable {
                    pathSave.retract()
                },
            text = "回撤"
        )
    }

}

private fun SnapshotStateList<Pair<Boolean, Pair<Float, Float>>?>.retract(): SnapshotStateList<Pair<Boolean, Pair<Float, Float>>?> {
    var lastStart = 0;
    forEachIndexed { index, item ->
        if (item != null && item.first) {
            lastStart = index
        }
    }

    if (lastStart != 0) {
        removeRange(lastStart, size)
        removeLast()
    } else {
        clear()
    }
    return this
}

fun SnapshotStateList<Pair<Boolean, Pair<Float, Float>>?>.toPath(): Path {
    val path = Path()
    forEach {
        if (it != null) {
            if (it.first) {
                path.moveTo(it.second.first, it.second.second)
            } else {
                path.lineTo(it.second.first, it.second.second)
            }
        }
    }
    return path
}

  • pointerInteropFilter

    进行触摸事件处理和原来的差不多

  • Pair<Boolean, Pair<Float, Float>>

    用于记录坐标,按下的为 true ,其他为false,false 连在一起



实现一个进度条的 LevelView

需求:实现绘制弧形,进度显示

@Preview("levelView")
@Composable
fun LevelView(singleAngle: Float = 30F, levelSize: Int = 8, angleValue: Float = 45F) {

    val size = 300.dp
    val lineSize = 20.dp

    //计算出旋转的角度
    val ration = ration(levelSize * singleAngle)
    val slipSize = 360 / singleAngle / 2
    val slipWidth = 5.dp

    Canvas(
        modifier = Modifier
            .size(size)
    ) {
        drawIntoCanvas { canvas ->
            val lineWidth = lineSize.toPx()
            rotate(ration, center) {

                canvas.saveLayer(
                    Rect(
                        Offset.Zero,
                        Size(size.toPx(), size.toPx())
                    ),
                    Paint()
                )

                drawArc(
                    color = Color.Yellow,
                    startAngle = 0F, sweepAngle = levelSize * singleAngle,
                    useCenter = false,
                    topLeft = Offset(lineWidth / 2, lineWidth / 2),
                    size = Size(
                        size.toPx() - lineWidth,
                        size.toPx() - lineWidth,
                    ),
                    alpha = 1.0f,
                    style = Stroke(lineWidth),
                )

                drawArc(
                    color = Color.Red,
                    startAngle = 0F, sweepAngle = angleValue,
                    useCenter = false,
                    topLeft = Offset(lineWidth / 2, lineWidth / 2),
                    size = Size(
                        size.toPx() - lineWidth,
                        size.toPx() - lineWidth,
                    ),
                    alpha = 1.0f,
                    style = Stroke(lineWidth),
                )

                //划线
                for (index in 0..slipSize.toInt()) {
                    rotate(index * singleAngle, center) {
                        drawLine(
                            Color.Yellow,
                            Offset(0f, size.toPx() / 2),
                            Offset(size.toPx(), size.toPx() / 2),
                            slipWidth.toPx(),
                            blendMode = BlendMode.DstOut
                        )
                    }
                }

                canvas.restore()
            }
        }
    }


}

fun ration(maxAngle: Float): Float {
    return if (maxAngle <= 90f) {
        270F - (maxAngle / 2)
    } else if (maxAngle > 90F && maxAngle <= 180F) {
        180F - (maxAngle / 2)
    } else if (maxAngle > 180F && maxAngle <= 270F) {
        180F - (maxAngle + 180 - 360) / 2
    } else {
        90F + (450F - (maxAngle + 90)) / 2
    }
}

  • rotate(ration, center) {}

    选择函数,如果是以往的是

    view.rotate(ration,center)

    用括号包裹的内容是作用的对象区域,在使用时要加以区别
  • 其他绘制和原来的区别不大



Compose 是什么

进过以上的实操和体验后,我们来看看Android Compose的定义和说明:

用于构建原生界面的新款 Android 工具包。它可简化并加快 Android 上的界面开发。使用更少的代码、强大的工具和直观的 Kotlin API,快速让应用生动而精彩。有以下的特点

  • 声明性编程,减少了遍历树,内部管理状态
  • 简单的组合函数
  • 动态内容



总结

Compose 和之前的 View 比较

View Compose
命令式编程 声明式编程
树形结构的遍历查找设置 组件内部进行变更
基于View和ViewGroup衍生 基于更为底层的Canvas绘制
面向对象的继承 组合与函数化
xml相对静态 动态组合
支持预览 支持丰富的预览,包括点击触摸



上手体验

先跟随文档进行简易的编码,有了整体的认知后,进行小需求去实现,看看自己理解的盲区在哪里,之后去修复。



学习资料

名字 推荐原因 网站
Jetpack Compose 官方出品,全面详尽 https://developer.android.google.cn/jetpack/compose
compose-samples 有详尽的代码,方便手动编译和改动 https://github.com/android/compose-samples



版权声明:本文为u012131702原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。