网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
这是一个关于我如何使用Kotlin代码以矢量绘制格式绘制棋盘图案的故事,以及我如何最终得到一个比最初小5倍的文件。我做这件事很有趣,所以我想和大家分享。
为什么
对于即将在 Play Store 上发布的 Wear OS 应用,我需要同一应用的 3 个变体:
- 调试变体
- 一个缩小的变体,我可以在本地签署更新,用于性能测试
- Play Store 变体,Google 将使用我没有的密钥进行签名
为了在我的测试设备上轻松区分这 3 个应用程序,我知道 2 个解决方案:
- 为未发布的变体在应用名称中添加前缀字母/符号
- 在未发布的变体的图标上有一个视觉标记
我在之前的工作中成功地使用了第一种方法,但这一次,我想尝试独特的图标方法。我决定为我的调试和缩小变体使用彩色棋盘图案,我不会发布。由于我将定期测试的设备都运行 Android 8 或更高版本(API 26+),因此我可以简单地利用自适应图标,您可以在其中指向依赖于 buildType 的资源作为图标的背景。
VectorDrawables
现在,我想得到棋盘图案。可以在 XML 中定义并在自适应图标中引用的 VectorDrawables 听起来像是获得良好结果的最直接的选择。由于棋盘图案仅由正方形组成,我认为我不需要使用 Affinity Design、Sketch、Adobe Illustrator 或 Inkscape 等矢量图形工具。相反,我可以直接编写绘图命令并确保获得最优化的结果,从而降低 GPU/CPU 负载。
在我之前的工作中,我一直在学习如何自己编写 SVG 路径数据,以便制作加泰罗尼亚的旗帜。如果你只做直线,这实际上很容易。只需要学习“移动”、“水平(线)”、“垂直(线)”和“线”的几个缩写/替代。它们分别是绝对位置的“M”、“H”、“V”和“L”,如果你切换到小写,你就有了相对位置。还有一个z字符,意思是“如果需要,用直线关闭路径”。
例如,这里是如何绘制一个 2x1: 的矩形M0,0 H2 V1 H0 V0 z。用简单的英语,它的意思是:移动到0,0(左上角)并开始绘图,水平线到2(x轴),垂直线到1(y轴),水平线到0(x轴),垂直线到0(再次y轴),最后关闭路径。如果您拿起笔并在一张纸上按照这些说明进行操作,您会看到您刚刚绘制了一个 2x1 的矩形,如广告所示。请注意,空格都是可选的,因此我们可以写入M0,0H2V1H0V0z, 并节省 5 个字节,这是 27.77%,因为我们有 18 个字节和空格。相反,将这 5 个字节添加到 13 意味着增加 38.46% 的大小。无论如何,您稍后会看到为什么尺寸很重要。
从无聊到警告
我开始愉快地用手画我的棋盘图案,几秒钟之内,一种强烈的无聊感开始占据我的心,因为我犯了很多错误并且进展非常缓慢。我没有让它扼杀这个想法。相反,我在 Android Studio 中执行了“文件 > 新建 > 暂存文件”(也适用于 IntelliJ IDEA),选择了 Kotlin,并开始利用软件来完成这项繁琐的任务。在 2 分钟内,我在 a 中编写了 2 个for循环buildString { … },它似乎生成了我正在寻找的内容。我将它复制到了pathData我开始手写的 VectorDrawable 中,经过 1 或 2 次修复,加上大小更改,它准确地显示了我想要的……还有一个额外的东西,来自 IDE 的警告。
非常长的向量路径(1504个字符),这对性能不利。考虑降低精度、删除次要细节或光栅化矢量。
我知道这可能会在绘制图标时影响设备性能,这是我绝对不想造成的。
这是我为大小为 2 的盒子编写的初始代码:
fun generateChessboardPattern1(size: Int): String = buildString {
for (x in 0 until size step 2) {
for (y in 0 until size step 2) {
if ((x - y) % 4 != 0) continue
append("M$x,${y}H${x + 2}V${y + 2}H${x}z")
}
}
}
如您所见,对于需要绘制的每个框,它都添加了类似的内容:Mx,yHaVbHcz. 也就是说,每个盒子至少有 11 个字符,在 14x14 的网格上(28x28,技术上是 2x2 的盒子),它很快就超过了千字节的矢量绘图指令。
我不想去光栅化,我想保持相同的网格大小,所以我想办法在减少指令的同时得到相同的结果。
迭代优化
最小化移动命令
在之前的方法中,移动命令(Mx,y)占了 4 个字符,大约占整个命令的三分之一,所以我首先想到的是减少这些调用。使用下面的代码,我现在每 2 行只得到一个,而不是每个框都要填充:
fun generateChessboardPattern2(size: Int): String = buildString {
check(size % 4 == 0) { "Size must be a multiple of 4" }
for (y in 0 until size step 4) {
append("M0,$y")
for (x in 0 until size step 4) {
append('H'); append(x + 2)
append('V'); append(y + 4)
append('H'); append(x + 4)
append('V'); append(y)
}
append('V'); append(y + 2)
append('H'); append(0)
append('V'); append(y)
append("z ")
}
}
当然,它引入了一个新的限制:大小现在必须是 4 的倍数。这对我来说很好,因为它不会面向客户。但是,我想我可以做得更好。
完全删除移动命令
首先,我问自己:“我能用一条路径画出那个棋盘吗?”
在我的实验中,我发现小写h和v命令是相对位置的变体,这使得在 XML 中手动进行实验变得更加容易。
我尝试了最简单的版本来回答我的问题。
这是我得到的路径数据:M0,0h2v4h2V2H0z
,这就是我在 IDE 中看到的:
然后,我想看看如果我删除前导会发生什么M0,0。预览没有任何变化。我试图打破这件事(成功地),当我恢复我刚刚做的坏事时:它再次显示了正确的事情。我刚刚意识到0,0默认情况下任何路径都会从这里开始,这正是我需要从我的用例开始的地方!
如果您想在实际的 Android Studio 项目中亲自尝试,这里是完整的 VectorDrawable。
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="4"
android:viewportHeight="4">
<path
android:fillColor="#C0C0"
android:pathData="h2v4h2V2H0z"
android:strokeWidth="0.3"
android:strokeColor="#0F0" />
</vector>
2x2 网格(或技术上的 4x4)似乎是一种特殊情况,我想知道填充我想要空的盒子会被我想要填充的盒子包围会发生什么。它们也会被填满,还是像我想要的那样是空的?
我手动继续,一步一步地,我得到了以下路径数据:h2v8h2V0h2v8h2 v-2H0v-2h8v-2H0z
,幸运的是,这正是我正在寻找的结果:
然后我去写代码,还设计了函数,让它支持可变的盒子大小,所以它可以进化为支持矩形板和矩形框。它缺少一些先决条件,例如拒绝boxSize大于 的a size,但它只在我的开发机器上本地运行,我不明白为什么我会尝试这样的事情。
它不是逐个绘制框,也不是使用嵌套的 for 循环绘制 2 行乘 2 行,而是绘制垂直线,然后在关闭路径之前绘制水平线。
之前的实现具有二次复杂度,结果的大小会随着网格大小而增长很多,而下面的实现具有线性复杂度,结果也只会随着大小线性增长。
这是代码:
fun Int.isOdd() = this % 2 != 0 // It's odd that this isn't built into Kotlin, isn't it?
fun generateChessboardPattern3(
size: Int,
boxSize: Int
): String = buildString {
val boxWidth = boxSize
val boxHeight = boxSize
val width = size
val height = size
for (x in 0 until width step boxWidth) {
append("h${boxWidth}")
if (x + boxWidth == width) break // Don't draw the last line.
val verticalOffset = if ((x / boxWidth).isOdd()) 0 else height
append('V')
append(verticalOffset)
}
for (y in height downTo 0 step boxHeight) {
append("v-${boxHeight}")
if (y - boxHeight == 0) break // Don't draw the last line.
val horizontalOffset = if ((y / boxHeight).isOdd()) height else 0
append('H')
append(horizontalOffset)
}
append('z')
}
最后的小优化
在我开始编写上面的第三个版本之前,我在想潜在的等价v-2物比 长V8,但同时v-2比 短V10。我并没有让这阻止我首先编写一个简单的工作版本,但是,我在第二个 for 循环中为第四次迭代更改了 3 行,允许我在结果中尽可能减少一些额外的字节:
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!