005-Kotlin界面开发之程序猿初试Composable

组合的概念

在Jetpack Compose中,提出了一个概念就是可组合的声明式界面开发。

这个Compose就是可组合中"组合"的意思。也就是我们描述界面,有一系列可以组合使用的元素,这些元素嵌套组合,构成复杂的界面。这种方式和传统的XML方式有点类似。

比如,描述一列标签构成的界面,我们可以这样写:

kotlin 复制代码
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

这个例子很烦人,每个教程的开头都是这个例子,但是这个例子很好地展示了Compose的特点。我们通过一个函数来描述一个标签,这个标签的内容是一个字符串,这个字符串是一个模板,我们可以通过模板来生成不同的标签。这个函数就是一个可组合的函数,我们可以在其他地方调用这个函数,来生成一个标签。

乃至,我们在描述一个按钮的时候,也是类似的:

kotlin 复制代码
@Composable
fun MyButton(text: String, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text(text = text)
    }
}

可以看到,这里按钮中的文字,居然是一个标签,

Java 复制代码
JButton button = new JButton("Click Me");

button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button Clicked!");
    }
});

这就是Compose的特点。跟传统Java Swing的方式不同,Java Swing的按钮是一个对象,这个对象有一个方法叫做 setText,我们可以通过这个方法来设置按钮的文字。而在Compose中,按钮的文字是一个标签,这个标签是一个函数,我们可以通过这个函数来设置按钮的文字。听起来也没啥不同的。

但是这里实际上涉及到一个功能分离和复用的概念。按钮的核心是点击事件;文本控件关心的是显示一串字符。当已经有了文本控件,按钮只需要内涵(组合)一个文本控件,而不需要自己在额外处理文本、字符串的显示。这样的设计,使得我们可以更加灵活地组合界面元素,而不需要关心界面元素的具体实现。

这也是可组合的核心思想,不同的功能,组合起来使用,而不是形成复杂的对象继承关系,难以追溯某个具体的功能是在哪个类中实现的。

例如,Compose中,我们的布局,也是通过可组合的函数来实现的:

kotlin 复制代码
@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

这个可组合函数实现了一个竖向配列其内容的布局。可以看看这个函数的几个参数,来猜一猜这个函数本身的功能。

  • modifier:布局的修饰器,用来修饰布局的样式。
  • verticalArrangement:垂直排列的方式,可以是顶部对齐、居中对齐、底部对齐。
  • horizontalAlignment:水平对齐的方式,可以是左对齐、居中对齐、右对齐。

最后一个参数,正好是接受者函数,这个函数的功能是描述布局的内容。这个函数的接受者是一个 ColumnScope,这个 ColumnScope 是一个接口,用来描述布局的内容。这个接口中包含了一系列的函数,用来描述布局的内容。

我们就能在这个函数中,描述布局的内容,比如:

kotlin 复制代码
Column {
    Text("Hello, World!")
    Text("Hello, Compose!")
    Greeting("Android")
    Row {
        Text("Hello, World!")
        Text("Hello, Compose!")
        Greeting("Android")
    }
}

看看,嵌套一个自定义的Compose函数,这就是可组合的思想。我们可以通过这种方式,组合各种各样的界面元素,来构建复杂的界面。

核心的概念:

  • 一个可组合函数描述一项能力(功能)
  • 自由组合无负担
  • 实现机制:函数接受者
  • 定义方法:@Composable注解

对照实际的界面开发

卑微的起点

这个井字棋的界面,我们在需求分析的阶段可能提出如下的描述:

  • 3x3的格子,可以放置棋子
  • 棋子有两种状态,X和O
  • 点击格子,可以放置棋子
  • 每轮切换棋子状态
  • 提供一个展示当前玩家的标签、同时显示胜负

我们首先可以自己定义一个Grid函数,试试看:

kotlin 复制代码
@Composable
fun Grid() {
    Column {
        Row {
            Cell()
            Cell()
            Cell()
        }
        Row {
            Cell()
            Cell()
            Cell()
        }
        Row {
            Cell()
            Cell()
            Cell()
        }
    }
}

如果这个Cell函数是一个可组合函数,描述一个格子的话,那么这个Grid函数就是描述一个3x3的格子的布局。这样,我们就完成了一个简单的界面的描述。

对于整个界面,我们可以这样描述:

kotlin 复制代码
@Composable
fun TicTacToe() {
    Column {
        Grid()
            Text("Restart")
        }
        Text("Player X's turn")
    }
}

这样,我们就完成了一个简单的界面的描述。

稍微好一点点的改进

看看我们前面的Grid函数,正常情况下,我们就会试图通过循环来生成这个3x3的格子。这样,我们就可以通过一个循环来生成这个3x3的格子,而不是一个一个手动写。

kotlin 复制代码
@Composable
fun Grid() {
    Column {
        for (i in 0..2) {
            Row {
                for (j in 0..2) {
                    Cell()
                }
            }
        }
    }
}

这样是不是就显得高级多了,也让可组合函数的内涵更加丰富,组合函数,甚至可以让程序来组合。

游戏中的最终界面

最终我们的选择跟上面类似,就是增加了亿点点细节。

首先是主函数:

kotlin 复制代码
fun main() = application {
    useResource("config.json") {
        config = loadConfig(it)
    }

    Window(
        onCloseRequest = ::exitApplication,
        title = "Tic Tac Toe",
        state = rememberWindowState(
            position = WindowPosition.Aligned(Alignment.Center),
            size = DpSize(320.dp, 400.dp)
        )
    ) {
        Box(
            modifier = Modifier.background(Color.White).fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            TicToc()
        }

    }
}

我们忽略前面的配置加载,直接看Window函数,这个函数是一个窗口的描述,我们可以看到,这个窗口的内容是一个Box,这个Box是一个盒子布局,这个盒子布局的内容是一个TicToc函数。

这里,我们就把Window理解为一个窗口,这个窗口的参数大概如下:

kotlin 复制代码
@Composable
fun Window(
    onCloseRequest: () -> Unit,
    state: WindowState = rememberWindowState(),
    visible: Boolean = true,
    title: String = "Untitled",
    icon: Painter? = null,
    undecorated: Boolean = false,
    transparent: Boolean = false,
    resizable: Boolean = true,
    enabled: Boolean = true,
    focusable: Boolean = true,
    alwaysOnTop: Boolean = false,
    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
    onKeyEvent: (KeyEvent) -> Boolean = { false },
    content: @Composable FrameWindowScope.() -> Unit
) {
//    ...
}

在实际的调用中,我们可以什么参数都不用设置,只需要利用最后那个content: @Composable FrameWindowScope.() -> Unit参数,来描述窗口的内容。

当然,我们这里增加了三个参数,onCloseRequest是窗口关闭的回调函数,state是窗口的状态,title是窗口的标题。可以看到,如何设定窗口的位置和大小,都是通过state参数来设置的。这个参数的玄机,还得下一次再讲。

现在看Window组合的唯一一个对象, Box,这个Box是一个盒子布局,其内容是一个TicToc函数。

kotlin 复制代码
Box(
    modifier = Modifier.background(Color.White).fillMaxSize(),
    contentAlignment = Alignment.Center
) {
    TicToc()
}

这里Box的第一个参数是一个典型的组合对象修饰方法,大部分(几乎所有)的组合对象都有这个修饰方法,用来修饰这个组合对象的样式。这里,我们设置了背景颜色为白色,然后设置了这个盒子布局的大小为最大。然后这个Modifier最好的特点就是可以链式调用,这样我们可以一次性设置多个修饰条件。第二个函数式参数是一个对齐方式,这个对齐方式是用来设置这个盒子布局的内容的对齐方式。

TicToc函数中,我们可以描述整个井字棋的界面:

kotlin 复制代码
@Composable
@Preview
fun TicToc() {
    // ...

    Column {
        for (i in 0..2) {
            Row {
                for (j in 0..2) {
                    TicTocTile(i, j, ticTacToe)
                }
            }
        }
        Text(
            modifier = Modifier.padding(10.dp).align(Alignment.CenterHorizontally),
            text = ticTacToe.gameText(),
            fontSize = 30.sp,
            color = ticTacToe.color(),
            textAlign = TextAlign.Center
        )

    }

}

好吧,这跟前面我们设计的完全相同,不过也是增加了亿点点细节,修饰符啊,记录的信息啊。

值得注意的是,这里的标签同样增加了修饰符,Modifier对象。具有很好的一致性。

那么在这个TicTocTile函数中,我们可以描述一个格子的内容:

kotlin 复制代码
@Composable
fun TicTocTile(
    x: Int,
    y: Int,
    ticTacToe: TicTacToe
) {

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {

        Button(modifier = Modifier
            .padding(5.dp)
            .fillMaxSize(),
            colors = ButtonDefaults.buttonColors(backgroundColor = ticTacToe[x, y].color()),
            onClick = {
                if (ticTacToe.isGameOver()) {
                    ticTacToe.startNewGame()
                    return@Button
                }

                ticTacToe.nextMove(x, y)

            }) {
            Text(
                text = ticTacToe.textAt(x, y),
                fontSize = 36.sp,
                color = Color.Black
            )
        }
    }
}

更多的细节,毫无必要的复杂性......我们同样是一个Box,里面组合一个Button,这个Button里面是一个Text

无穷无尽的组合,无穷无尽的修饰符,这就是Compose的世界。

这里也就是设置了一些颜色啊、边界啊、间隔啊、大小啊。最后,通过一个onClick事件,来处理点击事件。这个当然也要下面再详细说明。

从前面的概念和具体的例子,我们已经对Compose的可组合的声明式界面开发有了一个初步的感受了。

这里的核心思想就是:

  • 一个可组合函数描述一项能力(功能)
  • 自由组合
  • 实现机制:函数接受者
  • 增加修饰符,逐步完善

Jetpack Compose提供的常用布局和控件

作为一个不那么成熟的桌面开发工具,Jetpack Compose Desktop提供的布局和控件还是比较有限的。

标准布局

  • Column:竖向布局
  • Row:横向布局
  • Box:盒子布局

通过嵌套这三种布局,设置各种不同的修饰条件,我们可以实现各种复杂的布局。

常用控件

  • Text:文本控件
  • Button:按钮控件
  • TextField:文本输入框
  • Checkbox:复选框
  • RadioButton:单选按钮
  • Slider:滑动条
  • ProgressBar:进度条

总之,Jetpack Compose Desktop提供了一系列的基确布局和控件,可以满足基本的界面开发需求。

总结

设计了UI,布局了界面,接下来就是处理事件和交互:

  • 输入信息
  • 显示信息

这两个核心的界面开发内容,下一次我们再来讨论。

相关推荐
刘争Stanley13 小时前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏
sickworm陈浩14 小时前
Java 转 Kotlin 系列:究竟该不该用 lateinit?
android·kotlin
droidHZ2 天前
Compose Multiplatform 之旅—声明式UI
android·kotlin
zhangphil2 天前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
android·kotlin
alexhilton4 天前
Android技巧:学习使用GridLayout
android·kotlin·android jetpack
zhangphil5 天前
Android使用PorterDuffXfermode的模式PorterDuff.Mode.SRC_OUT实现橡皮擦,Kotlin(1)
android·kotlin
IH_LZH6 天前
OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
android·java·okhttp·kotlin
天花板之恋7 天前
ViewModel
jetpack·viewmodel
casual_clover7 天前
Android之RecyclerView显示数据列表和网格
android·kotlin
俺不理解8 天前
Android Compose 悬浮窗
android·生命周期·compose·悬浮窗