- 文章信息 -

Flutter笔记:谈Material状态属性-为什么FlatButton等旧版按钮就废弃了-LMLPHP



1. 概述

在 Flutter 中,MaterialState 是一个枚举,用于表示 Material 组件在用户交互时的不同状态。这些状态包括:悬停(hovered)、聚焦(focused)、按压(pressed)、拖动(dragged)、选中(selected)、滚动覆盖(scrolledUnder)、禁用(disabled)和错误(error)。这些状态帮助开发者根据组件的不同交互状态来调整其视觉表现。
MaterialStateProperty 简介
MaterialStateProperty 是一个接口,它允许开发者根据组件的状态来解析出不同的值。这是一种非常灵活的机制,用于在组件的不同状态下提供不同的视觉反馈。例如,按钮在正常状态下可能显示一种颜色,在按下时显示另一种颜色。
本文将…

2. 实际问题背景

在Flutter的早期版本中,开发者在使用如FlatButton(现已废弃)等部件时,常常面临一个问题:如何通过参数传递来确保UI组件在不同状态下具有恰当的视觉表现。例如,一个按钮可能需要在不同的状态(如正常、禁用、聚焦、高亮等)下显示不同的颜色和边框样式。传统的方法是为每种状态提供一个对应的颜色参数:

FlatButton(
  color: Color,
  disabledColor: Color,
  focusColor: Color,
  highlightColor: Color,
  textColor: Color,
  disabledTextColor: Color,
  focusTextColor: Color,
  highlightTextColor: Color,
  borderColor: Color,
  disabledBorderColor: Color,
  focusBorderColor: Color,
  highlightBorderColor: Color,
)

这种方法不仅使得组件的构造函数参数冗长,而且难以管理。随着状态种类的增加,参数数量也成倍增长,这使得维护和扩展变得复杂和困难。此外,即使提供了多达十二个或更多的参数,开发者仍然可能无法精确控制组件在所有可能的状态组合下的表现。例如,如果需要处理悬停和聚焦状态的颜色组合,传统方法很难灵活应对。

这种方法的根本问题在于,它试图通过静态值来定义一个本质上依赖于运行时状态的属性。每当组件的状态改变时,都需要重新构建整个组件来反映这些变化,这不仅效率低下,而且在实际应用中很难适应复杂的用户交互需求。

Flutter 1.20版本引入了MaterialStateProperty,这标志着对这一问题的根本性改进。MaterialStateProperty不再要求为每个状态单独传递静态颜色值,而是接受一个函数,这个函数基于组件当前的状态集合(如悬停、聚焦等)动态解析出相应的值。这种方法不仅简化了参数列表,提高了代码的可读性和可维护性,而且极大增强了组件在不同状态下的自定义能力和灵活性。

通过使用MaterialStateProperty,开发者可以编写更加简洁和强大的代码,实现在组件状态变化时动态调整其视觉表现,而无需重新构建组件。这种方法更符合Flutter的响应式编程范式,使得状态管理变得更加直观和高效。

3. MaterialState和MaterialStateProperty

MaterialState 枚举和 MaterialStateProperty 接口在Flutter中的应用展示了如何有效地利用状态管理来增强用户界面的交互性和视觉吸引力。通过这些工具,开发者可以更容易地实现复杂的状态依赖行为,同时保持代码的清晰和可维护性。这种状态驱动的设计方法不仅提高了开发效率,也为最终用户提供了更流畅和直观的交互体验。

3.1 MaterialStates

MaterialStates是Material语言定义的一种交互状态,该集合包括hovered、pressed、focused和disabled以及其它隐性状态,如Material中引入的scrolledUnder。在Flutter中他们都被实现为简单的枚举(material_state.dart中)。

在Material设计中(参考https://m2.material.io/design/interaction/states.html或者https://m3.material.io/foundations/interaction/states/overview),些状态反映了用户与Material组件的交互。MaterialStates 是 Material 设计语言中定义的一系列交互状态,这些状态包括 hovered(悬停)、pressed(按压)、focused(聚焦)、disabled(禁用)以及其他隐性状态如 scrolledUnder。这些状态被实现为 MaterialState 枚举类型,存储在 material_state.dart 文件中。

MaterialState 枚举包含以下值:

这些状态枚举使得开发者可以根据用户与组件的交互来调整组件的视觉表现。例如,一个按钮在被按压时可能需要显示不同的颜色,或者在被禁用时显示灰色。以下是一个简单的函数示例,展示了如何根据不同的 MaterialState 来动态调整颜色:

Color? getColor(Set<MaterialState> states) {
  const Color defaultColor = Colors.blue; // 默认颜色
  const Color pressedColor = Colors.blueAccent; // 按压时的颜色
  const Color hoveredColor = Colors.lightBlue; // 悬停时的颜色
  const Color focusedColor = Colors.blueGrey; // 聚焦时的颜色
  const Color disabledColor = Colors.grey; // 禁用时的颜色

  if (states.contains(MaterialState.pressed)) {
    return pressedColor;
  } else if (states.contains(MaterialState.hovered)) {
    return hoveredColor;
  } else if (states.contains(MaterialState.focused)) {
    return focusedColor;
  } else if (states.contains(MaterialState.disabled)) {
    return disabledColor;
  }

  return defaultColor; // 如果没有特定状态,返回默认颜色
}

这种方法虽然在一定程度上解决了状态管理的问题,但在处理多个状态组合或更复杂的状态逻辑时,代码会变得冗长且难以维护。为了提供更灵活的解决方案,Flutter引入了 MaterialStateProperty,这是一个能够根据组件当前的状态集合动态解析属性值的接口。在下一节中,我们将详细介绍 MaterialStateProperty 的使用和优势。

3.2 MaterialStateProperty

在Flutter中,MaterialState 的应用非常广泛,它更多地是通过与 MaterialStateProperty 接口与各种属性(如颜色、边框、文本样式等)结合使用,允许开发者定义基于状态的动态变化。这种设计使得组件可以根据其交互状态显示不同的视觉样式,而无需为每种状态编写大量的条件代码。那么 什么是MaterialStateProperty呢。

MaterialStatePropertyFlutter 中一个接口,它为开发者提供了一种高度灵活的机制,用于根据组件的交互状态动态解析属性值。这种机制在多种 Material 组件中得到应用,特别是在那些需要根据用户交互改变视觉表现的组件中,如按钮、文本字段、滑块等。

MaterialStateProperty 的主要功能是允许开发者为一个属性定义多个状态依赖的值。这意味着开发者可以为组件在不同的交互状态(如悬停、聚焦、按压等)下设置不同的视觉样式,而无需为每种状态单独更新组件。这种方法提高了代码的可维护性和扩展性,同时保持了代码的简洁性。

MaterialStateProperty 是一个泛型接口,它可以用于不同类型的属性,如颜色、尺寸、边框等。它的核心是 resolve 方法,该方法接受一个包含当前组件状态的 Set,并返回对应状态下的属性值。

开发者可以通过两种主要方式使用 MaterialStateProperty

  1. 直接实现:创建一个 MaterialStateProperty 的子类,并实现 resolve 方法。这种方式适用于需要高度定制的场景,可以根据组件的具体需求来精细控制状态变化的逻辑。

  2. 使用工厂方法MaterialStateProperty 提供了 resolveWith 工厂方法,允许直接传入一个回调函数来定义如何根据状态集解析值。这种方式简洁方便,适用于大多数常见的需求。

以下是一个使用 MaterialStateProperty.resolveWith 方法来定义一个按钮的背景颜色的示例:

TextButton(
  style: ButtonStyle(
    backgroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        return Colors.blue; // 按下状态下的颜色
      } else if (states.contains(MaterialState.disabled)) {
        return Colors.grey; // 禁用状态下的颜色
      }
      return Colors.red; // 默认颜色
    }),
  ),
  onPressed: () {},
  child: Text('点击我'),
)

在这个示例中,按钮的背景颜色根据其状态动态解析:

  • 当按钮被按下时显示蓝色;
  • 当禁用时显示灰色;
  • 其余情况显示红色。

例如,按钮组件可以通过 MaterialStateProperty.resolveWith 方法,根据不同的状态(如按下、悬停、禁用)解析出不同的背景颜色。这种方法不仅简化了代码,提高了可维护性,而且增强了用户界面的响应性和交互性。

ButtonStyle style = ButtonStyle(
  backgroundColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
    if (states.contains(MaterialState.pressed))
      return Theme.of(context).colorScheme.primary.withOpacity(0.5);
    else if (states.contains(MaterialState.disabled))
      return Colors.grey;
    return Theme.of(context).colorScheme.primary; // 默认颜色
  }),
);

通过这种方式,MaterialState 不仅定义了一组标准的交互状态,还提供了一种高效的机制来根据这些状态动态调整组件的属性。这种灵活性是Flutter框架设计中的一大优势,它允许开发者以声明式的方式处理状态变化,而不是依赖于繁琐的逻辑判断或多个参数设置。

4. MaterialStatePropertyAll<T> 类

4.1 功能描述

MaterialStatePropertyAll<T> 是一个便利类,用于创建一个 MaterialStateProperty,该属性对所有状态解析为给定的值。这个类特别适用于那些属性值在不同状态下保持不变的场景,简化了状态管理的复杂性。

4.2 实现细节与使用方式

MaterialStatePropertyAll<T> 实现了 MaterialStateProperty<T> 接口,其主要功能是无论组件处于何种状态,都返回初始化时指定的固定值。这种实现方式适用于那些不需要根据状态改变属性值的场景。

class MaterialStatePropertyAll<T> implements MaterialStateProperty<T> {
  final T value;

  const MaterialStatePropertyAll(this.value);

  @override
  T resolve(Set<MaterialState> states) => value;

  @override
  String toString() {
    if (value is double) {
      return 'MaterialStatePropertyAll(${debugFormatDouble(value as double)})';
    } else {
      return 'MaterialStatePropertyAll($value)';
    }
  }
}

MaterialStateProperty.resolveWith 等其他 MaterialStateProperty 实现相比,MaterialStatePropertyAll<T> 提供了一种更简单、更直接的方式来处理不依赖状态变化的属性值。当你需要一个属性在所有状态下都保持一致时,使用 MaterialStatePropertyAll<T> 是一个理想的选择,而 MaterialStateProperty.resolveWith 更适合那些需要根据不同状态动态改变值的场景。

4.3 代码示例

下面是如何在不同组件中使用 MaterialStatePropertyAll<T> 来简化状态管理的示例:

ButtonStyle flatButtonStyle = ButtonStyle(
  backgroundColor: MaterialStatePropertyAll(Colors.blue),
  foregroundColor: MaterialStatePropertyAll(Colors.white),
);

TextButton(
  style: flatButtonStyle,
  onPressed: () {},
  child: Text('Always Blue'),
)

在这个示例中,无论按钮处于何种状态(如按下、悬停、禁用等),背景色和前景色始终保持蓝色和白色。这种方式简化了代码,并确保了界面的一致性。

5. MaterialStateColor

5.1 功能描述

MaterialStateColor 用于定义依赖于组件状态的颜色。这个类实现了 MaterialStateProperty<Color> 接口,允许颜色根据组件的不同状态(如悬停、聚焦、按压等)动态变化。这为开发者提供了一种灵活的方式来调整组件在不同交互状态下的视觉表现。

MaterialStateColor 适用于任何需要根据状态改变颜色的场景,例如按钮的背景色、文本颜色或边框颜色。它特别适用于那些对视觉反馈有高要求的交互元素,如表单控件、导航项和交互式列表项。

5.2 实现细节与使用方式

abstract class MaterialStateColor extends Color implements MaterialStateProperty<Color> {
  /// 抽象常量构造函数。这个构造函数使得子类可以提供
  /// 常量构造函数,以便它们可以在常量表达式中使用。
  const MaterialStateColor(super.defaultValue);

  /// 从 [MaterialPropertyResolver<Color>] 回调函数创建一个 [MaterialStateColor]。
  ///
  /// 如果作为普通颜色使用,则在默认状态下(空状态集)解析的颜色将被使用。
  ///
  /// 给定的回调参数必须在默认状态下返回一个非空颜色。
  static MaterialStateColor resolveWith(MaterialPropertyResolver<Color> callback) => _MaterialStateColor(callback);

  /// 返回一个 [Color],用于当 Material 组件处于指定状态时使用。
  @override
  Color resolve(Set<MaterialState> states);
}

要使用 MaterialStateColor,你可以通过两种方式:

  1. 直接实现:创建一个 MaterialStateColor 的子类,并实现其 resolve 方法。这种方式允许完全自定义状态逻辑。

  2. 使用工厂方法:通过 MaterialStateColor.resolveWith 方法,传入一个回调函数来定义状态和颜色的关系。

5.3 代码示例

下面是一个使用 MaterialStateColor.resolveWith 方法来定义按钮背景颜色的示例:

ButtonStyle style = ButtonStyle(
  backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.pressed)) {
      return Colors.deepPurple; // 按下状态下的颜色
    } else if (states.contains(MaterialState.hovered)) {
      return Colors.deepPurpleAccent; // 悬停状态下的颜色
    }
    return Colors.purple; // 默认颜色
  }),
);

TextButton(
  style: style,
  onPressed: () {},
  child: Text('Press Me'),
)

6. MaterialStateBorderSide

6.1 功能描述

MaterialStateColor 是一个抽象类,用于定义依赖于组件状态的颜色。这个类实现了 MaterialStateProperty<Color> 接口,允许颜色根据组件的不同状态(如悬停、聚焦、按压等)动态变化。这为开发者提供了一种灵活的方式来调整组件在不同交互状态下的视觉表现。

6.2 实现细节与使用方式

MaterialStateColor 是一个抽象类,需要通过继承并实现其 resolve 方法来使用,或者通过 resolveWith 静态方法创建,传入一个回调函数来定义状态和颜色的关系。

abstract class MaterialStateColor extends Color implements MaterialStateProperty<Color> {
  const MaterialStateColor(super.defaultValue);

  /// 从回调函数创建 MaterialStateColor。
  static MaterialStateColor resolveWith(MaterialPropertyResolver<Color> callback) => _MaterialStateColor(callback);

  @override
  Color resolve(Set<MaterialState> states);
}

6.3 代码示例

下面是一个使用 MaterialStateColor.resolveWith 方法来定义按钮背景颜色的示例:

ButtonStyle style = ButtonStyle(
  backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.pressed)) {
      return Colors.deepPurple; // 按下状态下的颜色
    } else if (states.contains(MaterialState.hovered)) {
      return Colors.deepPurpleAccent; // 悬停状态下的颜色
    }
    return Colors.purple; // 默认颜色
  }),
);

TextButton(
  style: style,
  onPressed: () {},
  child: Text('Press Me'),
);

在这个示例中,按钮的背景颜色根据其状态动态解析:当按钮被按下时显示深紫色,悬停时显示深紫色加亮,其余情况显示普通紫色。

7. MaterialStateMouseCursor

7.1 功能描述

MaterialStateMouseCursor 用于定义依赖于组件状态的鼠标光标样式。这个类实现了 MaterialStateProperty<MouseCursor> 接口,允许鼠标光标根据组件的不同状态(如悬停、聚焦、按压等)动态变化。这为开发者提供了一种灵活的方式来调整组件在不同交互状态下的视觉表现和用户反馈。

MaterialStateMouseCursor 适用于任何需要根据状态改变鼠标光标的场景,例如按钮、链接或其他交互式元素。它特别适用于那些对用户交互反馈有高要求的元素。

7.2 实现细节与使用方式

MaterialStateMouseCursor 是一个抽象类,需要通过继承来实现。开发者可以通过以下两种方式使用它:

  • 直接实现:创建一个 MaterialStateMouseCursor 的子类,并实现其 resolve 方法。这种方式允许完全自定义状态逻辑。

  • 使用工厂方法:通过 MaterialStateMouseCursor.resolveWith 方法,传入一个回调函数来定义状态和鼠标光标的关系。

7.3 代码示例

下面是一个使用 MaterialStateMouseCursor.resolveWith 方法来定义按钮鼠标光标样式的示例:

ButtonStyle style = ButtonStyle(
  mouseCursor: MaterialStateMouseCursor.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.hovered)) {
      return SystemMouseCursors.click; // 悬停状态下的光标
    } else if (states.contains(MaterialState.pressed)) {
      return SystemMouseCursors.grabbing; // 按下状态下的光标
    }
    return MouseCursor.defer; // 默认光标
  }),
);

TextButton(
  style: style,
  onPressed: () {},
  child: Text('Hover or Press'),
)

在这个示例中,按钮的鼠标光标根据其状态动态解析:当按钮被悬停时显示点击光标,被按下时显示抓取光标,其余情况使用默认光标。这种方式简化了代码,并确保了界面的交互性和一致性。

Flutter笔记:谈Material状态属性-为什么FlatButton等旧版按钮就废弃了-LMLPHP
插播一张作者头像,禁止盗用

8. MaterialStateOutlinedBorder

8.1 功能描述

MaterialStateOutlinedBorder 是一个抽象类,用于定义依赖于组件状态的边框样式。这个类实现了 MaterialStateProperty<OutlinedBorder> 接口,允许边框样式根据组件的不同状态(如悬停、聚焦、按压等)动态变化。这为开发者提供了一种灵活的方式来调整组件在不同交互状态下的视觉表现。

MaterialStateOutlinedBorder 适用于任何需要根据状态改变边框样式的场景,例如按钮的边框、文本字段的边框或任何其他装饰性边框。它特别适用于那些对视觉反馈有高要求的交互元素,如表单控件、导航项和交互式列表项。

8.2 实现细节与使用方式

MaterialStateOutlinedBorder 是一个抽象类,要使用它,你需要通过以下两种方式之一:

  1. 直接实现:创建一个 MaterialStateOutlinedBorder 的子类,并实现其 resolve 方法。这种方式允许完全自定义状态逻辑。

  2. 使用工厂方法:通过 MaterialStateOutlinedBorder.resolveWith 方法,传入一个回调函数来定义状态和边框样式的关系。

8.3 代码示例

下面是一个使用 MaterialStateOutlinedBorder.resolveWith 方法来定义按钮边框样式的示例:

ButtonStyle style = ButtonStyle(
  shape: MaterialStateOutlinedBorder.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.pressed)) {
      return RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
        side: BorderSide(color: Colors.deepPurple, width: 2),
      ); // 按下状态下的边框样式
    } else if (states.contains(MaterialState.hovered)) {
      return RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
        side: BorderSide(color: Colors.deepPurpleAccent, width: 2),
      ); // 悬停状态下的边框样式
    }
    return RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(10),
      side: BorderSide(color: Colors.purple, width: 1),
    ); // 默认边框样式
  }),
);

TextButton(
  style: style,
  onPressed: () {},
  child: Text('Press Me'),
)

在这个示例中,按钮的边框样式根据其状态动态解析:当按钮被按下时显示更粗的深紫色边框,悬停时显示深紫色调的边框,其余情况显示普通的紫色边框,悬停时显示深紫色调的边框,其余情况显示普通的深紫色边框,悬停时显示深紫色调的边框,其余情况显示普通的紫色边框。这种方法不仅简化了代码,提高了可维护性,而且增强了用户界面的响应性和交互性。通过这种方式,MaterialStateOutlinedBorder 不仅定义了一组标准的交互状态,还提供了一种高效的机制来根据这些状态动态调整组件的边框样式。这种灵活性是Flutter框架设计中的一大优势,它允许开发者以声明式的方式处理状态变化,而不是依赖于繁琐的逻辑判断或多个参数设置。

9. MaterialStateOutlineInputBorder

9.1 功能描述

MaterialStateOutlineInputBorder 是一个专门用于定义依赖于组件状态的边框样式的类。这个类实现了 MaterialStateProperty<OutlineInputBorder> 接口,允许边框样式根据组件的不同状态(如悬停、聚焦、按压等)动态变化。这为开发者提供了一种灵活的方式来调整组件在不同交互状态下的视觉表现。

MaterialStateOutlineInputBorder 适用于任何需要根据状态改变边框样式的场景,例如输入框、按钮或任何其他需要突出显示状态变化的界面元素。

9.2 实现细节与使用方式

MaterialStateOutlineInputBorder 是一个抽象类,要使用它,你需要通过以下两种方式之一:

  1. 直接实现:创建一个 MaterialStateOutlineInputBorder 的子类,并实现其 resolve 方法。这种方式允许完全自定义状态逻辑。

  2. 使用工厂方法:通过 MaterialStateOutlineInputBorder.resolveWith 方法,传入一个回调函数来定义状态和边框样式的关系。

9.3 代码示例

下面是一个使用 MaterialStateOutlineInputBorder.resolveWith 方法来定义输入框边框样式的示例:

InputDecoration inputDecoration = InputDecoration(
  border: MaterialStateOutlineInputBorder.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.focused)) {
      return OutlineInputBorder(
        borderSide: BorderSide(color: Colors.blue, width: 2.0),
      ); // 聚焦状态下的边框样式
    } else if (states.contains(MaterialState.error)) {
      return OutlineInputBorder(
        borderSide: BorderSide(color: Colors.red, width: 2.0),
      ); // 错误状态下的边框样式
    }
    return OutlineInputBorder(
      borderSide: BorderSide(color: Colors.grey, width: 1.0),
    ); // 默认边框样式
  }),
);

TextField(
  decoration: inputDecoration,
)

在这个示例中,输入框的边框样式根据其状态动态解析:当输入框聚焦时显示蓝色边框,出现错误时显示红色边框,其余情况显示灰色边框。这种方式简化了代码,并确保了界面的一致性和响应性。

10. MaterialStateTextStyle

10.1 功能描述

MaterialStateTextStyle 是一个实现了 MaterialStateProperty<TextStyle> 接口的类,用于定义依赖于组件状态的文本样式。这使得文本组件在不同的用户交互状态下(如悬停、聚焦、按压等)可以显示不同的样式,提供了一种灵活的方式来调整组件在不同交互状态下的视觉表现。

10.2 实现细节与使用方式

MaterialStateTextStyle 是一个抽象类,要使用它,开发者可以通过以下两种方式:

  1. 直接实现:创建一个 MaterialStateTextStyle 的子类,并实现其 resolve 方法。这种方式允许开发者根据具体的状态逻辑自定义文本样式的变化。

  2. 使用工厂方法:通过 MaterialStateTextStyle.resolveWith 方法,传入一个回调函数来定义状态和文本样式的关系。这种方式简洁方便,适用于大多数常见需求。

10.3 代码示例

下面是一个使用 MaterialStateTextStyle.resolveWith 方法来定义按钮文本样式的示例:

ButtonStyle style = ButtonStyle(
  textStyle: MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.pressed)) {
      return TextStyle(
        color: Colors.white,
        fontSize: 16,
        fontWeight: FontWeight.bold
      ); // 按下状态下的文本样式
    } else if (states.contains(MaterialState.hovered)) {
      return TextStyle(
        color: Colors.white,
        fontSize: 14,
        decoration: TextDecoration.underline
      ); // 悬停状态下的文本样式
    }
    return TextStyle(
      color: Colors.black,
      fontSize: 14
    ); // 默认文本样式
  }),
);

TextButton(
  style: style,
  onPressed: () {},
  child: Text('Press Me'),
)

在这个示例中,按钮的文本样式根据其状态动态解析:当按钮被按下时显示白色粗体字,悬停时显示带下划线的白色文本,其余情况显示黑色文本。这种方式不仅简化了代码,提高了可维护性,而且增强了用户界面的响应性和交互性。

11. MaterialStateUnderlineInputBorder

11.1 功能描述

MaterialStateUnderlineInputBorder 是一个专门用于定义依赖于组件状态的下划线边框的类。这个类实现了 MaterialStateProperty<UnderlineInputBorder> 接口,允许下划线边框根据组件的不同状态(如悬停、聚焦、按压等)动态变化。这为开发者提供了一种灵活的方式来调整组件在不同交互状态下的视觉表现。

11.2 实现细节与使用方式

MaterialStateUnderlineInputBorder 是一个抽象类,需要通过继承并实现其 resolve 方法来使用。这种方式允许开发者根据具体的交互状态定制边框的样式,例如改变边框颜色或厚度。

abstract class MaterialStateUnderlineInputBorder extends UnderlineInputBorder implements MaterialStateProperty<UnderlineInputBorder> {
  /// 抽象常量构造函数。这个构造函数使子类能够提供常量构造函数,以便它们可以在常量表达式中使用。
  const MaterialStateUnderlineInputBorder();

  /// 返回一个 [UnderlineInputBorder],用于指定状态下使用的 Material 组件。
  @override
  UnderlineInputBorder resolve(Set<MaterialState> states);
}

11.3 代码示例

下面是一个使用 MaterialStateUnderlineInputBorder 来定义输入框边框样式的示例:

class CustomUnderlineInputBorder extends MaterialStateUnderlineInputBorder {
  @override
  UnderlineInputBorder resolve(Set<MaterialState> states) {
    // 如果输入框处于聚焦状态,则返回蓝色边框
    if (states.contains(MaterialState.focused)) {
      return const UnderlineInputBorder(
        borderSide: BorderSide(color: Colors.blue, width: 2.0),
      );
    }
    // 否则返回灰色边框
    return const UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.grey, width: 1.0),
    );
  }
}

TextField(
  decoration: InputDecoration(
    // 使用自定义的边框样式
    border: CustomUnderlineInputBorder(),
  ),
)

在这个示例中,输入框的下划线边框会根据是否聚焦来改变颜色和宽度,聚焦时显示蓝色和较宽的边框,未聚焦时显示灰色和较细的边框。这种方式简化了代码,并确保了界面的一致性和响应性。

12. 结论

MaterialState 枚举和相关的 MaterialStateProperty 类在 Flutter 中提供了一种高效且灵活的方式来根据组件的交互状态动态调整其属性。这些工具极大地增强了 UI 组件的交互性和视觉反馈,使得开发者能够以声明式的方式处理状态变化,而不是依赖于繁琐的逻辑判断或多个参数设置。

通过使用如 MaterialStateColor、MaterialStateTextStyle、MaterialStateOutlinedBorder 等,开发者可以为不同的状态定义不同的视觉表现,如颜色、文本样式和边框样式。这些状态包括但不限于悬停、聚焦、按压等,每种状态都可以有其独特的样式响应。

此外,MaterialState 提供的 resolve 方法和 resolveWith 工厂方法进一步简化了状态管理,使得开发者可以更加专注于创造性的 UI 设计和用户体验的提升。

可见,MaterialState 和 MaterialStateProperty 在 Flutter 中是处理组件状态和属性的强大工具,它们的灵活性和易用性为开发高质量、响应式的用户界面提供了坚实的基础。

F. 附录

F.1 MaterialState枚举源码

/// 交互状态,一些 Material 部件在接收用户输入时可以处于的状态。
///
/// 状态由 https://material.io/design/interaction/states.html#usage 定义。
///
/// 一些 Material 部件会在 `Set<MaterialState>` 中跟踪它们当前的状态。
///
enum MaterialState {
  /// 用户将鼠标光标拖动到给定部件上时的状态。
  ///
  /// 参见:https://material.io/design/interaction/states.html#hover.
  hovered,

  /// 用户使用键盘导航到给定部件时的状态。
  ///
  /// 有时也会在部件被点击时触发。例如,当 [TextField] 被点击时,它会变为 [focused]。
  ///
  /// 参见:https://material.io/design/interaction/states.html#focus.
  focused,

  /// 用户在给定部件上主动按下时的状态。
  ///
  /// 参见:https://material.io/design/interaction/states.html#pressed.
  pressed,

  /// 用户正在通过拖动将此部件从一个位置拖动到另一个位置时的状态。
  ///
  /// https://material.io/design/interaction/states.html#dragged.
  dragged,

  /// 当此项被选中时的状态。
  ///
  /// 适用于可以切换的内容(如芯片和复选框)以及从一组选项中选择的内容(如选项卡和单选按钮)。
  ///
  /// 参见:https://material.io/design/interaction/states.html#selected.
  selected,

  /// 当此部件与滚动视图下方的内容重叠时的状态。
  ///
  /// 由 [AppBar] 使用,指示主滚动视图的内容已向上滚动并位于应用栏后面。
  scrolledUnder,

  /// 当此部件被禁用且无法进行交互时的状态。
  ///
  /// 禁用的部件不应对悬停、聚焦、按压或拖动交互做出响应。
  ///
  /// 参见:https://material.io/design/interaction/states.html#disabled.
  disabled,

  /// 当部件进入某种无效状态时的状态。
  ///
  /// 参见 https://material.io/design/interaction/states.html#usage.
  error,
}

F.2 MaterialStateProperty接口源码

/// 用于根据部件的交互“状态”(定义为一组 MaterialState)返回类型为 `T` 的值的接口。
/// MaterialStateProperty 代表依赖于部件的材料“状态”的值。状态被编码为一组 MaterialState 值,如 MaterialState.focused、MaterialState.hovered、MaterialState.pressed。例如,InkWell.overlayColor 定义了当 InkWell 被按下(“涟漪颜色”)、聚焦或悬停时填充的颜色。InkWell 使用 overlay color 的 resolve 方法来计算当前状态下 InkWell 的颜色。
/// ButtonStyle 用于配置按钮(如 TextButton、ElevatedButton 和 OutlinedButton)的外观,具有许多 material state 属性。按钮部件会跟踪它们当前的 material state,并在需要值时解析按钮样式的 material state 属性。
abstract class MaterialStateProperty<T> {
  /// 根据 [states] 返回类型为 `T` 的值。
  ///
  /// 像 TextButton 和 ElevatedButton 这样的部件会将此方法应用于它们当前的 MaterialState,以在构建时计算颜色和其他视觉参数。
  T resolve(Set<MaterialState> states);

  /// 如果 `value` 是 MaterialStateProperty,则根据给定的状态集解析值,否则返回值本身。
  ///
  /// 对于那些参数可以选择是 MaterialStateProperty 的部件很有用。例如,InkWell.mouseCursor 可以是 MouseCursor 或 MaterialStateProperty<MouseCursor>。
  static T resolveAs<T>(T value, Set<MaterialState> states) {
    if (value is MaterialStateProperty<T>) {
      final MaterialStateProperty<T> property = value;
      return property.resolve(states);
    }
    return value;
  }

  /// 从仅有 MaterialPropertyResolver 函数创建 MaterialStateProperty 的便捷方法。
  /// Convenience method for creating a [MaterialStateProperty] that resolves
  /// to a single value for all states.
  ///
  /// If you need a const value, use [MaterialStatePropertyAll] directly.
  ///
  // TODO(darrenaustin): Deprecate this when we have the ability to create
  // a dart fix that will replace this with MaterialStatePropertyAll:
  // https://github.com/dart-lang/sdk/issues/49056.
  static MaterialStateProperty<T> all<T>(T value) => MaterialStatePropertyAll<T>(value);

  /// 在两个 [MaterialStateProperty] 之间进行线性插值。
  static MaterialStateProperty<T?>? lerp<T>(
    MaterialStateProperty<T>? a,
    MaterialStateProperty<T>? b,
    double t,
    T? Function(T?, T?, double) lerpFunction,
  ) {
    // 避免为常见情况创建 _LerpProperties 对象。
    if (a == null && b == null) {
      return null;
    }
    return _LerpProperties<T>(a, b, t, lerpFunction);
  }
}

class _LerpProperties<T> implements MaterialStateProperty<T?> {
  const _LerpProperties(this.a, this.b, this.t, this.lerpFunction);

  final MaterialStateProperty<T>? a;
  final MaterialStateProperty<T>? b;
  final double t;
  final T? Function(T?, T?, double) lerpFunction;

  @override
  T? resolve(Set<MaterialState> states) {
    final T? resolvedA = a?.resolve(states);
    final T? resolvedB = b?.resolve(states);
    return lerpFunction(resolvedA, resolvedB, t);
  }
}

class _MaterialStatePropertyWith<T> implements MaterialStateProperty<T> {
  _MaterialStatePropertyWith(this._resolve);

  final MaterialPropertyResolver<T> _resolve;

  @override
  T resolve(Set<MaterialState> states) => _resolve(states);
}

/// 用于创建一个 [MaterialStateProperty],该属性对所有状态都解析为给定值的便利类。
class MaterialStatePropertyAll<T> implements MaterialStateProperty<T> {

  /// 构造一个总是解析为给定值的 [MaterialStateProperty]。
  const MaterialStatePropertyAll(this.value);

  /// 将用于所有状态的属性值。
  final T value;

  @override
  T resolve(Set<MaterialState> states) => value;

  @override
  String toString() {
    if (value is double) {
      return 'MaterialStatePropertyAll(${debugFormatDouble(value as double)})';
    } else {
      return 'MaterialStatePropertyAll($value)';
    }
  }
}

/// 管理一组 [MaterialState] 并在更改时通知监听器。
///
/// 由那些为了支持额外状态而公开其内部状态的扩展而使用。参见 [TextButton] 作为示例。
///
/// 控制器的 [value] 是其当前的状态集。每当 [value] 更改时,监听器都会收到通知。应该只使用 [update] 来更改 [value];不应直接修改它。
///
/// 控制器的 [value] 表示一组状态,通常是 [MaterialStateProperty] 值解析的状态。它 _不是_ 组件的内在状态。组件负责确保控制器的 [value] 跟踪其内在状态。例如,不能通过将 [MaterialState.focused] 添加到其控制器来请求组件的键盘焦点。当组件获得焦点或失去焦点时,它将 [update] 其控制器的 [value] 并通知更改的监听器。
class MaterialStatesController extends ValueNotifier<Set<MaterialState>> {
  /// 创建一个 MaterialStatesController。
  MaterialStatesController([Set<MaterialState>? value]) : super(<MaterialState>{...?value});

  /// 如果 [add] 为 true,则将 [state] 添加到 [value],否则将其移除,并在 [value] 更改时通知监听器。
  void update(MaterialState state, bool add) {
    final bool valueChanged = add ? value.add(state) : value.remove(state);
    if (valueChanged) {
      notifyListeners();
    }
  }
}
05-03 08:42