Ohhnews

分类导航

$ cd ..
DZone Java原文

技能、Java 17与主题强调色:Codename One本周更新

#codename one#java 17#agents.md#原生主题#metal渲染器

上周我们聊了 Metal 和皮肤设计器。本周的头条内容是关于生成全新项目时的默认样貌:默认 JDK 升级为 Java 17,并且每个生成的项目都附带了一个 AGENTS.md 创作技能,让任何现代 AI 代理都能智能地处理该项目。

此外,还有一些值得关注的内容:新原生主题上的运行时强调色板、三个 Metal 跟进(其中一个引入了新的矩阵纠正 translate API)、JDK 11+ String API 缺口已补齐,以及 iOS 推送权限不再在应用启动时触发。

[LOADING...]

Java 17 成为默认版本

我们将 Initializr 生成的新项目默认改为 Java 17+,以聚焦 Codename One 的未来。如果你有特殊原因需要使用 Java 8,仍可从单选面板中选择。无论选哪个都行,但我们推荐新项目走 Java 17 路径。

生成的项目可以使用任何 JDK 17 及以上版本进行构建(我们经常测试 21 和 25);你不需要专门安装 Java 17。关于 Java 17 支持如何在工具链中运作(包括哪些语言特性会被引入你的应用代码,以及 iOS/Android 端口如何处理新版字节码)的更大图景,已在今年早些时候的官方 Java 17 实验性支持一文中介绍过。本周的变化是默认值及其描述:(实验性)标签已移除,除非你主动选择退出,否则 Java 17 就是默认值。

AGENTS.md 与 Codename One 技能

PR #4946 的另一项改动是:Initializr 生成的每个 Java 17 项目现在都会在项目根目录下附带一个 AGENTS.md 文件,以及一个 Codename One 创作技能。

AGENTS.md 是一种约定,用于向任意 AI 代理传递项目特定的上下文。Claude Code、Cursor、Codex、Aider 等工具都会查找它。Codename One 项目现在也提供了这个文件。

实际技能内容位于 .agent-skills/codename-one/ 目录(与供应商无关),其源码在仓库的 scripts/initializr/common/src/main/resources/skill 路径下,你可以直接阅读。此外还在 .claude/skills/codename-one/SKILL.md 处有一个薄桩文件,以便 Claude Code 的 /skills 选择器可以索引到它;该桩文件会重定向到相同的供应商无关内容。

我们特意将此范围限定在 Java 17 项目。较旧的 Java 8 构建有额外的限制(Java 5/8 源码目标、retrolambda、历史字节码重写规则),这使得“实际可用的内容”回答起来明显更复杂。将技能限定在 Java 17,我们可以为代理提供关于语言级别、工具链和构建命令的更清晰图景,而无需将 SKILL.md 的一半篇幅用于说明限制。如果你继续使用 Java 8,项目布局保持不变,一切照旧。

以下是该技能带来的几个我认为确实有用的功能:

代理可以在 jdb 下调试 Codename One 应用。这是我最满意的一点。模拟器是一个常规 JVM,因此标准 Java 调试器可以干净地附加,但代理之前完全不知道这个工作流的存在。技能的 debugging.md 参考文档详细介绍了如何使用正确的 -Xrunjdwp 标志启动模拟器、附加 jdb、设置断点、转储局部变量和单步执行。同样的工作流也适用于 CI 以及任何无法使用图形调试器的无头环境。对于原本只能“加个 println 然后祈祷”的 LLM 来说,这无疑是一把更锋利的工具。

代理可以在建议某个 API 之前检查它是否属于 Codename One 子集。Codename One 面向的是类似 Java 5/8 形状的 JDK,因此相同的字节码可以翻译到 iOS、Android 和 JavaScript。一个只读过常规 Java 惯用法的代理通常会尝试使用 java.nio.filejava.time 或框架未包含的 java.util.concurrent 中的部分内容。该技能附带了一个单文件 IsApiSupported.java 工具,代理可以在编写代码之前调用它来验证某个类或方法。

代理可以在应用 CSS 片段之前验证其有效性。Codename One CSS 有自己独立的子集;对于浏览器开发者来说看起来很好的规则可能会被编译器静默丢弃。IsCssValid.java 工具允许代理在不启动模拟器的情况下确认编译器会接受该片段。

这三件事结合起来,就是之前对一个 Codename One 项目“礼貌但无用”的代理,现在变得真正高效的原因。如果你不使用代理,同一份 Markdown 也是我们所写过的关于框架心智模型的最佳导览之一;打开你今天生成的任何项目中的 .agent-skills/codename-one/SKILL.md,从头到尾读一遍。

原生主题强调色

PR #4884 闭环了我们两周前发布的新的 iOS Modern 和 Material 3 原生主题。这些原生主题现在将其强调色板公开为命名主题常量,因此将你的应用重新品牌化为自己的颜色只需五行 CSS 更改,而无需分支修改。

在你的 theme.css#Constants 块内覆盖这些常量:

$ style
#Constants {
    includeNativeBool: true;
    darkModeBool: true;
    --accent-color: #ff2d95;
    --accent-color-dark: #ff2d95;
    --accent-pressed-color: #c71a75;
    --accent-on-color: #ffffff;
}

就是这样。每个带有强调色的 UIID 都会采用新颜色。亮色和暗色是独立的(--accent-color 对应亮色,--accent-color-dark 对应暗色),并且部分覆盖是允许的;没有重新声明的部分会保持框架默认值。

Material 3 还有一些额外的容器级常量用于凸起表面色调;iOS 会忽略这些常量。此外,还提供了一条运行时路径用于动态主题化(应用内强调色切换、品牌变体、A/B 测试),它也使用相同的常量。

开发者指南中的“原生主题”章节详细介绍了这一点,包括完整的 iOS 和 Android 常量表以及绑定系统故意不适用的地方:强调色板覆盖

值得强调的一点是:主题化中那些不随应用变化的部分(哪些 UIID 参与强调色板、它们暴露哪些状态、它们的暗色对应项)留在框架内部。而那些随应用变化的部分(你的颜色)则作为五个常量放在你的项目中,仅此而已。这正是本次改动存在的全部理由。

Metal 跟进

上周发布了 Metal 渲染器。本周是跟进周:三个 PR,加上 Graphics 上的一个新 API,我认为这个 API 未来会悄然带来巨大回报。

逐轴缩放分解(#4939,修复 #3302)

长期存在的问题 #3302 有一个清晰的复现步骤:g.translate + g.scale(sx, sy) + fillShape,当 sx != sy 时,生成的形状会明显偏离框架同时发出的轴对齐 drawRectdrawLine 调用。内接于矩形的三角形会逃出它的边界矩形。

原因是旧有的 alpha 遮罩路径以统一缩放(对角线比率 h2/h1)光栅化形状,然后通过 GPU 矩阵非均匀地拉伸生成的纹理以获得所需的宽高比。边界框数学在实数上是精确的,但纹理在中间的统一缩放阶段经过像素舍入,因此拉伸会使光栅化形状偏离 drawRectdrawLine 已经处于的像素网格。

修复方法是将用户变换的 2x2 线性部分分解,取列范数作为 (sx, sy),然后以 S(sx, sy) 光栅化路径,这样逐轴拉伸会在光栅化时针对矢量路径进行,而不是针对像素网格;然后在 GPU 上仅应用剩余的 transform * S(1/sx, 1/sy)。剩余部分只是旋转(最坏情况下还有剪切),因此在采样时不会发生逐轴拉伸,alpha 遮罩纹理与它的 drawRect 兄弟落在同一个像素网格上。

此更改仅限于 Metal;GL ES2 路径保留其旧分支,因此现有的 GL golden 文件字节相同。一个新的 InscribedTriangleGrid 屏幕截图测试已注册到 Cn1ssDeviceRunner,因此内接三角形属性现在可以在 CI 中通过视觉验证。

旋转下裁剪的诊断(#4924,朝向 #3921)

PR #4924 并不修复一个 bug,而是定位一个 bug。问题 #3921 是“旋转下裁剪在某些端口上行为错误”,同时与一个 getClip / setClip(int[])) 往返限制纠缠在一起,而报告者本人也称这是一个单独的问题。为了将两者分开,我们发布了一个仅使用 pushClip / popCliprotateRadians 的屏幕截图测试。裁剪区域通过 clipRect 在 30 度旋转内部变为非轴对齐,这迫使框架进入其多边形裁剪分支。预期结果是倾斜 30 度的红色填充,在两条对角处与海军蓝轮廓重叠,在另外两条对角处则短于轮廓。PR 中预标记了两种可区分的失败模式:裁剪区域扩大到轴对齐边界框(红色与海军蓝轮廓完全匹配),或多边形裁剪完全失效(红色填充整个单元格)。当 iOS Metal 单元渲染此测试时,我们一眼就能知道我们看到的是三种行为中的哪一种。预期的失败单元也是一个假设:ClipRect.m 的多边形初始化器存储 x = y = w = h = -1,然后 Metal 执行路径调用 CN1MetalSetScissor(0, 0, -2, -2),其 width <= 0 / height <= 0 分支将裁剪区域设置为整个帧缓冲而不是预期的多边形。如果屏幕截图证实了该假设,修复方法就是将多边形裁剪回退替换为一行代码。

iOS Metal 色彩空间提示(#4909,修复 #4908)

PR #4909 添加了一个 ios.metal.colorSpace 构建提示。在此之前,Metal 层的 CAMetalLayer.colorspace 被硬编码为 sRGB。对于大多数应用来说,这是正确的;sRGB 正是你现有素材所编写成的色彩空间。但在 iPhone XR 及更高版本上,Apple 的屏幕是宽色域(Display P3),而某个营销驱动的品牌如果发布了 P3 素材,通过 sRGB 管道传输时会明显丢失饱和度。

可接受的值有:sRGB(默认)、displayP3deviceRGBlinearSRGBextendedSRGBextendedLinearSRGBnone。在 codenameone_settings.properties 中设置:

codename1.arg.ios.metal.colorSpace=displayP3

ios.metal=false 时,该提示处于休眠状态,因此现有的 GL 构建保持不变。无法识别的值会产生警告日志并回退到 sRGB。文档位于 Working-With-iOS.asciidoc

新的 translateMatrix API

#4939 中的内接三角形网格测试还暴露了 Graphics 中一个值得作为独立功能提出的细微问题。Graphics.translate(int, int) 不会像 scale()rotateRadians() 那样组合到仿射变换中。它会累积到一个每 Graphics 实例的整数偏移量中,该偏移量在绘制坐标传递给实现矩阵之前被添加。这是框架最初版本(当时 Graphics 根本没有矩阵)留下的遗产。现在,其后果是令人惊讶的:后续的 g.scale(sx, sy) 也会乘以整数平移值,这意味着相同的代码会根据你在缩放之前还是之后进行平移而产生明显不同的位置。

新的 Graphics.translateMatrix(float, float) 直接将平移组合到实现矩阵中,方式与 scalerotateRadians 已经采用的方式相同。结果是无论你是在 FormGraphics 中绘制,还是在可变的 ImageGraphics 中绘制,都能在 iOS(GL 和 Metal)、JavaSE、Android 和 JavaScript 端口上获得一致的“后乘平移到当前变换”语义。

$ java
// 矩阵正确组合。当你希望 translate 像 scale 和 rotate 一样(组合到仿射变换中)时,使用此方法。
g.translateMatrix(centerX, centerY);
g.rotateRadians(angle);
g.scale(sx, sy);
g.translateMatrix(-centerX, -centerY);

对于编写仿射变换管道的应用代码(来自 Java2D 和 AWT 的“平移至枢轴、旋转、缩放、平移回来”惯用法),这个 API 正是你所需要的。isTranslateMatrixSupported() 在每个现代端口上都返回 true。旧的 translate(int, int) 并未弃用,也不会消失;框架一半的内部滚动代码都建立在它之上。新方法是在新的绘图代码中应该使用的方法,特别是任何将平移与缩放或旋转结合使用的情况。

String API:replace(CharSequence, CharSequence)replaceAllreplaceFirst

PR #4893 闭合了问题 #4878 中报告的一个长期存在的缺口。JDK 1.5+ 中接受 CharSequence 参数的 String.replace 重载(几乎是每一个现代 Java 教程都会用到的那个)在 Codename One 子集中缺失了。String.replaceAll(String, String)String.replaceFirst(String, String) 也同样缺失。由于这三个方法都不在引导类路径上,尝试使用它们的代码根本无法针对 Codename One 项目进行编译;你必须知道回退到较旧的 replace(char, char) 重载,并自己实现正则表达式。

现在这三个方法都已接入。String.replace(CharSequence, CharSequence)vm/JavaAPI 中有真实实现。replaceAllreplaceFirst 通过字节码合规性重写器连接到一对新的 JdkApiRewriteHelper,这些助手委托给现有的 RE 正则引擎(与我们在 String.split 上使用多年的模式相同)。新的合规性测试涵盖了这两种重写规则。

从代码行数来看,这是一个很小的改动。但在实践中,它显著减少了“我从 Stack Overflow 复制了一段代码,结果在 iOS 上不行”变成真正 bug 的频率。现代 Java 中三个最常用的 String 方法现在已成为设备端 API 的一部分。

iOS 推送权限不再在应用启动时触发

PR #4894 修复了问题 #4876。当 ios.includePush=true 时,框架以前会在 application:didFinishLaunchingWithOptions: 中调用 requestAuthorizationWithOptions,这意味着 iOS 系统权限对话框会在应用启动完成后立即弹出,用户还没有看到你的任何界面。在这种情况下,用户一旦点击“不允许”,就很难挽回。用户还没有体验过应用,不知道通知为什么重要,而点击“不允许”是最省力的路径。一旦被拒绝,再次提示就需要引导用户前往设置。

修复方案将提示移至自然触发点。Push.register() 会触发系统提示(该代码路径已经在 IOSNative.m 内部请求了权限;我们只是不再提前触发它)。LocalNotification.schedule() 也会触发它,通过在 sendLocalNotification 中添加新的 requestAuthorizationWithOptions 调用。这与 Android 多年来的流程相同。

实际结果是,你现在可以在系统对话框触发之前显示自己的理由界面(例如“我们希望在您的订单发货时通知您”)。如果你的应用需要旧有的启动时行为,一个向后兼容的构建提示可以恢复它:

codename1.arg.ios.notificationPermissionAtLaunch=true

默认值为 false,因此未选择加入的现有应用在下次重建时会采用新行为。文档位于 Push-Notifications.asciidoc

云端的构建服务器变更作为 BuildDaemon #71 提交,因此本地构建和云构建保持一致。

如果你正在更新现有的 iOS 应用,有一点需要注意:如果你的登录流程依赖启动时自动触发提示,那么现在你的提示永远不会触发,除非在代码中调用了 Push.register()LocalNotification.schedule()。这几乎肯定是你想要的,但请检查调用位置。

皮肤设计器 FAQ 跟进

在上周的皮肤设计器文章之后,讨论 #4928 中出现了一些问题,值得在这里提一提,因为它们经常以相同的形式出现:

  • 皮肤不会影响 CSS。皮肤是模拟器脚手架(设备外壳、屏幕矩形、切口、安全区域嵌入);你的 theme.css 和你的原生主题是无关的。
  • 对于已知设备,默认值通常是对的。选择设备,点击“选择形状”,点击“完成”。自定义 UI 存在的意义是当我们设备数据库不完整时(例如 iPhone 17e 条目可能写着“无刘海”,但实际上有刘海,或者刘海位置偏差几个像素);当你有一台实体设备可以测量时,你就使用自定义功能进行微调。
  • 主题正在脱离皮肤。历史上,原生主题捆绑在每个皮肤内部,因为这在当时是有意义的。未来,正确的归属是框架本身,通过 Maven 分发,这样你可以自动获得更新。新的原生主题已经以这种方式工作。每个皮肤内嵌的主题为了向后兼容而保留,皮肤设计器仍然会为你生成一个,但我们两周前推出的“原生主题”菜单是未来的方向。
  • 皮肤设计器读取的设备数据库位于 scripts/skindesigner/common/src/main/resources/devices.json,如果你发现缺少某个设备或某行的详情有误,欢迎提交 PR。

总结

两个提醒。首先,如果你还没有,请在本周内将你的真实应用切换到 ios.metal=true。默认切换开关就在这几天了,我们宁愿在你的屏幕上发现任何剩余的边缘情况,也不愿在启动那天面对所有已安装用户。

其次,如果你最近没有从 Initializr 生成过项目,试试看;Java 17 默认值和 AGENTS.md 技能都值得亲自体验。

本周特别感谢 #3302 的报告者,他长期以来一直坚持跟踪内接三角形 bug,即使 GL 是唯一目标;感谢 Durank 报告 #4876 的 iOS 推送权限问题;以及 #4878 的报告者,他指出了缺失的 String.replace(CharSequence, CharSequence);这个问题在缺口里待了很长时间。

问题跟踪器在这里PlaygroundInitializr 是尝试新默认值的最简单途径,上周的皮肤设计器仍然可用,如果你需要某个设备形状的皮肤。