概述

#

Flutter 新增了一套基本的 Material Design 按鈕元件和主題。舊的類已被棄用,並將最終被移除。總體目標是使按鈕更加靈活,並且可以透過建構函式引數或主題輕鬆配置。

FlatButtonRaisedButtonOutlineButton 元件已被 TextButtonElevatedButtonOutlinedButton 分別取代。每個新按鈕類都有自己的主題:TextButtonThemeElevatedButtonThemeOutlinedButtonTheme。舊的 ButtonTheme 類不再使用。按鈕的外觀由 ButtonStyle 物件指定,而不是一大堆小部件引數和屬性。這大致相當於使用 TextStyle 物件定義文字外觀的方式。新的按鈕主題也使用 ButtonStyle 物件進行配置。ButtonStyle 本身只是視覺屬性的集合。其中許多屬性是透過 MaterialStateProperty 定義的,這意味著它們的值可以依賴於按鈕的狀態。

背景

#

與其嘗試原地修改現有的按鈕類及其主題,不如引入新的替換按鈕元件和主題。除了讓我們擺脫原地修改現有類會帶來的向後相容性的困境外,新名稱還使 Flutter 與 Material Design 規範保持一致,該規範在按鈕元件上使用了新名稱。

舊元件舊主題新元件新主題
FlatButtonButtonTheme文字按鈕TextButtonTheme
RaisedButtonButtonTheme凸起按鈕ElevatedButtonTheme
OutlineButtonButtonThemeOutlinedButtonOutlinedButtonTheme

新主題遵循 Flutter 大約一年前為新 Material 元件採用的“標準化”模式。主題屬性和元件建構函式引數預設值為 null。非 null 的主題屬性和元件引數指定了對元件預設值的覆蓋。實現和記錄預設值是按鈕元件的唯一職責。預設值主要基於整體主題的 colorScheme 和 textTheme。

在視覺上,新按鈕看起來略有不同,因為它們符合當前的 Material Design 規範,並且它們的顏色是根據整體主題的 ColorScheme 配置的。在內邊距、圓角半徑以及懸停/焦點/按下反饋方面也有一些小的差異。

許多應用程式可以簡單地用新類名替換舊類名。使用黃金影像測試或透過建構函式引數或原始 ButtonTheme 配置了按鈕外觀的應用程式可能需要參考遷移指南和後續的介紹性材料。

API 變更:使用 ButtonStyle 替代單個樣式屬性

#

除了簡單的用例外,新按鈕類的 API 與舊類不相容。新按鈕和主題的視覺屬性是透過單個 ButtonStyle 物件配置的,類似於 TextFieldText 小部件可以使用 TextStyle 物件進行配置的方式。大多數 ButtonStyle 屬性是透過 MaterialStateProperty 定義的,因此一個屬性可以根據按鈕的按下/聚焦/懸停/等狀態表示不同的值。

按鈕的 ButtonStyle 不定義按鈕的視覺屬性,它定義的是對按鈕預設視覺屬性的覆蓋,其中預設屬性由按鈕小部件本身計算。例如,要覆蓋 TextButton 在所有狀態下的預設前景(文字/圖示)顏色,可以這樣寫:

dart
TextButton(
  style: ButtonStyle(
    foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
  ),
  onPressed: () { },
  child: Text('TextButton'),
)

這種覆蓋很常見;但是,在許多情況下,還需要覆蓋文字按鈕用於指示其懸停/焦點/按下狀態的疊加顏色。這可以透過將 overlayColor 屬性新增到 ButtonStyle 來完成。

dart
TextButton(
  style: ButtonStyle(
    foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
    overlayColor: MaterialStateProperty.resolveWith<Color?>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.hovered))
          return Colors.blue.withOpacity(0.04);
        if (states.contains(MaterialState.focused) ||
            states.contains(MaterialState.pressed))
          return Colors.blue.withOpacity(0.12);
        return null; // Defer to the widget's default.
      },
    ),
  ),
  onPressed: () { },
  child: Text('TextButton')
)

顏色 MaterialStateProperty 只需要為應覆蓋其預設值的顏色返回一個值。如果返回 null,則將使用小部件的預設值。例如,要僅覆蓋文字按鈕的焦點疊加顏色:

dart
TextButton(
  style: ButtonStyle(
    overlayColor: MaterialStateProperty.resolveWith<Color?>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.focused))
          return Colors.red;
        return null; // Defer to the widget's default.
      }
    ),
  ),
  onPressed: () { },
  child: Text('TextButton'),
)

ButtonStyle 的 styleFrom() 實用方法

#

Material Design 規範根據 colorScheme 的 primary color 定義了按鈕的前景和疊加顏色。primary color 會根據按鈕的狀態以不同的不透明度渲染。為了簡化建立包含所有依賴於 colorScheme 顏色的屬性的按鈕樣式,每個按鈕類都包含一個靜態的 styleFrom() 方法,該方法從一組簡單值(包括它依賴的 ColorScheme 顏色)構建一個 ButtonStyle

此示例建立了一個按鈕,該按鈕使用指定的 primary color 和 Material Design 規範中的不透明度來覆蓋其前景顏色和疊加顏色。

dart
TextButton(
  style: TextButton.styleFrom(
    foregroundColor: Colors.blue,
  ),
  onPressed: () { },
  child: Text('TextButton'),
)

TextButton 文件表明,按鈕被停用時的前景顏色基於 colorScheme 的 disabledForegroundColor 顏色。要使用 styleFrom() 覆蓋該項,

dart
TextButton(
  style: TextButton.styleFrom(
    foregroundColor: Colors.blue,
    disabledForegroundColor: Colors.red,
  ),
  onPressed: null,
  child: Text('TextButton'),
)

如果您正嘗試建立 Material Design 的變體,那麼使用 styleFrom() 方法是建立 ButtonStyle 的首選方式。最靈活的方法是直接定義一個 ButtonStyle,其中包含您想要覆蓋其外觀的狀態的 MaterialStateProperty 值。

ButtonStyle 預設值

#

新按鈕類等小部件會根據整體主題的 colorSchemetextTheme 以及按鈕的當前狀態來*計算*其預設值。在少數情況下,它們還會考慮整體主題的 color scheme 是淺色還是深色。每個按鈕都有一個受保護的方法,該方法會在需要時計算其預設樣式。雖然應用程式不會直接呼叫此方法,但其 API 文件解釋了所有預設值。當按鈕或按鈕主題指定 ButtonStyle 時,只有按鈕樣式的非 null 屬性會覆蓋計算出的預設值。按鈕的 style 引數會覆蓋相應按鈕主題指定的非 null 屬性。例如,如果 TextButton 樣式的 foregroundColor 屬性非 null,它將覆蓋 TextButonTheme 樣式的相同屬性。

如前所述,每個按鈕類都包含一個名為 styleFrom 的靜態方法,該方法從一組簡單值(包括它依賴的 ColorScheme 顏色)構建一個 ButtonStyle。在許多常見情況下,使用 styleFrom 建立一個覆蓋預設值的單次 ButtonStyle 是最簡單的。當自定義樣式的目的是覆蓋預設樣式所依賴的 color scheme 顏色(如 primaryonPrimary)時,尤其如此。對於其他情況,您可以直接建立 ButtonStyle 物件。這樣做可以讓你控制所有可能按鈕狀態(如按下、懸停、停用和聚焦)的視覺屬性(如顏色)的值。

遷移指南

#

使用以下資訊將您的按鈕遷移到新 API。

恢復原始按鈕視覺效果

#

在許多情況下,只需將舊按鈕類切換到新按鈕類即可。這假設尺寸/形狀的小變化以及顏色可能帶來的較大變化不是問題。

要在此類情況下保留原始按鈕的外觀,可以定義儘可能接近原始按鈕的按鈕樣式。例如,以下樣式使 TextButton 看起來像預設的 FlatButton

dart
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
  foregroundColor: Colors.black87,
  minimumSize: Size(88, 36),
  padding: EdgeInsets.symmetric(horizontal: 16),
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.all(Radius.circular(2)),
  ),
);

TextButton(
  style: flatButtonStyle,
  onPressed: () { },
  child: Text('Looks like a FlatButton'),
)

類似地,要使 ElevatedButton 看起來像預設的 RaisedButton

dart
final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom(
  foregroundColor: Colors.black87,
  backgroundColor: Colors.grey[300],
  minimumSize: Size(88, 36),
  padding: EdgeInsets.symmetric(horizontal: 16),
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.all(Radius.circular(2)),
  ),
);
ElevatedButton(
  style: raisedButtonStyle,
  onPressed: () { },
  child: Text('Looks like a RaisedButton'),
)

OutlinedButtonOutlineButton 樣式稍微複雜一些,因為當按鈕被按下時,邊框的顏色會變為 primary color。邊框的外觀由 BorderSide 定義,您將使用 MaterialStateProperty 來定義按下的邊框顏色。

dart
final ButtonStyle outlineButtonStyle = OutlinedButton.styleFrom(
  foregroundColor: Colors.black87,
  minimumSize: Size(88, 36),
  padding: EdgeInsets.symmetric(horizontal: 16),
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.all(Radius.circular(2)),
  ),
).copyWith(
  side: MaterialStateProperty.resolveWith<BorderSide?>(
    (Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        return BorderSide(
          color: Theme.of(context).colorScheme.primary,
          width: 1,
        );
      }
      return null;
    },
  ),
);

OutlinedButton(
  style: outlineButtonStyle,
  onPressed: () { },
  child: Text('Looks like an OutlineButton'),
)

要恢復應用程式中按鈕的預設外觀,可以在應用程式的主題中配置新的按鈕主題:

dart
MaterialApp(
  theme: ThemeData.from(colorScheme: ColorScheme.light()).copyWith(
    textButtonTheme: TextButtonThemeData(style: flatButtonStyle),
    elevatedButtonTheme: ElevatedButtonThemeData(style: raisedButtonStyle),
    outlinedButtonTheme: OutlinedButtonThemeData(style: outlineButtonStyle),
  ),
)

要恢復應用程式部分按鈕的預設外觀,可以將小部件子樹包裝在 TextButtonThemeElevatedButtonThemeOutlinedButtonTheme 中。例如:

dart
TextButtonTheme(
  data: TextButtonThemeData(style: flatButtonStyle),
  child: myWidgetSubtree,
)

遷移自定義顏色的按鈕

#

以下部分涵蓋了 FlatButtonRaisedButtonOutlineButton 的顏色引數的使用。

dart
textColor
disabledTextColor
color
disabledColor
focusColor
hoverColor
highlightColor*
splashColor

新按鈕類不再支援單獨的 highlight color,因為它已不再是 Material Design 的一部分。

遷移具有自定義前景和背景顏色的按鈕

#

原始按鈕類的兩種常見自定義是 FlatButton 的自定義前景顏色,或 RaisedButton 的自定義前景和背景顏色。使用新按鈕類生成相同結果很簡單:

dart
FlatButton(
  textColor: Colors.red, // foreground
  onPressed: () { },
  child: Text('FlatButton with custom foreground/background'),
)

TextButton(
  style: TextButton.styleFrom(
    foregroundColor Colors.red,
  ),
  onPressed: () { },
  child: Text('TextButton with custom foreground'),
)

在這種情況下,TextButton 的前景(文字/圖示)顏色以及其懸停/焦點/按下疊加顏色將基於 Colors.red。預設情況下,TextButton 的背景填充顏色是透明的。

遷移具有自定義前景和背景顏色的 RaisedButton

dart
RaisedButton(
  color: Colors.red, // background
  textColor: Colors.white, // foreground
  onPressed: () { },
  child: Text('RaisedButton with custom foreground/background'),
)

ElevatedButton(
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.red,
    foregroundColor: Colors.white,
  ),
  onPressed: () { },
  child: Text('ElevatedButton with custom foreground/background'),
)

在這種情況下,按鈕相對於 TextButton 顛倒了 colorScheme 的 primary color 的使用:primary 是按鈕的背景填充顏色,onPrimary 是前景(文字/圖示)顏色。

遷移具有自定義疊加顏色的按鈕

#

覆蓋按鈕預設的焦點、懸停、高亮或漣漪顏色不太常見。FlatButtonRaisedButtonOutlineButton 類具有這些依賴於狀態的顏色的單獨引數。新的 TextButtonElevatedButtonOutlinedButton 類使用單個 MaterialStateProperty<Color> 引數。新按鈕允許指定所有顏色的依賴於狀態的值,而舊按鈕僅支援指定現在稱為“overlayColor”的內容。

dart
FlatButton(
  focusColor: Colors.red,
  hoverColor: Colors.green,
  splashColor: Colors.blue,
  onPressed: () { },
  child: Text('FlatButton with custom overlay colors'),
)

TextButton(
  style: ButtonStyle(
    overlayColor: MaterialStateProperty.resolveWith<Color?>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.focused))
          return Colors.red;
        if (states.contains(MaterialState.hovered))
            return Colors.green;
        if (states.contains(MaterialState.pressed))
            return Colors.blue;
        return null; // Defer to the widget's default.
    }),
  ),
  onPressed: () { },
  child: Text('TextButton with custom overlay colors'),
)

新版本更靈活,儘管不那麼簡潔。在舊版本中,不同狀態的優先順序是隱含的(且未記錄)和固定的,而在新版本中,它是顯式的。對於頻繁指定這些顏色的應用程式,最簡單的遷移路徑是定義一個或多個 ButtonStyles 來匹配上例,然後只使用 style 引數,或者定義一個無狀態包裝器小部件來封裝三個顏色引數。

遷移具有自定義停用顏色的按鈕

#

這是一種相對少見的自定義。FlatButtonRaisedButtonOutlineButton 類具有 disabledTextColordisabledColor 引數,它們定義了當按鈕的 onPressed 回撥為 null 時的背景和前景顏色。

預設情況下,所有按鈕都使用 colorScheme 的 disabledForegroundColor 顏色,停用前景顏色的不透明度為 0.38。只有 ElevatedButton 具有非透明的背景顏色,其預設值為 disabledForegroundColor 顏色,不透明度為 0.12。因此,在許多情況下,您可以使用 styleFrom 方法來覆蓋停用顏色。

dart
RaisedButton(
  disabledColor: Colors.red.withOpacity(0.12),
  disabledTextColor: Colors.red.withOpacity(0.38),
  onPressed: null,
  child: Text('RaisedButton with custom disabled colors'),
),

ElevatedButton(
  style: ElevatedButton.styleFrom(disabledForegroundColor: Colors.red),
  onPressed: null,
  child: Text('ElevatedButton with custom disabled colors'),
)

為了完全控制停用顏色,必須顯式地用 MaterialStateProperties 定義 ElevatedButton 的樣式。

dart
RaisedButton(
  disabledColor: Colors.red,
  disabledTextColor: Colors.blue,
  onPressed: null,
  child: Text('RaisedButton with custom disabled colors'),
)

ElevatedButton(
  style: ButtonStyle(
    backgroundColor: MaterialStateProperty.resolveWith<Color?>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.disabled))
          return Colors.red;
        return null; // Defer to the widget's default.
    }),
    foregroundColor: MaterialStateProperty.resolveWith<Color?>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.disabled))
          return Colors.blue;
        return null; // Defer to the widget's default.
    }),
  ),
  onPressed: null,
  child: Text('ElevatedButton with custom disabled colors'),
)

與前一種情況一樣,在應用程式中經常出現此遷移的情況下,有顯而易見的方法可以使新版本更簡潔。

遷移具有自定義陰影的按鈕

#

這也是一種相對少見的自定義。通常,只有 ElevatedButton(以前稱為 RaisedButtons)包含陰影更改。對於與基線陰影成比例的陰影(根據 Material Design 規範),可以非常簡單地全部覆蓋它們。

預設情況下,停用按鈕的陰影為 0,其餘狀態相對於基線 2 定義。

dart
disabled: 0
hovered or focused: baseline + 2
pressed: baseline + 6

因此,要遷移已定義所有陰影的 RaisedButton

dart
RaisedButton(
  elevation: 2,
  focusElevation: 4,
  hoverElevation: 4,
  highlightElevation: 8,
  disabledElevation: 0,
  onPressed: () { },
  child: Text('RaisedButton with custom elevations'),
)

ElevatedButton(
  style: ElevatedButton.styleFrom(elevation: 2),
  onPressed: () { },
  child: Text('ElevatedButton with custom elevations'),
)

要任意覆蓋僅一個陰影,例如按下的陰影:

dart
RaisedButton(
  highlightElevation: 16,
  onPressed: () { },
  child: Text('RaisedButton with a custom elevation'),
)

ElevatedButton(
  style: ButtonStyle(
    elevation: MaterialStateProperty.resolveWith<double?>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.pressed))
          return 16;
        return null;
      }),
  ),
  onPressed: () { },
  child: Text('ElevatedButton with a custom elevation'),
)

遷移具有自定義形狀和邊框的按鈕

#

原始的 FlatButtonRaisedButtonOutlineButton 類都提供了一個 shape 引數,該引數定義了按鈕的形狀及其邊框的外觀。相應的類及其主題支援分別指定按鈕的形狀和邊框,具有 OutlinedBorder shapeBorderSide side 引數。

在此示例中,原始 OutlineButton 版本在其高亮(按下)狀態下為邊框指定了與其餘狀態相同的顏色。

dart
OutlineButton(
  shape: StadiumBorder(),
  highlightedBorderColor: Colors.red,
  borderSide: BorderSide(
    width: 2,
    color: Colors.red
  ),
  onPressed: () { },
  child: Text('OutlineButton with custom shape and border'),
)

OutlinedButton(
  style: OutlinedButton.styleFrom(
    shape: StadiumBorder(),
    side: BorderSide(
      width: 2,
      color: Colors.red
    ),
  ),
  onPressed: () { },
  child: Text('OutlinedButton with custom shape and border'),
)

OutlinedButton 元件的大多數樣式引數,包括其形狀和邊框,都可以使用 MaterialStateProperty 值指定,也就是說,它們可以根據按鈕的狀態具有不同的值。要指定按鈕被按下時不同的邊框顏色,請執行以下操作:

dart
OutlineButton(
  shape: StadiumBorder(),
  highlightedBorderColor: Colors.blue,
  borderSide: BorderSide(
    width: 2,
    color: Colors.red
  ),
  onPressed: () { },
  child: Text('OutlineButton with custom shape and border'),
)

OutlinedButton(
  style: ButtonStyle(
    shape: MaterialStateProperty.all<OutlinedBorder>(StadiumBorder()),
    side: MaterialStateProperty.resolveWith<BorderSide>(
      (Set<MaterialState> states) {
        final Color color = states.contains(MaterialState.pressed)
          ? Colors.blue
          : Colors.red;
        return BorderSide(color: color, width: 2);
      }
    ),
  ),
  onPressed: () { },
  child: Text('OutlinedButton with custom shape and border'),
)

時間線

#

已釋出版本:1.20.0-0.0.pre
穩定版本:2.0.0

參考資料

#

API 文件

相關 PR