Skills、Java 17 和主题强调色——Codename One 更新
目录
Java 17 默认AGENTS.md 与 Codename One 技能原生主题强调色Metal 后续更新
- 逐轴缩放分解(#4939,修复 #3302)
- 旋转下裁剪诊断(#4924,推进 #3921)
- iOS Metal 色彩空间提示(#4909,修复 #4908)
- 新的 translateMatrix API
String API:replace(CharSequence, CharSequence)、replaceAll、replaceFirstiOS 推送权限不再在应用启动时触发Skin Designer 常见问题跟进总结
[LOADING...]
上周的主题是 Metal 和 Skin Designer。本周的重点内容涉及生成全新项目时的样貌:默认 JDK 为 Java 17,每个生成的项目都会附带一个 AGENTS.md 创作技能,使任何现代 AI 代理都能智能地处理该项目。此外,还有一些值得关注的内容:新原生主题的运行时强调色板、三项 Metal 后续更新(其中一项引入了新的矩阵修正平移 API)、填补 JDK 11+ String API 的空白,以及 iOS 推送权限不再在应用启动时触发。
什么是 Codename One?
Codename One 是一个开源框架,用于从单一的 Java 或 Kotlin 代码库构建原生 iOS、Android、桌面和 Web 应用。了解更多请访问 codenameone.com。
Java 17 默认
我们将 Initializr 生成的默认项目改为 Java 17+,以便聚焦 Codename One 的未来。如果您有理由使用 Java 8,仍可从单选面板中选择该选项。请根据您的需求选择。
Java 17 路径是我们现在为新项目推荐的。生成的项目可使用任何 JDK 17 及以上版本构建(我们通常测试 21 和 25);您无需专门安装 Java 17。关于 Java 17 支持在工具链中的整体工作方式,包括哪些语言特性进入应用代码,以及 iOS/Android 端口如何处理更新的字节码,已于今年早些时候在官方实验性 Java 17 支持中介绍过。本周的变化在于默认设置和措辞:(Experimental) 标签已移除,除非您主动选择退出,否则默认使用 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.file、java.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 块内覆盖常量:
就是这样。每个使用强调色的 UIID 都会采用新颜色。浅色和深色是独立的(--accent-color vs --accent-color-dark),部分覆盖也是允许的;您未重新声明的任何内容都将保持框架默认。Material 3 还有一些额外的容器级常量用于凸起表面色调;iOS 会忽略这些常量。
还有一条用于动态主题的运行时路径(应用内强调色切换、品牌变体、A/B 测试)。它使用相同的常量。开发者指南中的“原生主题”章节对此进行了详细说明,并提供了完整的 iOS 和 Android 常量表,以及有意不应用绑定系统的位置:强调色板覆盖。
值得指出的一点是:每个应用都不需要更改的主题部分(哪些 UIID 参与强调色板、它们暴露哪些状态、它们有哪些深色模式对应项)都位于框架内部并保持不变。每个应用确实需要更改的部分(您的颜色)作为五个常量存在于您的项目中,别无其他。这完全是此更改存在的意义。
Metal 后续更新
上周是关于发布 Metal 渲染器。本周是后续更新周:三个 PR,外加 Graphics 上的一个新 API,我认为它会在未来带来丰厚回报。
逐轴缩放分解(#4939,修复 #3302)
长期存在的问题 #3302 有一个清晰的复现步骤:g.translate + g.scale(sx, sy) + fillShape 当 sx != sy 时,产生的形状会明显偏离与框架一起发出的轴对齐 drawRect 和 drawLine 调用。内接于矩形的三角形会脱离其边界矩形。
原因在于,旧版 alpha-mask 路径以统一缩放比例(对角线比率 h2/h1)光栅化形状,然后通过 GPU 矩阵不均匀地拉伸生成的纹理,以恢复请求的宽高比。边界框数学在实数上是精确的,但纹理在中间统一缩放步骤是像素四舍五入的,因此拉伸会使光栅化的形状偏离 drawRect 和 drawLine 已经所在的像素网格。
修复方法是将用户变换的 2x2 线性部分分解,取列范数作为 (sx, sy),然后在 S(sx, sy) 处光栅化路径,这样逐轴拉伸在光栅化时针对矢量路径而非像素网格进行,并且仅在 GPU 上应用剩余的 transform * S(1/sx, 1/sy)。剩余部分是纯旋转(最坏情况下为剪切),因此采样时不会发生逐轴拉伸,alpha-mask 纹理与 drawRect 兄弟像素落在同一像素网格上。
该更改限定于 Metal;GL ES2 路径保留其旧分支,因此现有的 GL 黄金图像字节完全相同。一个新的 InscribedTriangleGrid 屏幕截图测试已注册到 Cn1ssDeviceRunner,因此内接三角形属性现在可在 CI 中通过视觉验证。
旋转下裁剪诊断(#4924,推进 #3921)
PR #4924 不修复 bug,而是定位一个 bug。问题 #3921 是“旋转下裁剪在某些端口上行为错误”,与 getClip / setClip(int[]) 往返限制纠缠在一起,报告者本人也指出这是另一个问题。为了将两者分开,我们发布了一个仅使用 pushClip / popClip 和 rotateRadians 的屏幕截图测试。剪辑区域通过 30 度旋转内的 clipRect 变成非轴对齐,这迫使框架进入其多边形剪辑分支。
预期结果是一个 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(默认)、displayP3、deviceRGB、linearSRGB、extendedSRGB、extendedLinearSRGB 和 none。在 codenameone_settings.properties 中设置:
当 ios.metal=false 时,此提示处于休眠状态,因此现有的 GL 构建不受影响。无法识别的值会产生警告日志并回退到 sRGB。已记录在 Working-With-iOS.asciidoc 中。
新的 translateMatrix API
#4939 中的 Inscribed-Triangle-Grid 测试还暴露了 Graphics 中一个静默的痛点,值得单独作为一个特性提取出来。
Graphics.translate(int, int) 不像 scale() 和 rotateRadians() 那样合成到仿射变换中。它会累积到一个每 Graphics 的整数偏移中,该偏移在 impl 矩阵应用之前添加到绘制坐标中。这是框架最初版本的遗留问题,当时 Graphics 根本没有矩阵。如今,后果是令人惊讶的:后续的 g.scale(sx, sy) 也会乘上整数平移,这意味着相同的代码根据您是在平移之前还是之后缩放会产生明显不同的位置。
新的 Graphics.translateMatrix(float, float) 直接将平移合成到 impl 矩阵中,方式与 scale 和 rotateRadians 已经做的相同。结果是在 iOS(GL 和 Metal)、JavaSE、Android 和 JavaScript 端口上具有统一的“后乘平移到当前变换”语义。相同的代码,在屏幕上产生相同的位置,无论您是绘制到 Form 的 Graphics 还是可变的 Image 的 Graphics。
对于编写仿射变换管线的应用代码(来自 Java2D 和 AWT 的“平移到中心点,旋转,缩放,平移回来”惯用法),这是您想要的 API。isTranslateMatrixSupported() 在每个现代端口上都返回 true。旧的 translate(int, int) 并未弃用,也不会消失;框架一半的内部滚动代码都基于它构建。新方法是在新的绘制代码中应该使用的方法,特别是任何将平移与缩放或旋转结合使用的情况。
String API:replace(CharSequence, CharSequence)、replaceAll、replaceFirst
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 中有实际实现。replaceAll 和 replaceFirst 通过字节码兼容性重写器连接到新的 JdkApiRewriteHelper 对,该对委托给现有的 RE 正则表达式引擎(与多年来用于 String.split 的模式相同)。新的合规性测试覆盖了两种重写规则。
这是一行代码量上的小改动。实际上,它显著减少了“我从 Stack Overflow 复制了一段代码,但在 iOS 上不起作用”变成真正 bug 的频率。现代 Java 中最常用的三个 String 方法现在已成为设备端 API 的一部分。## iOS 推送权限不再在应用启动时弹出
PR #4894 修复了 issue #4876。此前,当 ios.includePush=true 时,框架会在 application:didFinishLaunchingWithOptions: 中调用 requestAuthorizationWithOptions,这意味着 iOS 系统权限对话框会在应用启动完成瞬间弹出,用户尚未看到任何界面。此时点击“不允许”几乎没有挽回余地:用户还未体验应用,不明白通知为何重要,而点击“不允许”是最省事的选择。一旦被拒绝,再次弹窗只能引导用户前往系统设置。
本次修复将弹窗移至自然的触发点。Push.register() 会触发系统弹窗(该代码路径原本已在 IOSNative.m 中请求权限,现在我们只是不再提前触发)。LocalNotification.schedule() 也会通过 sendLocalNotification 中的新 requestAuthorizationWithOptions 调用触发弹窗。这与 Android 多年来采用的流程一致。实际效果是,你现在可以在系统弹窗之前展示自己的说明界面(例如“我们想在订单发货时通知您”)。
如果你的应用需要保留旧的启动时行为,可以通过以下编译提示还原向后兼容性:
默认值为 false,因此未进行设置的应用在下一次重构时会自动采用新行为。相关文档已更新至 Push-Notifications.asciidoc。服务端构建修改已随 BuildDaemon #71 发布,因此本地构建与云端构建保持一致。
需要注意:如果你正在更新现有的 iOS 应用,且之前的引导流程依赖于启动时自动弹窗,那么现在除非在某处调用了 Push.register() 或 LocalNotification.schedule(),否则弹窗将不再触发。这很可能正是你需要的,但请确认调用位置正确。
主题设计器常见问题后续
上周关于主题设计器的帖子引发了讨论 #4928,其中几个问题值得在此详细说明,因为它们经常以类似形式出现:
- 主题不影响 CSS。 主题(skin)是模拟器外壳(设备边框、屏幕矩形、刘海屏、安全区域),与你的
theme.css及原生主题无关。 - 对于已知设备,默认值通常正确。 选择设备,点击“选取形状”,然后点击“完成”。自定义界面的作用是在我们的设备数据库不完整时进行调整(例如 iPhone 17e 条目可能显示“无刘海”,但实际上有,或者刘海位置偏离几个像素);当你拥有物理设备可以进行对比时,可以在此处进行微调。
- 主题正在脱离皮肤。 历史上,原生主题被打包在每个皮肤中,因为当时这样做是合理的。未来,主题的正确归宿是框架本身,通过 Maven 分发,这样你可以自动获取更新。新的原生主题已经采用此方式。每个皮肤内嵌的主题为了向后兼容仍然保留,主题设计器也会继续为你生成一个,但两周前我们发布的“原生主题”菜单才是未来的方向。
主题设计器读取的设备数据库位于 scripts/skindesigner/common/src/main/resources/devices.json,如果你希望提交 PR 添加我们遗漏的设备或修正某行的详细信息,欢迎参与。
总结
两点提醒。首先,如果你还没有,本周请在真实应用上启用 ios.metal=true。默认启用已近在眼前,我们宁愿在发布当天之前在你的界面上发现任何剩余边缘情况,而不是在安装基数上。其次,如果你最近没有从 Initializr 生成项目,请尝试一下;Java 17 默认设置和 AGENTS.md 技能都值得亲身体验。
本周特别感谢 #3302 的举报者,在 GL 还是唯一目标时一直坚持内切三角形 bug;感谢 Durank 在 #4876 中报告 iOS 推送权限问题;以及 #4878 的举报者,指出了缺失的 String.replace(CharSequence, CharSequence) 方法;该问题已经存在很长时间了。
问题跟踪器在这里,Playground 和 Initializr 是体验新默认配置的最佳场所,上周的主题设计器仍然可用,如果你需要为某个设备形状创建皮肤的话。
这篇文章《Skills, Java 17, And Theme Accents with Codename One》首次出现在 foojay 上。