跳至主要內容

新的按鈕與按鈕主題

摘要

#

Flutter 中新增了一組基本 Material 按鈕 Widget 和主題。原始類別已被棄用,最終將會移除。整體目標是讓按鈕更具彈性,並且更容易透過建構函式參數或主題來設定。

FlatButtonRaisedButtonOutlineButton 等元件已被分別替換為 TextButtonElevatedButtonOutlinedButton。每個新的按鈕類別都有其自身的主題:TextButtonThemeElevatedButtonThemeOutlinedButtonTheme。原本的 ButtonTheme 類別已不再使用。按鈕的外觀由一個 ButtonStyle 物件指定,而不是一大組元件參數和屬性。這大致上與使用 TextStyle 物件定義文字外觀的方式相當。新的按鈕主題也使用 ButtonStyle 物件進行配置。ButtonStyle 本身只是一組視覺屬性的集合。這些屬性中的許多屬性都使用 MaterialStateProperty 定義,這表示它們的值可以取決於按鈕的狀態。

背景

#

我們沒有嘗試原地發展現有的按鈕類別及其主題,而是引入了新的替代按鈕元件和主題。除了讓我們擺脫原地發展現有類別所帶來的向後相容性迷宮之外,新的名稱也使 Flutter 與 Material Design 規範同步,該規範對按鈕元件使用新的名稱。

舊元件舊主題新元件新主題
FlatButtonButtonThemeTextButtonTextButtonTheme
RaisedButtonButtonThemeElevatedButtonElevatedButtonTheme
OutlineButtonButtonThemeOutlinedButtonOutlinedButtonTheme

新的主題遵循 Flutter 大約在一年前為新的 Material 元件採用的「標準化」模式。主題屬性和元件建構函式參數預設為 null。非 null 的主題屬性和元件參數指定元件預設值的覆寫。實作和記錄預設值是按鈕元件元件的唯一責任。預設值本身主要基於整體 Theme 的 colorScheme 和 textTheme。

在視覺上,新的按鈕看起來有些不同,因為它們符合目前的 Material Design 規範,並且它們的顏色是根據整體 Theme 的 ColorScheme 配置的。在邊距、圓角半徑和懸停/焦點/按下回饋方面還有其他細微差異。

許多應用程式只需將新的類別名稱替換為舊的即可。具有黃金影像測試或按鈕外觀已使用建構函式參數或原始 ButtonTheme 配置的應用程式可能需要查閱遷移指南和後面的介紹性資料。

API 變更:使用 ButtonStyle 取代個別樣式屬性

#

除了簡單的使用案例外,新按鈕類別的 API 與舊類別不相容。新按鈕和主題的視覺屬性使用單個 ButtonStyle 物件配置,類似於如何使用 TextStyle 物件配置 TextFieldText 元件。大多數 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'),
)

styleFrom() ButtonStyle 實用方法

#

Material Design 規範根據色彩配置的主要顏色定義按鈕的前景和覆蓋顏色。主要顏色以不同的不透明度呈現,具體取決於按鈕的狀態。為了簡化建立包含所有取決於色彩配置顏色的屬性的按鈕樣式,每個按鈕類別都包含一個靜態 styleFrom() 方法,該方法會從一組簡單的值建構 ButtonStyle,包括它所依賴的 ColorScheme 顏色。

此範例會建立一個按鈕,該按鈕使用指定的原色和 Material Design 規範中的不透明度來覆寫其前景顏色及其覆蓋顏色。

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

TextButton 文件指出,當按鈕停用時,前景顏色基於色彩配置的 onSurface 顏色。要也使用 styleFrom() 覆寫它

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

如果您嘗試建立 Material Design 變體,則使用 styleFrom() 方法是建立 ButtonStyle 的首選方式。最彈性的方法是直接定義 ButtonStyle,並為要覆寫其外觀的狀態使用 MaterialStateProperty 值。

ButtonStyle 預設值

#

新的按鈕類別等元件會根據整體主題的 colorSchemetextTheme 以及按鈕的目前狀態計算其預設值。在少數情況下,它們還會考慮整體主題的色彩配置是淺色還是深色。每個按鈕都有一個受保護的方法,該方法會根據需要計算其預設樣式。雖然應用程式不會直接呼叫此方法,但其 API 文件說明了所有預設值。當按鈕或按鈕主題指定 ButtonStyle 時,只有按鈕樣式的非 null 屬性才會覆寫計算的預設值。按鈕的 style 參數會覆寫對應按鈕主題指定的非 null 屬性。例如,如果 TextButton 樣式的 foregroundColor 屬性為非 null,它會覆寫 TextButonTheme 樣式的相同屬性。

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

遷移指南

#

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

還原原始按鈕視覺效果

#

在許多情況下,只需從舊的按鈕類別切換到新的按鈕類別即可。前提是大小/形狀的微小變更以及顏色可能發生的較大變更不是問題。

為了在這些情況下保留原始按鈕的外觀,可以定義盡可能與原始按鈕相符的按鈕樣式。例如,以下樣式使 TextButton 看起來像預設的 FlatButton

dart
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
  primary: 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(
  onPrimary: Colors.black87,
  primary: 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 樣式稍微複雜一些,因為在按下按鈕時,外框的顏色會變更為主要顏色。外框的外觀由 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

新的按鈕類別不支援單獨的醒目提示顏色,因為它不再是 Material Design 的一部分。

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

#

原始按鈕類別的兩個常見自訂是 FlatButton 的自訂前景顏色,或 RaisedButton 的自訂前景和背景顏色。使用新的按鈕類別產生相同的結果很簡單

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

TextButton(
  style: TextButton.styleFrom(
    primary: Colors.red, // foreground
  ),
  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(
    primary: Colors.red, // background
    onPrimary: Colors.white, // foreground
  ),
  onPressed: () { },
  child: Text('ElevatedButton with custom foreground/background'),
)

在這種情況下,按鈕對色彩配置主要顏色的使用相對於 TextButton 是反轉的:主要顏色是按鈕的背景填滿顏色,而 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 時的背景和前景顏色。

依預設,所有按鈕都使用色彩配置的 onSurface 顏色,停用前景顏色的不透明度為 0.38。只有 ElevatedButton 具有非透明的背景顏色,其預設值是 onSurface 顏色,不透明度為 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(onSurface: 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