液态玻璃、Material 3 与大量底层工作
这是一周来改动量远大于标题宣称的那一周。标题很短——Codename One 现在搭载了现代原生主题:iOS 的“液态玻璃”外观和 Android Material 3 外观,已集成到 iOS 和 Android 端口,在 Playground 中默认启用,并可通过模拟器中的全新菜单选择。标题背后的改动跨越数千行代码,涉及平台端口、模拟器、GUI 管道以及一大波截图测试。这些工作的主题很简单:Codename One 在每个我们支持的平台上都应该开箱即用,看上去现代且响应迅速。过去一周的提交几乎全部服务于这两个目标之一。
立即在 Playground 中体验
查看这些改动最简便的方式是 Playground。Playground 现在在设备切换为 iPhone 时默认使用 iOS Modern 主题,切换为 Android 时默认使用 Android Material 3 主题,同时支持浅色和深色模式。无需设置,无需 pom.xml,无需构建提示——只需打开页面,放入任意标准组件,就能看到现代外观。如果你觉得 Codename One 过去发布的版本看起来过时,那么 Playground 就是你的起点。
Video 10Audio 5Audio 6访问广告商网站GO TO PAGE
模拟器是第二简单的尝试方式。我们稍后会介绍。
全新原生主题
在 Codename One 的大部分生命周期中,iOS 原生主题是经典的 iOS 7 扁平主题,Android 原生主题是 Holo Light。这两者仍然保留——向后兼容一直是我们最重要的目标之一——但它们不再是新应用的起点。本周我们花费了大部分精力构建了两个面向当前平台美学的新主题:
- iOS Modern —— Apple 系统颜色(强调色
#007aff浅色 /#0a84ff深色,分组表单表面,系统分隔符调色板),选项卡采用药丸边框,iOS 设置风格的MultiButton,CHECK_CIRCLE风格的复选框图示,以及Dialog和TabsContainer的半透明表面,使其看起来像磨砂玻璃覆盖在背景之上。它并非真正的UIVisualEffectView背景——那是我们尚未构建的端口侧原语——但其视觉风格比我们以往发布的任何版本都更接近 iOS 26 的气质。 - Android Material 3 —— Material 3 基准色调调色板(主色调
#6750a4浅色 /#d0bcff深色,表面容器层级,因实际的投影阴影仍在待办清单上,所以通过色调近似实现抬高容器),以及所有 Material 的密度和内边距选择——仿 Roboto 的比例,带底色下划线的顶部标签栏,标准方形复选框图示。
每个主题覆盖了大约 25 个 UIID:基础组件(Component、Form、ContentPane、Container)、排版(Label、SecondaryLabel、TertiaryLabel、SpanLabel*)、按钮(Button、RaisedButton、FlatButton 包含 .pressed 和 .disabled)、文本输入、选择控件、工具栏、标签页、侧边菜单、列表、MultiButton、对话框/面板、FAB,以及所有支撑的分隔线和弹出部件。两个主题都完整支持浅色和深色模式。
提供源码的 CSS 位于仓库中:native-themes/ios-modern/theme.css 和 native-themes/android-material/theme.css,任何想了解每个 UIID 具体样式的人都可以查阅。
iOS Modern
[LOADING...]
这是新截图套件中的 ShowcaseTheme 捕获,在 iOS 上运行于浅色和深色模式。相同的表单,相同的组件,切换 Display.setDarkMode(...) 并重新解析。表单的构建方式如下:
单个屏幕上即可看到全貌:
Default按钮使用默认的ButtonUIID。Raised按钮使用RaisedButton,它通过cn1-derive继承自Button,并在 iOS 系统蓝色之上添加了一层带有色调的药丸形状——这就是 iOS Modern 在两种模式下的强调色。TextField是一个带 iOS 系统灰色填充的圆角矩形表面,与 Apple 在设置应用中使用的形状相同。CheckBox和RadioButton使用新的可选主题常量@checkBoxCheckedIconInt/@radioCheckedIconInt切换到CHECK_CIRCLE/CHECK_CIRCLE_OUTLINE图示——iOS 上呈现提醒事项应用美学,而 Android 保持标准方形复选。SpanLabel正文使用主题的基础字体,并继承透明背景,因此不会覆盖在半透明父组件之上。
完整源码见 DarkLightShowcaseThemeScreenshotTest.java。
Android Material 3
[LOADING...]
Android 上相同的 ShowcaseTheme 源码。Material 3 基准调色板为 Default 提供了主容器颜色,为 Raised 提供了抬高表面色调,深色变体通过深色角色映射正确翻转了关系。内边距和字体大小遵循 Material 密度,可以从同一表单布局在 Android 上比 iOS 更紧凑这一点看出。
半透明表面
[LOADING...]
这是截图套件中针对纹理对角条纹背景的 DialogTheme 捕获。背景是有意设置的——让审查者能够看到应该半透明的组件是否真的半透明。iOS Modern 的 Dialog 使用 rgba 表面填充(浅色模式下 alpha 0.78,深色模式下 0.95——深色需要更高不透明度,因为亮色条纹会透出),其 DialogBody、DialogTitle、ContentPane、CommandArea 子 UIID 都是透明的,因此圆角看起来干净利落。相同的技巧也应用于 TabsContainer 和 iOS 的 MultiButton。
运行时调色板覆盖
[LOADING...]
原生主题旨在作为起点——你可以在不分支主题的情况下在其上叠加自己的调色板。上图是 PaletteOverrideTheme 捕获:基础是 iOS Modern,但测试在运行时通过 UIManager.addThemeProps(...) 叠加了洋红色调色板。RaisedButton、FlatButton、禁用色调以及正文副本 span 在浅色和深色模式下都应用了覆盖——覆盖衔接在资源包层工作,与用户主题覆盖真实应用中原生主题的机制完全相同。
在模拟器中
三部分功能均已在模拟器中生效:
- 主题已捆绑。模拟器的 jar-with-dependencies 包含了两个现代主题以及四个旧版主题(
iPhoneTheme、iOS7Theme、androidTheme、android_holo_light),位于 jar 的根目录。模拟器可以在运行时选择任意一个主题,而无需接触皮肤仓库。 - 新增“原生主题”菜单。在皮肤菜单旁边,现在有一个原生主题菜单,包含六个主题的单选组,以及“自动”和“使用皮肤内嵌主题”选项。选择其中一个会写入
simulatorNativeTheme首选项,设置模拟器重载标志,并销毁当前窗口,使皮肤重载器以新主题启动。你可以固定一个皮肤,在数秒内切换所有原生主题。 - 构建提示已识别。新的构建提示
nativeTheme、ios.themeMode和and.themeMode在启动时已在模拟器的构建提示 UI 中注册——包括标签、类型、值列表、描述等所有信息。(旧版键cn1.nativeTheme和cn1.androidTheme仍为向后兼容保留。)你可以在构建提示对话框、codenameone_settings.properties或通过-D系统属性设置它们;它们会流式传递到设备构建和模拟器。原生主题菜单中的“自动”选项会遵循这些构建提示——在你的项目设置中设置ios.themeMode=modern,“自动”会预览 iOS Modern;将同一项目切换为ios.themeMode=ios7,“自动”会预览 iOS 7。显式菜单条目(iOS Modern、iOS 7 等)会覆盖提示。-Dcn1.forceSimulatorTheme仍然作为最高优先级覆盖被支持;选择“使用皮肤内嵌主题”则完全绕过框架主题,直接使用皮肤自带的主题。
在设备上
在 iOS 和 Android 上,选择加入的方式相同。平台开关遵循单一命名模式——ios.themeMode 和 and.themeMode——并接受以下值:iOS 上为 modern / liquid / auto / ios7 / flat,Android 上为 modern / material / auto / hololight / legacy。还有一个跨平台快捷方式 nativeTheme=modern,当 ios.themeMode 未设置时,iOS 构建器会参考它,而 Android 端口在运行时将其作为 and.themeMode 的默认值。旧版别名 cn1.androidTheme 和 cn1.nativeTheme 仍为向后兼容保留,and.hololight=true 也是如此。现有应用的默认值在所有平台上保持旧版主题。我们不会在未选择加入的情况下改变一个已有 15 年历史的应用的外观。从初始化工具生成的新应用会预先在 codenameone_settings.properties 中设置 nativeTheme=modern、ios.themeMode=modern 和 and.themeMode=modern,因此全新项目会从现代主题开始。Playground 也是如此,Playground 项目下载会将这些默认值带入生成的 codenameone_settings.properties 中。HTML5 端口具有现代主题的运行时支持,但尚未将其与用户应用捆绑——这是我们在下一轮迭代中希望完成的工作之一。
粘性标题
我们还想强调的另一个视觉功能是 StickyHeaderContainer,它终于在框架中有了一个合适的位置。这是 iOS 通讯录列表 / 分段素材列表组件:滚动经过一个分节边界时,前一个标题会被下一个标题替换。本周新增了动画交换效果。向前滚动时,当前分区标题会向上滑动;向后滚动时则向下滑动,你也可以选择交叉淡入淡出。
[LOADING...]
上图是截图测试中的六帧扫描——用户滚动经过分区 A、B、C、D、E,固定标题会根据当前视口顶部的活动分区重新着色。API 很小。构建容器,使用 addSection(header, content) 注册分区,配置过渡样式和持续时间,然后将其添加到表单中:
TRANSITION_SLIDE 是默认值。TRANSITION_FADE 在传出标题之上交叉淡入传入标题。TRANSITION_NONE 保留之前的瞬间切换。相关 issue:#4807。
如何测试
本文中的每一张截图都是由测试捕获的,这些测试在真实 iOS 设备、Android 模拟器和无头 Chrome 上运行应用,然后将每个捕获与存储的金色图像进行对比。差异本身就是测试——如果渲染像素偏离,运行就会失败。对于动画,测试会在固定持续时间的过渡期间抓取一系列帧,然后将它们合成到一张索引图像中。这就是为什么每项测试的双重外观截图最终会并排显示:
[LOADING...]
以及为什么粘性标题动画会以六帧条带拼接成 GIF:
[LOADING...]
如果你希望阅读源码,测试套件位于 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/。
本周错误和杂项功能
主题工作是本周最引人注目的部分,但还有大量其他提交一同落地:
- SIMD 大分配回退。iOS 上的 SIMD 路径为了速度通过
alloca在栈上分配工作缓冲区。当缓冲区大小超过一定限度时,栈分配会失败——没有足够的栈空间可用,导致进程崩溃。修复方案检测到这种情况,并在请求过大时回退到常规堆分配。小 SIMD 操作保留快速的alloca路径;大操作不再崩溃。 - 可插拔 AnimationTime 时钟。
Motion、Timeline、MorphAnimation、Image.animate和Label的滴答现在都通过一个新的AnimationTime类路由,默认使用System.currentTimeMillis(),但可以被覆盖。测试可以确定地逐帧驱动动画;演示可以慢放或快进;Motion.slowMotion不再是唯一的工具。 - 非 ASCII 字母的 POSIX 字符类。
[[:alpha:]]、[[:alnum:]]、[[:lower:]]和[[:upper:]]此前静默无法匹配基本 ASCII 范围之外的任何字符——希腊字母、西里尔字母、中日韩统一表意文字、重音字母、普通分数、货币符号。现在它们按预期匹配,并包含五个回归测试覆盖 issue 中的失败情况。 - JDK < 11 时快速失败。模拟器和“作为桌面应用运行”目标会使用
--add-exports=java.desktop/com.apple.eawt=ALL-UNNAMED分支 JVM,JDK 8 会拒绝此参数并显示无帮助的“无法创建 Java 虚拟机”。现在 Maven 插件在进入cn1:run和cn1:debug时会检查运行时 JDK 版本,并友好地中止,显示检测到的版本、JAVA_HOME以及指向 Adoptium 的链接。JDK 11 到 25 是模拟器支持的运行时范围,JDK 8 仍然是核心框架的构建时需求,JDK 8 在运行时仍然完全支持已发布桌面应用——只有模拟器 / “作为桌面应用运行” Maven 目标需要 JDK 11+。 - Sheet 滚动、滑动和动画。
Sheet终于可以从底部拖动并带有真正的动画,而不是直接弹跳。Issue #4825。 - Picker 定位。
Picker获得了额外的按钮定位选项和一小批覆盖测试。 - Playground 优化。Playground 将所有
Dialog.show(...)调用改为InteractionDialog模式,因此用户代码调用Dialog.show不会覆盖编辑器界面——它会渲染到分层窗格中。错误消息得到了大幅改进。预览解析语法得到扩展,使 Playground 能够从更广泛的表达式中选择预览,并附带新工具在 CI 中保持诚实。 - 更深入的
refreshTheme()。Form.refreshTheme()已经存在很久——它重新解析单个表单上的样式。本周新增的是UIManager.getInstance().refreshTheme(),它会快照当前主题属性 和 主题常量,清除已解析样式缓存,然后重新应用全部内容。这正是截图套件能够在套件中途切换深色模式并看到新样式的原因,也是运行时调色板覆盖能够立即生效的原因。大多数应用永远不需要直接调用它——调色板通常不会在运行时更改,而Display.setDarkMode(...)调用已经触发了正确的失效。如果你确实更改了调色板并希望更改在下次绘制时生效而无需从磁盘重新加载主题,则可以使用此方法。
未来方向——以及感谢
上周的文章 是关于 Codename One _感觉_更快:修正的像素密度、原则性的滚动物理、iOS 上的 SIMD 以及辅助功能文本缩放。本周是共生型的另一半——Codename One _看起来_像属于 2026 年的手机。这两半是同一个项目。如果它周围的 UI 看起来像 2014 年的应用,那么提供一个 SIMD 加速的 Base64 意义不大;如果它下方的滚动结结巴巴,那么提供一个磨砂玻璃效果的 Dialog 意义也不大。这两半都尚未完成。它们都在进行中,并且都依赖于社区的帮助——错误报告、RFE(功能请求)、在 issue 线程上耐心的来回讨论,有人描述你未拥有的 iPhone 上的布局问题。
特别感谢那些推动了本周提交相关 issue 的人们:Thomas (@ThomasH99) 提出了 #4781(最初的“构建液态玻璃示例”RFE,启动了整个工作)、#4807(粘性标题)、#4838(侧向选项卡滑动)、#4841(POSIX 正则表达式修复)、#4819(选择器按钮)以及其他几个;Francesco Galgani (@jsfan3) 提出了 #4825(Sheet 滑动动画)和 #4824(初始工具默认浅色+深色主题);@ddyer0 发现了 #4811(EDT 栈溢出)和 #4767(iPad 重启表单大小);Lucca Biagi (@LuccaPrado) 提出了 #4817(IntelliJ 中的表单创建)。其中几个是 RFE,除非你每天都在使用框架,否则不会提出,而这种反馈正是可以转化为可发布工作的东西。
截至本文发布,我们拥有 496 个未解决的 issue。这是缓慢但稳定的进展——这个数字每周都在朝正确的方向移动,而且关闭的 issue 往往以你可以看到的功能或修复形式发布,而不是默默地分类。如果你遇到问题,请提出 issue。如果你有功能请求,也请提出。你上面看到的主题就是从 RFE 开始的。
你可以通过打开 Playground、在项目的 codenameone_settings.properties 中设置 nativeTheme=modern(或更精细地设置 ios.themeMode=modern / and.themeMode=modern),或者从模拟器的新原生主题菜单中选择,来试用新主题。从初始工具生成的新项目已经默认启用它们。提供正式资源的文件已在本周捆绑到 iOS 和 Android 端口中。