跟我一起使用 compose 做一个跨平台的黑白棋游戏(2)界面布局

news2024/11/28 16:27:50

前言

在上一篇文章中,我们讲解了实现这个游戏的总体思路,这篇文章我们将讲解如何实现游戏界面。

本文将涉及到 compose 的自定义绘制与触摸处理,这些内容都可以在我往期的文章中找到对应的教程,如果对这部分内容不太熟悉的话,可以翻回去看看。

实现过程

效果预览

s1

界面分析

我们想要实现的界面分为三个大部分:

  1. 顶部的游戏信息界面:在这个界面中标识当前棋子与棋手信息以及对局信息
  2. 中间的游戏棋盘
  3. 底部控制按钮

其中,1 和 3 都可以使用基础的 compose 组件实现,而 2 的棋盘以及棋子需要使用自定义绘制来手动绘制。

分析完成,我们首先绘制出棋盘。

绘制棋盘

棋盘同样由三个部分组成:背景、线条、棋子。

在绘制之前,我们需要先构建出绘制作用域(DrawScope),这里直接使用 Canvas 绘制:

@Composable
fun ReversiView(
    modifier: Modifier,
    chessBoard: Array<ByteArray>,
    onClick: (row: Int, col: Int) -> Unit
) {
    Canvas(
        modifier = modifier
    ) { 
        // ……
    }
}

在这里,我们给 ReversiView 抽出了三个参数:

modifier 这个不用多说,几乎所有 composable 都会抽出这个参数,但是这里没有给出默认值而是选择使用必须值是因为 Canvas 明确要求必须使用 modifier 指定组件的大小,无论是指定准确值还是使用 fillMaxSize 等指定相对值都可以,但是这里会有一个坑,下面会讲到。

chessBoard 则是当前的棋盘数据数组,这里使用 Byte 来表示是因为我们要使用的算法用的是 Byte …… 其中,使用 -1 表示黑子; 1 表示 白子;0 表示空白。

onClick 是点击棋盘格子的回调匿名函数,其中 rowcol 分别表示点击的横纵坐标(这里的坐标指格子坐标,如 7x7 表示最右下角的格子),并且我们需要对点击范围做处理,确保只回调点击格子内的触摸事件,格子外不会回调。

接下来,我们先计算出需要使用的几个参数:

// 棋盘内容边界
val chessBoardSide = size.width * ChessBoardScale
// 棋盘线长
val lineLength = size.width - chessBoardSide * 2
// 棋盘格子尺寸
val boxSize = lineLength / 8

其中,sizeDrawScope 提供的变量,表示的是当前绘制区域的大小;ChessBoardScale 是我们定义的一个常量,表示棋盘四周的边界比例:const val ChessBoardScale = 0.05f

绘制背景

然后先绘制出背景的木板,这里我们其实就是直接将准备好的图片放了上去:

// 画棋盘背景
drawImage(
    image = backgroundImage,
    srcOffset = IntOffset(0, 0),
    dstSize = IntSize(size.width.toInt(), size.width.toInt())
)

画背景这里有两点需要注意。

一是 drawImage 需要的是一个 ImageBitmap 类型的图片,这里我们可以将其理解为 compose 封装的,可以跨平台的 Bitmap 数据。

我们这里获取 ImageBitmap 的函数如下:

// 在 ViewUtils 中
/**
 * 安卓平台需要的是 Int 类型的 ID, 但是在桌面端,使用的是 String 类型的路径,
 * 为了后期移植方便,现在直接写成 String 类型
 * */
@Composable
fun loadImageBitmap(resourceName: String): ImageBitmap {
    return ImageBitmap.imageResource(id = resourceName.toInt())
}

// ……

// 在 ReversiView 中
val backgroundImage = loadImageBitmap(resourceName = R.drawable.mood.toString())

上面代码的注释中我们也说了,这里单独抽出一个方法用于获取资源文件是为了之后的跨平台处理,因为不同平台对于资源加载的方式不一样,所以需要自己处理一下。

第二点需要注意的是,我们需要指定绘制的 ImageBitmap 的大小,不然取决于调用时附加的 modifier 可能会出现意想不到的结果。

指定绘制大小的方法也很简单,使用 dstSize = IntSize(size.width.toInt(), size.width.toInt()) 这个参数的作用就是将绘制的图片铺满绘制区域(size.width)

对了,因为黑白棋的棋盘是一个 8x8 格子的正方形,并且我们编写的是一个竖屏游戏,所以我们会以宽为基准作为绘制区域尺寸,所以这里我们的宽和高使用的都是 size.width ,并不是我写错了哦。

效果:

s2

绘制线条

线条的绘制十分简单,没有什么需要注意的地方,直接画就完事了:

// 画棋盘线
for (i in 0..8) {
    // 横线
    drawLine(
        color = Color.Black,
        start = Offset(chessBoardSide, chessBoardSide + i * boxSize),
        end = Offset(lineLength+chessBoardSide, chessBoardSide + i * boxSize)
    )
    // 竖线
    drawLine(
        color = Color.Black,
        start = Offset(chessBoardSide + i * boxSize, chessBoardSide),
        end = Offset(chessBoardSide + i * boxSize, lineLength+chessBoardSide)
    )
}

效果:

s3

绘制棋子

绘制棋子时需要遍历 chessBoard 这个数组,并根据其中的数值大小决定需要绘制的棋子颜色,或者是否绘制棋子:

val whiteChess = loadImageBitmap(resourceName = R.drawable.white_chess.toString())
val blackChess = loadImageBitmap(resourceName = R.drawable.black_chess.toString())

// ……

// 画棋子
for (col in 0 until 8) {
    for (row in 0 until 8) {
        if (chessBoard[col][row] == (-1).toByte()) {  // 黑子
            drawImage(
                image = blackChess,
                srcOffset = IntOffset(0, 0),
                dstOffset = IntOffset(
                    (chessBoardSide + col * boxSize).toInt(),
                    (chessBoardSide + row * boxSize).toInt()
                ),
                dstSize = IntSize(boxSize.toInt(), boxSize.toInt())
            )
        }
        if (chessBoard[col][row] == (1).toByte()) {  // 白子
            drawImage(
                image = whiteChess,
                srcOffset = IntOffset(0, 0),
                dstOffset = IntOffset(
                    (chessBoardSide + col * boxSize).toInt(),
                    (chessBoardSide + row * boxSize).toInt()
                ),
                dstSize = IntSize(boxSize.toInt(), boxSize.toInt())
            )
        }
    }
}

绘制棋子,我们依旧使用的是直接绘制图片,其实这里我想自己画一个棋子来着,但是画了一通都觉得画出来的棋子好丑啊,所以就放弃了,索性直接用图片算了。

需要注意的是,绘制棋子需要对绘制的图片做偏移处理,使其绘制到正确的格子内:

dstOffset = IntOffset(
    (chessBoardSide + col * boxSize).toInt(),
    (chessBoardSide + row * boxSize).toInt()
)

这里我们通过每个格子的大小(boxSize)乘以横向坐标(col 横向格子数)能得到 x 轴坐标,同理通过 row 计算得到 y 轴坐标。

并且,我们需要指定棋子尺寸为占满格子尺寸:dstSize = IntSize(boxSize.toInt(), boxSize.toInt())

最终效果如下(这里是棋盘的初始状态):

s4

完成棋盘的点击事件

给 Canvas 的 modifier 添加修饰符:

modifier = modifier.pointerInput(Unit) {
    detectTapGestures(
        onTap = { offset: Offset ->
            getChessCoordinate(
                size, offset, onClick
            )
        }
    )
}

其中,getChessCoordinate 定义如下:

fun getChessCoordinate(
    size: IntSize,
    offset: Offset,
    onClick: (row: Int, col: Int) -> Unit
) {
    // 棋盘内容边界
    val chessBoardSide = size.width * ChessBoardScale
    // 棋盘线长
    val lineLength = size.width - chessBoardSide * 2
    // 棋盘格子尺寸
    val boxSize = lineLength / 8

    if (offset.x in chessBoardSide..size.width-chessBoardSide
        && offset.y in chessBoardSide..size.width-chessBoardSide) { // 判断是否在有效范围内
        // 计算点击坐标
        val row = floor((offset.x - chessBoardSide) / boxSize).toInt()
        val col = floor((offset.y - chessBoardSide) / boxSize).toInt()

        // 回调点击函数
        onClick(row, col)

        Log.i("test", "ReversiView: row=$row, col=$col")
    }
}

上面代码也很简单,和绘制时差不多,按照坐标计算出点击的是哪个格子,并回调给上级函数。

不过在计算时会先判断点击的是不是格子区域,如果不是则不会回调。

完成剩余组件

剩下的就是将底部控制UI和顶部信息UI加上即可:

@Composable
fun GameView() {
    val screenWidth = LocalConfiguration.current.screenWidthDp

    Column(
        Modifier
            .fillMaxSize()
            .padding(24.dp)
    ) {
        // 顶部信息栏
        Row(
            Modifier
                .fillMaxWidth()
                .padding(bottom = 36.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Column(
                Modifier
                    .fillMaxWidth()
                    .weight(0.3f)
                    .background(Color.LightGray),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "您",
                    Modifier.padding(bottom = 8.dp),
                    fontSize = 18.sp
                )
                Row(
                    Modifier.padding(4.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Image(
                        bitmap = loadImageBitmap(resourceName = R.drawable.black_chess.toString()),
                        contentDescription = "black")
                    Text(text = "x2", Modifier.padding(2.dp))
                }
            }
            Column(
                Modifier
                    .fillMaxWidth()
                    .weight(0.3f),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "VS",
                    fontSize = 24.sp,
                    fontWeight = FontWeight.Bold
                )
            }
            Column(
                Modifier
                    .fillMaxWidth()
                    .weight(0.3f),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "电脑",
                    Modifier.padding(bottom = 8.dp),
                    fontSize = 18.sp
                )
                Row(
                    Modifier.padding(4.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Image(
                        bitmap = loadImageBitmap(resourceName = R.drawable.white_chess.toString()),
                        contentDescription = "black")
                    Text(text = "x2", Modifier.padding(2.dp))
                }
            }
        }

        // 游戏棋盘
        ReversiView(
            modifier = Modifier.size(screenWidth.dp),
            chessBoard = initChessBoard(),
            onClick = { row: Int, col: Int ->
                Log.i("test", "GameView: click row=$row, col=$col")
            }
        )

        // 底部控制按钮
        Row(
            Modifier
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceAround
        ) {
            Button(onClick = { /*TODO*/ }) {
                Text(text = "重新开始")
            }
            Button(onClick = { /*TODO*/ }) {
                Text(text = "提示")
            }
        }
    }
}

从上面的调用棋盘界面的代码中:

ReversiView(
    modifier = Modifier.size(screenWidth.dp),
    chessBoard = initChessBoard(),
    onClick = { row: Int, col: Int ->
        Log.i("test", "GameView: click row=$row, col=$col")
    }
)

我们可以看到,对于棋盘的尺寸定义,我们定义成了指定长宽均为屏幕宽度: val screenWidth = LocalConfiguration.current.screenWidthDp

为啥长宽都用屏幕宽度,上面已经说了,那么,思考一个问题,为什么这里不直接使用 Modifier .fillMaxWidth() 呢?而非要获取到屏幕宽度后再手动设置给它呢?

这个问题,留给各位略微思考一下,下一篇文章再告诉大家为什么。(ps:其实只要你自己写一下就知道为什么了)

最终效果:

s1

总结

自此,咱们的界面布局就算完成了,虽然现在看起来可能简陋了点,但是现在还只是在验证可行性,等所有代码写完,我们再进行亿点点优化,就会丰富好看多了。

对了,项目源码我将在这系列文章完结,也就是项目真正写完的时候上传到 Github,到时会在文中附上链接的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550707.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文阅读_语音合成_VALL-E

论文阅读 number headings: auto, first-level 2, max 4, _.1.1 name_en: Neural Codec Language Models are Zero-Shot Text to Speech Synthesizers name_ch: 神经网络编解码器语言模型实现零样本TTS paper_addr: http://arxiv.org/abs/2301.02111 date_read: 2023-04-25 da…

Docker代码环境打包进阶 - DockerHub分享镜像

1. Docker Hub介绍 Docker Hub是一个广泛使用的容器镜像注册中心&#xff0c;为开发人员提供了方便的平台来存储、共享和分发Docker容器镜像。它支持版本控制、访问控制和自动化构建&#xff0c;并提供了丰富的公共镜像库&#xff0c;方便开发人员快速获取和使用各种开源应用和…

Redis+Lua脚本防止超卖

超卖就是因为查询库存和扣减库存两个操作不是原子性操作&#xff0c;通过rua脚本执行这两个操作可以保证这两个操作原子性 判断库存量是不是大于等于1&#xff0c;如果大于等于1对库存减1&#xff0c;否则就不去减库存 StringBuilder sb new StringBuilder();sb.append("…

【数据分享】我国地级市绿地利用现状数据(9个指标\Shp格式)

绿地是城市生态的重要组成部分&#xff0c;在很多分析中都会用到绿地数据&#xff01;之前我们分享过Shp和Excel格式的全国地级市2003-2020年绿地面积数据&#xff08;可查看之前文章获悉详情&#xff09;&#xff0c;以及中国31个主要城市的绿地空间分布的栅格数据&#xff08…

vue中使用colorthief获取图片的主色调成分

colorthief官网 https://lokeshdhakar.com/projects/color-thief/#examples 安装 npm i --save colorthief yarn add colorthief 使用案例 <template><div class"box app" :style"{ background: bodyBgColor }"><div class"img-item&…

NSS LitCTF Web 部分wp

目录 1、PHP是世界上最好的语言&#xff01;&#xff01; 2、这是什么&#xff1f;SQL &#xff01;注一下 &#xff01; 3、Ping 4、作业管理系统 5、我Flag呢&#xff1f; 6、1zjs 7、Vim yyds 8、Http pro max plus 1、PHP是世界上最好的语言&#xff01;&#xff01…

C++中vector的用法

博主简介&#xff1a;Hello大家好呀&#xff0c;我是陈童学&#xff0c;一个与你一样正在慢慢前行的人。 博主主页&#xff1a;陈童学哦 所属专栏&#xff1a;CSTL 前言&#xff1a;Hello各位小伙伴们好&#xff01;欢迎来到本专栏CSTL的学习&#xff0c;本专栏旨在帮助大家了解…

Compose太香了,不想再写传统 xml View?教你如何在已有View项目中混合使用Compose

前言 在我的文章 记一次 kotlin 在 MutableList 中使用 remove 引发的问题 中&#xff0c;我提到有一个功能是将多张动图以N宫格的形式拼接&#xff0c;并且每个动图的宽保证一致&#xff0c;但是高不保证一致。 在原本项目中我使用的是传统 view 配合 RecyclerView 和 GridL…

jenkins入门与安装

一、实验环境 selinux iptables off 主机名IP系统版本gitlab10.10.10.200rhel7.5jenkins10.10.10.10rhel7.5tomcat10.10.10.11rhel7.5 二、安装jenkins 1、解压安装包 下载地址&#xff1a;https://download.docker.com/linux/static/stable/x86_64/ [rootjenkins ~]# tar xf …

C语言指针初级

目录 一、什么是指针 二、指针和指针类型 三、野指针 1.野指针的成因&#xff1a; 2.如何规避野指针 四、指针运算 1.指针-整数 2. 指针之间的加减 五、二级指针 六、指针数组 一个男人&#xff0c;到底要走多少的路&#xff0c;才能成为一个真正的男人 本专栏适用于…

【Linux】ubuntu设置ssh密钥登录详细教程,附Mobaxterm和pycharm ssh python解释器配置教程

0、写在前面 我们通常使用SSH 客户端来远程使用 Linux 服务器。但是&#xff0c;一般的密码方式登录&#xff0c;容易有密码被暴力破解的问题。所以&#xff0c;一般我们会将 SSH 的端口设置为默认的 22 以外的端口&#xff0c;或者禁用 root 账户登录。但是即使是将端口设置为…

复杂数据集,召回、精度等突破方法记录【以电科院过检识别模型为参考】

目录 一、数据分析与数据集构建 二、所有相关的脚本 三、模型效果 一、数据分析与数据集构建 由于电科院数据集有17w-18w张&#xff0c;标签错误的非常多&#xff0c;且漏标非常多&#xff0c;但是所有有效时间只有半个月左右&#xff0c;显卡是M60&#xff0c;训练速度特别…

linux防火墙之iptables

一、iptables概述 Linux 系统的防火墙 &#xff1a;IP信息包过滤系统&#xff0c;它实际上由两个组件netfilter 和 iptables组成。 主要工作在网络层&#xff0c;针对IP数据包。体现在对包内的IP地址、端口、协议等信息的处理上。 netfilter&#xff1a; 属于“内核态”&…

Java基础重温巩固

方法 方法与方法之间是平级关系&#xff0c;不能嵌套return表示结束当前方法 基本数据类型和引用数据类型 基本数据类型&#xff1a;数据存储在自己的空间中 引用数据类型&#xff1a;数据存储在其他空间中&#xff0c;自己空间存储的是地址值 值传递 传递基本数据类型时&…

详解Windows系统安装TensorRT

目录 下载TensorRT安装TensorRT测试 TensorRT 是 NVIDIA 推出的一款高性能神经网络部署引擎.Windows系统下TensorRT目前不能简单直接通过pip指令自动下载安装, 安装之前还需要提前安装好 CUDA 和 CUDNN. CUDA和CUDNN安装可参考: 详解 Windows系统下安装 CUDA 与 CUDNN. &…

ES6之Module:export、import

文章目录 前言一、export命令1.export2.export default&#xff08;默认暴露&#xff09; 二、import命令1.通用导入方式2.解析赋值导入方式 三、结果总结 前言 ES6之前&#xff0c;JavaScript语言一直没有模块&#xff08;Module&#xff09;体系&#xff0c;无法将一个大型程…

《计算机网络——自顶向下方法》精炼——3.5.1-3.5.4

人生像攀登一座山,而找寻出路,却是一种学习的过程,我们应当在这过程中,学习稳定、冷静,学习如何从慌乱中找到生机。——席慕蓉 文章目录 TCPTCP协议概述报文段结构序号、确认号 超时问题计算RTT计算重传时间 可靠数据传输 TCP TCP协议概述 TCP是面向连接的协议&#xff0c;在…

nest笔记十一:一个完整的nestjs示例工程(nestjs_template)

概述 链接&#xff1a;nestjs_template 相关文章列表 nestjs系列笔记 示例工程说明 这个工程是我使用nestjs多个项目后&#xff0c;总结出来的模板。这是一个完整的工程&#xff0c;使用了yaml做为配置&#xff0c;使用了log4js和redis和typeorm&#xff0c;sawgger&#…

Less和sass安装及使用

CSS预处理器 由来 CSS本身不是一种编程语言。你可以用它开发网页样式&#xff0c;但是没法用它编程。换句话说&#xff0c;CSS基本上是设计师的工具&#xff0c;不是程序员的工具。它并不像其它程序语言&#xff0c;比如说JavaScript等&#xff0c;有自己的变量、常量、条件语…

手把手教你验证upd与tcp“端口”开发策略

系列文章目录 文章目录 系列文章目录前言一、问题&#xff1f;二、验证网络策略步骤三、增强验证网络策略总结 前言 这篇文章&#xff0c;本意是让大家了解除了常用的telnet之外&#xff0c;在运维过程中&#xff0c;如果在服务器中未发现相关命令还可以借用像ssh、wget 等命令…