在鸿蒙设备上用Flutter开发时,按钮的hover、press、disabled等状态样式是绕不开的基础需求。之前在ArkTS中,我们可以靠.hoverEffect、.pressEffect这类内置属性快速实现,或者自己用@State控状态变量。但切换到Flutter后,发现它处理按钮状态的方式完全不一样——得靠MaterialStateProperty(新版叫WidgetStateProperty)这套机制。这篇文章结合我在HarmonyOS 6.0真机(nova12u)上的实践,详细讲解WidgetStateProperty的用法、与ArkTS的差异以及踩过的坑。
一、从MaterialStateProperty到WidgetStateProperty
早期Flutter叫MaterialStateProperty,对应的枚举是MaterialState。从Flutter 3.x某个版本开始,官方将其重命名为WidgetStateProperty和WidgetState。接口完全一样,新项目直接用WidgetStateProperty.resolveWith(...)即可,老代码会报deprecated但还能跑。
二、核心用法:按钮不同状态变背景色
最常见的需求:按钮默认蓝背景,hover变浅蓝,按下变橙,disabled变灰。使用ButtonStyle配合WidgetStateProperty代码如下:- ElevatedButton(
- onPressed: _disabled ? null : () {},
- style: ButtonStyle(
- backgroundColor:
- WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.pressed)) {
- return Colors.orange;
- } else if (states.contains(WidgetState.hovered)) {
- return Colors.blue.shade200;
- } else if (states.contains(WidgetState.disabled)) {
- return Colors.grey.shade300;
- }
- return Colors.blue;
- }),
- foregroundColor:
- WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.disabled)) {
- return Colors.grey.shade500;
- }
- return Colors.white;
- }),
- ),
- child: const Text('状态变色按钮'),
- )
复制代码 关键点:判断顺序要注意,pressed必须放最前面。因为按下时states里同时包含hovered和pressed,如果hovered写在前就会覆盖pressed效果。
三、TextButton和IconButton的状态覆盖
TextButton默认无背景色,但可以用WidgetStateProperty控制前景色和字体样式:- TextButton(
- onPressed: () {},
- style: ButtonStyle(
- foregroundColor:
- WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.hovered)) return Colors.red;
- if (states.contains(WidgetState.focused)) return Colors.orange;
- return Colors.grey;
- }),
- textStyle: WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.hovered)) {
- return const TextStyle(fontSize: 18, fontWeight: FontWeight.bold);
- }
- return const TextStyle(fontSize: 14);
- }),
- ),
- child: const Text('TextButton 状态样式'),
- )
复制代码 IconButton类似,注意默认foregroundColor是onSurface,如果手动设了颜色但没处理disabled状态,可能导致disabled时不变灰。
四、完整的状态覆盖:minimumSize / shape / elevation / padding / side
状态不限于颜色,按钮的尺寸、形状、阴影、内边距、边框都可以按状态区分:- ElevatedButton(
- onPressed: () {},
- style: ButtonStyle(
- minimumSize: WidgetStateProperty.all(
- const Size(double.infinity, 48)),
- shape: WidgetStateProperty.all(
- RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12))),
- elevation: WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.pressed)) return 8;
- return 2;
- }),
- padding: WidgetStateProperty.all(
- const EdgeInsets.symmetric(horizontal: 24, vertical: 12)),
- side: WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.hovered)) {
- return BorderSide(color: Colors.blue.shade400, width: 2);
- }
- return BorderSide.none;
- }),
- ),
- child: const Text('更多自定义'),
- )
复制代码 区分:all()用于所有状态统一,resolveWith用于需要按状态区分的。注意ButtonStyle的参数类型是WidgetStateProperty<T>?,不能直接传值,必须用WidgetStateProperty.all()或resolveWith包裹。
另外,overlayColor控制涟漪颜色,建议一并设置以免背景色与默认灰色涟漪冲突:- overlayColor: WidgetStateProperty.resolveWith((states) {
- if (states.contains(WidgetState.pressed)) {
- return Colors.blue.withOpacity(0.3);
- }
- return null;
- }),
复制代码
五、与鸿蒙ArkTS状态样式的对比
在ArkTS中给按钮加状态样式主要有三种方式:
1. 使用自带效果属性:.hoverEffect(HoverEffect.HIGHLIGHT)、.pressEffect(PressEffect.SCALE)、.enabled(false)。简单但定制能力有限。
2. 自己用@State控状态变量:监听onTouch、onHover,手动维护isPressed/isHovered,再映射到样式上。与Flutter思路类似,但步骤更多。
3. 通过@Styles封装复用。
Flutter的WidgetStateProperty将状态判断逻辑嵌入ButtonStyle的属性定义中,组件渲染时自动查询当前状态并返回对应值,无需外部维护变量和setState。ArkTS则更命令式,需要你自己安排状态变更、事件监听、样式映射。
另外,Flutter的Set<WidgetState>天然支持多状态共存(如同时hovered和disabled),可以在resolveWith中分别处理。而在ArkTS中,多状态共存时只能依赖系统默认优先级,想自定义就得自己在逻辑层叠加,重复代码较多。
六、踩过的坑
1. styleFrom与ButtonStyle参数类型不匹配:styleFrom的参数是基本类型Color?,内部自动用WidgetStateProperty.all()包裹,所以无法实现按状态区分的效果。如果按钮需要多状态样式,直接使用ButtonStyle。
2. setState不会自动刷新WidgetStateProperty:onPressed从null变为有效函数后,需要确保按钮组件重建,让WidgetState集合重新收集。解决方法是将切换_disabled和setState写在同一个回调里,触发widget tree重建。
3. WidgetStateProperty不是在build时实时求值,而是在状态实际变化时才触发回调。这意味着如果回调引用了外部变量,变量变了但状态没变,回调不会重新执行。
4. Material 3下elevation不生效:M3的ElevatedButton默认使用surfaceTintColor而不是传统elevation,需要在ButtonStyle里同时设置shadowColor才能看到阴影变化。或者关闭Material 3风格。
七、WidgetStateProperty在其他组件上的应用
Checkbox的fillColor、checkColor、overlayColor;Switch的thumbColor、trackColor;Slider的activeTrackColor、inactiveTrackColor、thumbColor等都可以用WidgetStateProperty。原理相同,只是状态枚举可能不同(如Checkbox用到selected)。
八、实用写法整理
- 只有一个状态统一值:用WidgetStateProperty.all(...)。
- 两个以上状态区分:用resolveWith。
- 从Map映射可使用MaterialStatePropertyFromMap(旧名),但新版没有公开API,仍建议用resolveWith。
总结:WidgetStateProperty的设计思路与ArkTS不同,建议在Flutter代码中直接采用这套机制,放弃手动事件监听模式。简单场景下ArkTS一行代码更方便,但复杂多状态组合时Flutter更省事。在HarmonyOS上使用Flutter 3.27开发时,上述实践已验证通过。 |