Flutter开关和切换器的高级指南

1,721 阅读10分钟

我们都很熟悉家庭开关,用于打开和关闭照明系统和其他电器设备。我们也熟悉切换按钮;如果你家里有电炊具或电磁炉,你可以在其电压和烹饪功能之间进行切换。

同样,我们的移动应用也有开关和拨动按钮来打开/关闭Wi-Fi、蓝牙等。

今天,我们要深刻地潜心了解Flutter开关和切换器的参数和属性。

什么是开关小部件?

开关按钮是一个只有两种状态的 Flutter 部件,即真/假或开/关。通常情况下,开关是一个带有拇指滑块的按钮,用户可以将其从左到右拖动,反之亦然,以在不同的状态之间切换。它不会自行维持其状态。你必须调用onChanged 属性来保持按钮的开启或关闭:

Button Toggling

什么是Toggle widget?

同样地,一个切换部件只有两种状态:真/假或开/关。但是,一个切换部件创建了多个排成行的按钮,允许用户在它们之间进行切换:

Phone Screen Toggle

开关和切换之间的关键区别

这是一个移动应用中的使用案例问题。在大多数情况下,这些小部件是在设置页面使用的。如果你拖下你的移动应用程序的通知面板,你会看到一个切换按钮的网格。但当你进入设置页面时,这些按钮就变成了开关。

你一定明白其中的区别。在你的移动应用程序中,如果你有一个只需要两种状态的控件列表,你应该使用开关。而如果你在一排或一个网格中有多个控件,你应该使用切换器。

开关小部件示例

Flutter提供了三种类型的开关小部件:

  • 开关 (Android)
  • CupertinoSwitch (iOS)
  • Switch.adaptive (它根据平台的不同进行调整)

让我们看看用于定制小部件的最常用属性。

Switch (Android)

Switch(
  // thumb color (round icon)
  activeColor: Colors.amber,
  activeTrackColor: Colors.cyan,
  inactiveThumbColor: Colors.blueGrey.shade600,
  inactiveTrackColor: Colors.grey.shade400,
  splashRadius: 50.0,
  // boolean variable value
  value: forAndroid,
  // changes the state of the switch
  onChanged: (value) => setState(() => forAndroid = value),
),

Android Switch Code

CupertinoSwitch (iOS)

CupertinoSwitch(
  // overrides the default green color of the track
  activeColor: Colors.pink.shade200,
  // color of the round icon, which moves from right to left
  thumbColor: Colors.green.shade900,
  // when the switch is off
  trackColor: Colors.black12,
  // boolean variable value
  value: forIos,
  // changes the state of the switch
  onChanged: (value) => setState(() => forIos = value),
),

iOS Switch Code

自适应开关小部件没有任何独特或不同的属性。但是,如果你想要一个图像或图标而不是通常的拇指颜色,AndroidSwitch 小组件可以进一步定制。你需要用一个资产图像来定义拇指图像属性。请看下面的代码。

安卓Switch 与图像

Switch(
  trackColor: MaterialStateProperty.all(Colors.black38),
  activeColor: Colors.green.withOpacity(0.4),
  inactiveThumbColor: Colors.red.withOpacity(0.4),
// when the switch is on, this image will be displayed
  activeThumbImage: const AssetImage('assets/happy_emoji.png'),
// when the switch is off, this image will be displayed
  inactiveThumbImage: const AssetImage('assets/sad_emoji.png'),
  value: forImage,
  onChanged: (value) => setState(() => forImage = value),
),

Image Switch

这就是代码的运行情况:

Android Switch

目前,我们没有保存开关小部件的状态;我们只是改变它。接下来是创建一个小的应用程序,我们将把主题从浅色变为深色,反之亦然,当你关闭该应用程序时,其状态将被保存。

这是一个简单的单页应用程序,在appBar ,它将改变主题的开关按钮。

我使用了Flutter Hive来保存应用程序的状态。你可以使用SharedPreferences ,但我选择了Hive,因为它是一个用于Flutter和Dart应用程序的快速、轻量级、NoSQL数据库。如果你需要一个没有众多关系的直接的键值数据库,Hive是有帮助的。它利用起来毫不费力,是一个离线数据库(在本地存储数据)。

让我们先看一下代码...

我们正在使用ValueListenableBuilder 来更新用户界面。每当它监听的值发生变化时,它就会建立特定的小部件。它的值与监听者保持同步;也就是说,每当值发生变化时,ValueListenable 监听它并更新UI,而不使用setState() 或任何其他状态管理技术:

const themeBox = 'hiveThemeBox';
void main() async {
 await Hive.initFlutter();
 await Hive.openBox(themeBox);
 runApp(const MyApp());
}

class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);

 @override
 Widget build(BuildContext context) {
   //to update the UI without using setState()
   return ValueListenableBuilder(
     valueListenable: Hive.box(themeBox).listenable(),
     builder: (context, box, widget) {
       //saving the value inside the hive box,
       var darkMode = Hive.box(themeBox).get('darkMode', defaultValue: false);
       return MaterialApp(
           debugShowCheckedModeBanner: false,
           //switching between light and dark theme,
           themeMode: darkMode ? ThemeMode.dark : ThemeMode.light,
           title: 'Flutter Demo',
           darkTheme: ThemeData.dark(),
           home: HomePage(
             value: darkMode,
           ));
     },
   );
 }
}

class HomePage extends StatelessWidget {
 final bool value;
 const HomePage({Key? key, required this.value}) : super(key: key);

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text(value ? 'Hive Dark Mode' : 'Hive Light Mode'),
       actions: [
         Switch(
           value: value,
           onChanged: (val) {
             Hive.box(themeBox).put('darkMode', !value);
           },
         ),
       ],
     ),
     body: Padding(
       padding: const EdgeInsets.all(8.0),
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.stretch,
         children: [ ],
       ),
     ),
   );
 }
}

Dark Mode Switch

切换小部件的例子

这里我们将看看在我们的应用程序中实现切换部件的四种不同方式:

  • 单一的和必需的。用户必须在两个选项中至少选择一个。
  • 单一且非必需。用户不需要选择任何选项
  • 多个且必须的。用户必须在给定的选项中至少选择一个,但也可以选择多个选项
  • 多重且非必需:用户可以根据需要选择或不选择。用户可以根据要求选择或取消选择,但也可以选择多个选项

首先,让我们看一下切换小部件的标准属性,以便对其进行定制,然后我们将通过每个小部件的代码,以及它的插图:

ToggleButtons(
  // list of booleans
  isSelected: isSelected,
  // text color of selected toggle
  selectedColor: Colors.white,
  // text color of not selected toggle
  color: Colors.blue,
  // fill color of selected toggle
  fillColor: Colors.lightBlue.shade900,
  // when pressed, splash color is seen
  splashColor: Colors.red,
  // long press to identify highlight color
  highlightColor: Colors.orange,
  // if consistency is needed for all text style
  textStyle: const TextStyle(fontWeight: FontWeight.bold),
  // border properties for each toggle
  renderBorder: true,
  borderColor: Colors.black,
  borderWidth: 1.5,
  borderRadius: BorderRadius.circular(10),
  selectedBorderColor: Colors.pink,
// add widgets for which the users need to toggle
   children: [ ],
// to select or deselect when pressed
  onPressed: (int newIndex) { }
);

Single Selection Required

单一和必要的切换开关

首先,我们必须初始化一个布尔变量的列表:

// one must always be true, means selected.
List<bool> isSelected = [true, false, false];

由于我们强制系统至少有一个选项总是被选中,我们已经初始化了一个值为true

我们已经讨论了自定义切换小部件的其他属性。现在我们将子部件添加到它的children 属性中。

注意,你必须添加与布尔值列表相同数量的子部件。否则,它将抛出一个错误。

// add widgets for which the users need to toggle
children: const [
  Padding(
    padding: EdgeInsets.symmetric(horizontal: 12),
    child: Text('MALE', style: TextStyle(fontSize: 18)),
  ),
  Padding(
    padding: EdgeInsets.symmetric(horizontal: 12),
    child: Text('FEMALE', style: TextStyle(fontSize: 18)),
  ),
  Padding(
    padding: EdgeInsets.symmetric(horizontal: 12),
    child: Text('OTHER', style: TextStyle(fontSize: 18)),
  ),
],

接下来,我们必须在setState() 函数中为切换窗口部件的onPressed() 属性添加逻辑:

  • 使用for 循环,我们将浏览布尔值的列表

  • 使用if 语句,我们将检查index 值,并始终将其设置为true 。其他按钮将被设置为false

    onPressed: (int newIndex) {
      setState(() {
        // looping through the list of booleans values
        for (int index = 0; index < isSelected.length; index++) {
          // checking for the index value
          if (index == newIndex) {
            // one button is always set to true
            isSelected[index] = true;
          } else {
            // other two will be set to false and not selected
            isSelected[index] = false;
          }
        }
      });
    },
    

这就是我们最终产品的样子:

Gender Selection

单一的和不需要的拨动开关

我们只需要做两个改动就可以了。用户可以在三个选项中只选择一个,但不需要选择它。

布尔变量列表里面的值都被初始化为false:

// all values are false
List<bool> isSelected = [false, false, false];

onPressed() 函数中的if 语句里面,我们只在按钮之间切换,将其设置为true

onPressed: (int newIndex) {
  setState(() {
    // looping through the list of booleans values
    for (int index = 0; index < isSelected.length; index++) {
      if (index == newIndex) {
        // toggling between the button to set it to true
        isSelected[index] = !isSelected[index];
      } else {
        // other two buttons will not be selected and are set to false
        isSelected[index] = false;
      }
    }
  });
},

Phone Selection

需要进行的多项选择

如前所述,用户可以选择多个选项,但系统会保持至少一个选项始终被选中。

是的,你猜对了,布尔列表中的一个值将是true

List<bool> isSelected = [true, false, false];

onPressed 函数里面,事情变得有点有趣。

首先,我们添加一个变量来循环浏览布尔运算列表并确保其值为真;因此,至少有一个按钮总是被选中:

final isOneSelected = isSelected.where((element) => element).length == 1;

如果只有一个按钮被选中,用户就不能把它切换到false ,直到另一个选项被选中:

if (isOneSelected && isSelected[newIndex]) return;

接下来,在setState() 函数里面,我们再次循环浏览我们的列表,检查新的索引值,并在新旧索引之间进行切换:

setState(() {
  // looping through the list of booleans
  for (int index = 0; index < isSelected.length; index++) {
    // checking for the index value
    if (index == newIndex) {
      // toggle between the old index and new index value
      isSelected[index] = !isSelected[index];
    }
  }
});

Multiple Selection Required

不需要的多个选择

这是很直接的。我做了一排你通常在任何文本编辑器中看到的文本编辑选项,以格式化书写的文本。有四个选项,所以我们的列表里面有四个值,而且都被设置为false

List<bool> isSelected = [false, false, false, false];

在我们的onPressed() 函数中,我们只需在truefalse 值之间进行切换:

onPressed: (int index) {
  setState(() {
    // simply toggling buttons between true and false state
    isSelected[index] = !isSelected[index];
  });

Format Selection

我们已经完成了对开关和切换小部件的解释,以及如何以通常的方式使用它。现在,让我们通过创建一个自定义的动画开关按钮来进行复杂的编程,在实现下一个代码集后,它将看起来像下面的图片。

创建一个自定义的动画开关按钮

我们把这个按钮分成两部分。第一部分是一个无状态的小部件,我将其命名为CustomAnimatedSwitch 。在这个无状态部件中,我们将创建自定义开关。稍后,我们将把它添加到有状态的部件中,以使用setState() 功能来打开和关闭。

Custom Animated Switch

第1步:添加依赖性

simple_animations: ^5.0.0+2

第2步:定义变量

首先,我们将使用一个枚举和一个布尔值来定义我们命名的常量变量:

enum _CustomSwitchParams { paddingLeft, color, text, rotation }

final bool toggle;

其次,由于我们使用的是带有级联符号(双点运算符)的简单动画包,我们在我们创建的MovieTween 对象上调用函数来访问其属性。基本上,我们正在为我们先前添加的枚举添加动画:

var customTween = MovieTween()
  ..scene(duration: const Duration(seconds: 1))
      .tween(_CustomSwitchParams.paddingLeft, 0.0.tweenTo(60.0))
  ..scene(duration: const Duration(seconds: 1))
      .tween(_CustomSwitchParams.color, Colors.red.tweenTo(Colors.green))
  ..scene(duration: const Duration(milliseconds: 500))
      .tween(_CustomSwitchParams.text, ConstantTween('OFF'))
      .thenTween(_CustomSwitchParams.text, ConstantTween('ON'),
          duration: const Duration(milliseconds: 500))
  ..scene(duration: const Duration(seconds: 1))
      .tween(_CustomSwitchParams.rotation, (-2 * pi).tweenTo(0.0));

第3步:CustomAnimationBuilder

接下来,我们将建立我们的CustomAnimationBuilder widget,并定义它所需的属性来组装开关动画:

CustomAnimationBuilder<Movie>(
   // control of the animation
   control: toggle ? Control.play : Control.playReverse,
   // the relative position where animation will start
   startPosition: toggle ? 1.0 : 0.0,
   // define unique key
   key: const Key('0'),
   duration: customTween.duration * 1.2,
   // movie tween object
   tween: customTween,
   curve: Curves.easeInOut,
   builder: (context, value, child) {
     return Container(
       decoration:
           _outerDecoration(color: value.get(_CustomSwitchParams.color)),
       width: 100.0,
       height: 40.0,
       padding: const EdgeInsets.all(4.0),
       child: Stack(
         children: [
           Positioned(
             child: Padding(
               padding: EdgeInsets.only(
                 left: value.get(_CustomSwitchParams.paddingLeft),
               ),
               child: Transform.rotate(
                 angle: value.get(_CustomSwitchParams.rotation),
                 child: Container(
                   decoration: _innerDecoration(
                     color: value.get(_CustomSwitchParams.color),
                   ),
                   width: 30.0,
                   child: Center(
                     child: Text(
                       value.get(_CustomSwitchParams.text),
                       style: const TextStyle(
                           height: 1.5,
                           fontSize: 12,
                           fontWeight: FontWeight.bold,
                           color: Colors.white),
                     ),
                   ),
                 ),
               ),
             ),
           ),
         ],
       ),
     );
   },
 );
}

第4步:CustomSwitchButton (有状态的小部件)

来到创建自定义开关按钮的第二部分,我们必须添加另一个Dart文件,其中包含一个有状态的部件,我们将其称为CustomSwitchButton

首先,定义一个布尔变量并将其值设置为false

bool _switched = false;

其次,用setState() 函数创建一个方法,在truefalse 之间进行切换:

void toggleSwitch() {
  setState(() {
    _switched = !_switched;
  });
}

最后,我们将我们的CustomAnimatedSwitch 添加到这个用GestureDetector 包装的Dart文件中,将toggleSwitch 方法添加到onTap() 函数中。

这就是了!我们有了功能齐全的、定制的、动画的开关按钮。请看下面的代码和与之相关的图片:

@override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Custom Animated Switch'),
     ),
     body: GestureDetector(
       onTap: toggleSwitch,
       child: Center(
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.center,
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             const Padding(
               padding: EdgeInsets.all(10.0),
               child: Text('Tap to Check Custom Animated Switch'),
             ),
             const SizedBox(
               height: 20.0,
             ),
             CustomAnimatedSwitch(toggle: _switched),
           ],
         ),
       ),
     ),
   );
 }
}

用于开关和切换的流行Flutter包

如果您不想创建您自己的开关按钮,您总是可以使用下面的任何一个包,它们的功能与我们做的完全一样,以制作我们自己的自定义动画开关:

  1. [AnimatedToggleSwitch](https://pub.dev/packages/animated_toggle_switch):简单而有活力的切换开关,用于多个选择。如果你不想使用像下拉菜单那样的东西,这是一个很好的选择。
    Nice Switch
  2. [FlutterSwitch](https://pub.dev/packages/flutter_switch):一个为Flutter创建的易于实现的自定义开关。给它一个自定义的高度和宽度,开关和切换的边框,边框半径,颜色,切换的大小,选择显示开和关的文本,并能够在切换内添加一个图标。
  3. [ToggleSwitch](https://pub.dev/packages/toggle_switch):一个简单的拨动开关小部件。它可以完全定制所需的图标、宽度、颜色、文本、角半径、动画等。它还可以保持一个选择状态

我留下了整个项目的链接,你可以在我的GitHub页面上找到。如果有任何问题或你能改进代码,请告诉我,我会给你访问我的项目的权限。

非常感谢你,并请注意安全!