Flutter常用Widget介绍

不羁的心 提交于 2020-01-24 02:56:46

转载请注明出处: https://learnandfish.com

Text文本组件 --最基础的组件

  • Text用于显示简单样式文本,它包含一些控制文本显示样式的一些属性。
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text(
            // 文本控件
            "常用Widget介绍", // 文本内容
            style: TextStyle(
                // 文本样式
                color: Colors.white, // 文字颜色
                fontSize: 20 // 文字大小
                ),
          ),
        ),
        body: Text(
          "Widget界面" * 5, // 字符串重复5次 
          textAlign: TextAlign.center, // 文字居中显示
        ),
      ),
    );
  }
}
  • RichText 富文本, 可以显示多种样式的text。
var richText = Text.rich( // 富文本
                         TextSpan( // 通过TextSpan组合文本内容
                           children: [ // 有多个TextSpan, 每个TextSpan定义一部分内容
                             TextSpan(text: "个人博客"),
                             TextSpan(
                               text: "https://learnandfish.com",
                               style: TextStyle(color: Colors.lightBlueAccent),
                             ),
                           ],
                         ),
                       );

Button 按钮

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      // 默认竖直方向排列的容器widget, 类似于Android的LinearLayout。
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
        // RaisedButton 默认带有阴影和灰色背景。按下后阴影会变大。
        RaisedButton(
          child: Text("不可点击状态"), // button中的文字显示
          onPressed: null, // 点击事件,如果赋值为null, 则控件状态为不可用,显示灰色不可点击
        ),
        RaisedButton(
          child: Text("正常状态"), // button中的文字显示
          onPressed: () {
            // 具体逻辑
          }, // 点击事件,正常状态
        ),
        RaisedButton(
          child: Text("打印我是可点击的RaisedButton"), // button中的文字显示
          onPressed: () {
            // 具体逻辑
            print("我是可点击的RaisedButton");
          }, // 点击事件,正常状态
        ),
        RaisedButton(
          child: Text("打印我是可点击的RaisedButton"), // button中的文字显示
          onPressed: () => print("我是可点击的RaisedButton"), // 点击事件,正常状态
        ),

        /// FlatButton 扁平化的按钮, 默认背景透明且不带阴影, 按下出现背景但没有阴影。
        /// 点击事件和结构和RaisedButton一致, 只是样式有些不同而已。
        FlatButton(
          child: Text("FlatButton"),
          onPressed: () => print("我是可点击的FlatButton"),
        ),

        /// OutlineButton 默认有一个边框, 不带阴影且背景透明。按下后边框颜色会变亮、同时出现背景和阴影。
        /// 点击事件和结构和RaisedButton一致, 只是样式有些不同而已。
        OutlineButton(
          borderSide: BorderSide(
              // 默认情况下的边框样式
              color: Colors.amber),
          highlightedBorderColor: Colors.red, // 按下时边框的颜色
          child: Text("OutlineButton"),
          onPressed: () => print("我是可点击的OutlineButton"),
        ),

        /// IconButton是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景。
        IconButton(
          icon: Icon(Icons.add),
          onPressed: () => print("Add"),
        ),

        /// 带图标的按钮, RaisedButton、FlatButton、OutlineButton都有一个icon 构造函数
        /// 通过该方法可以轻松的创建带图标的按钮。
        RaisedButton.icon(
          onPressed: () => print("带图标的RaiseButton"),
          icon: Icon(Icons.adb),
          label: Text("Adb"),
        ),
        FlatButton.icon(
          onPressed: () => print("带图标的FlatButton"),
          icon: Icon(Icons.access_alarms),
          label: Text("Alarm"),
        ),
        OutlineButton.icon(
          onPressed: () => print("带图标的OutlineButton"),
          icon: Icon(Icons.print),
          label: Text("Printer"),
        ),

        /// 圆角按钮, 圆角是通过shape属性定义的, RaisedButton、FlatButton、OutlineButton都有该属性。
        /// 我们以RaiseButton为例定义。
        RaisedButton(
          color: Colors.white,
          shape: RoundedRectangleBorder( //
            borderRadius: BorderRadius.circular(5)
          ),
          child: Text("圆角按钮"),
          onPressed: () => print("圆角按钮"),
        ),
        
        /// ButtonBar 定义一组控件, 如果一行的宽度足够, 则按照子控件都在一行排列
        /// 如果子控件太多一行控件不够排列, 则按照竖直方向排列。
      ],
    );
  }
}

FloatingActionButton 悬浮按钮

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text(
            // 文本控件
            "常用Widget介绍", // 文本内容
            style: TextStyle(
                // 文本样式
                color: Colors.white, // 文字颜色
                fontSize: 20 // 文字大小
                ),
          ),
        ),
        body: HomePage(),
        /// FloatingActionButtonLocation.centerDocked 配合BottomNavigationBar
        /// 能够实现闲鱼App底部效果。悬浮按钮位于底部导航栏的中间并覆盖底部导航栏。
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, // 悬浮按钮的位置
        floatingActionButton: FloatingActionButton( // 悬浮按钮, 必须放在Scaffold控件下方
          child: Icon(Icons.arrow_upward),
          onPressed: () => print("点击返回顶部"),
        ),
      ),
    );
  }
}

图片控件

  • Flutter通过Image控件加载图片资源, 图片的来源可以是asset文件、本地文件或者网络文件。
  • 对应的组件包含AssetImage和NetworkImage。
/// 在工程目录添加images文件夹, 并将图片拷贝进去。
/// 此时, 如果需要使用images中的文件, 需要在项目的pubspec.yaml文件中声明该文件才能正常使用。
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Image( // 获取Assets中的图片
          image: AssetImage("images/ic_flutter.png"),
          width: 50,
          height: 50,
        ),
        Image( // 获取网络图片
          image: NetworkImage("https://i.loli.net/2019/12/25/lXYknLjtTKOpaiJ.png"),
          width: 250,
          height: 250,
        ),
      ],
    );
  }
}
  • 有些小伙伴就说了每次这样写真的很麻烦, 其实Flutter官方给我们提供了更简单的方法实现上述效果。
  • 通过Image.asset和Image.network能实现同样的效果。这两个是我们比较常用的办法。
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Image.asset(
          "images/ic_flutter.png",
          width: 50,
          height: 50,
        ),
        Image.network(
          "https://i.loli.net/2019/12/25/lXYknLjtTKOpaiJ.png",
          width: 250,
          height: 250,
        )
      ],
    );
  }
}
  • 那么有细心的同学可能会问, 如果我们的图片太小或者太大, 而我们目标显示的大小是固定的怎么办。
  • 其实在我们上述图片控件中包含一个属性[fit], 通过设置该属性不同的值可以使给定图片根据我们设定的大小进行拉伸、裁剪等调整。
  • [fit]的所有值定义在[BoxFit]中, BoxFit是一个枚举类型的类, 包含以下值。
  • fill: 拉伸填充满显示空间, 图片本身长宽比会发生变化, 图片会变形。
  • cover: 按图片的长宽比放大后居中填满显示空间, 图片不会变形, 超出显示空间部分会被剪裁。
  • contain: 图片的默认适应规则, 图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间, 图片不会变形。
  • fitWidth: 图片的宽度会缩放到显示空间的宽度, 高度会按比例缩放, 然后居中显示, 图片不会变形, 超出显示空间部分会被剪裁。
  • fitHeight: 图片的高度会缩放到显示空间的高度, 宽度会按比例缩放, 然后居中显示, 图片不会变形, 超出显示空间部分会被剪裁。
  • none: 图片没有适应策略, 会在显示空间内显示图片, 如果图片太大, 只会显示中间部分。
  • 上述理论可能有点绕, 大家可以自行运行一下看看效果。我们常用的属性使cover。

单选开关Switch和复选框Checkbox

  • 这两个控件都是有状态的控件, 状态分为选中和未选中, 在下面的示例中我们通过父类保存状态, 通过点击事件进行动态的改变和更新。
  • 提到有状态控件大家可能有点陌生, 这里简单讲解一下。
  • Flutter控件分为有状态的组件和无状态的组件, 无状态的组件都派生自statelessWidget, 有状态组件都派生自StatefulWidget。
  • 例如我们如果页面只有一个显示文字的控件并且文字始终不变, 那么我们就可以使用无状态的组件。
  • 该系列第二篇博客中的计数器, 每次点击按钮屏幕中的数字就要自增1, 这需要记住最近的数字和点击后需要自增1进行刷新UI, 这就是有状态。
  • 由于Flutter是1帧组件, 也就是说一旦绘制完成不能动态改变值。Flutter官方提供了setState函数, 通过调用该函数重新绘制进行刷新状态。
class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _switchState = true; // 默认关闭状态
  bool _checkState = true; // 默认开启状态

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Switch(
          value: _switchState,
          // Dart单行函数
          onChanged: (value) => setState(() => _switchState = value),
        ),
        Checkbox(
          value: _checkState,
          onChanged: (value) {
            setState(() { // 通过该函数重新赋值并刷新UI
              _checkState = value;
            });
          },
        ),
      ],
    );
  }
}

输入框TextField和表单Form

  • 我们以几个小例子进行演示。
class _HomePageState extends State<HomePage> {

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        TextField(
          autofocus: true, // 自动获取焦点
          decoration: InputDecoration( // 输入框样式设置
            labelText: "用户名", // 无焦点时默认显示内容
            hintText: "请输入用户名", // 提示文本
            prefixIcon: Icon(Icons.person), // 左侧图标
          ),
          onChanged: (value) { // 内容改变时会触发回调, 每次输入或者删除都会回调。
            print("onChanged----$value");
          },
        ),
        TextField(
          decoration: InputDecoration(
            labelText: "密码",
            hintText: "请输入密码",
            prefixIcon: Icon(Icons.lock),
          ),
          obscureText: true, // 密码隐式显示
        ),
        TextField(
          decoration: InputDecoration(
            labelText: "手机号",
            hintText: "请输入手机号",
            prefixIcon: Icon(Icons.phone),
          ),
          keyboardType: TextInputType.phone, // 键盘输入类型, 此时为数字加*和#
        ),
      ],
    );
  }
}
  • 通过controller操作文本框
class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  TextEditingController _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    // 该方法只会调用一次, 适合做一些初始化操作。
    _controller.text = "初始值";
    _controller.addListener((){ // 监听状态变化
        print(_controller.text);
    });
  }

  @override
  void dispose() {
    super.dispose();
    // 该方法销毁组件时调用, 进行一些对象的回收工作, 减少内存消耗。
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        TextField(
          controller: _controller,
          autofocus: true, // 自动获取焦点
        ),
        RaisedButton(
          child: Text("输入框改变内容"),
          onPressed: (){
            _controller.text = "输入框改变内容";
          },
        )
      ],
    );
  }
}
  • Form表单
  • 通过Form表单控件,表单子控件FormField组成。如TextFormField继承自FormField, 也是TextField的一个包装类。
  • Form对应FormState进行状态管理。通过Form.of()或GlobalKey获得。
  • 下面以一个例子进行说明。
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FormTestRoute(),
    );
  }
}

class FormTestRoute extends StatefulWidget {
  @override
  _FormTestRouteState createState() => new _FormTestRouteState();
}

class _FormTestRouteState extends State<FormTestRoute> {
  TextEditingController _unameController = new TextEditingController();
  TextEditingController _pwdController = new TextEditingController();
  GlobalKey _formKey= new GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title:Text("Form Test"),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
        child: Form(
          key: _formKey, //设置globalKey,用于后面获取FormState
          autovalidate: true, //开启自动校验
          child: Column(
            children: <Widget>[
              TextFormField(
                  autofocus: true,
                  controller: _unameController,
                  decoration: InputDecoration(
                      labelText: "用户名",
                      hintText: "用户名或邮箱",
                      icon: Icon(Icons.person)
                  ),
                  // 校验用户名
                  validator: (v) {
                    return v
                        .trim()
                        .length > 0 ? null : "用户名不能为空";
                  }

              ),
              TextFormField(
                  controller: _pwdController,
                  decoration: InputDecoration(
                      labelText: "密码",
                      hintText: "您的登录密码",
                      icon: Icon(Icons.lock)
                  ),
                  obscureText: true,
                  //校验密码
                  validator: (v) {
                    return v
                        .trim()
                        .length > 5 ? null : "密码不能少于6位";
                  }
              ),
              // 登录按钮
              Padding(
                padding: const EdgeInsets.only(top: 28.0),
                child: Row(
                  children: <Widget>[
                    Expanded(
                      child: RaisedButton(
                        padding: EdgeInsets.all(15.0),
                        child: Text("登录"),
                        color: Theme
                            .of(context)
                            .primaryColor,
                        textColor: Colors.white,
                        onPressed: () {
                          // 通过_formKey.currentState 获取FormState后,
                          // 调用validate()方法校验用户名密码是否合法,校验
                          // 通过后再提交数据。
                          if((_formKey.currentState as FormState).validate()){
                            //验证通过提交数据
                            print("登录成功");
                          }
                        },
                      ),
                    ),
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

进度指示器

  • LinearProgressIndicator 线性、条状的进度条。
  • CircularProgressIndicator 圆形进度条。
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        SizedBox(
          height: 20,
        ),
        LinearProgressIndicator(
          backgroundColor: Colors.blue[200], // 背景颜色,200为深度, 取值50-900之间。
          valueColor: AlwaysStoppedAnimation(Colors.blue), // 动画
        ),
        SizedBox(
          height: 20,
        ),
        CircularProgressIndicator(
          backgroundColor: Colors.blue[200], // 背景颜色,200为深度, 取值50-900之间。
          valueColor: AlwaysStoppedAnimation(Colors.blue),
        ),
      ],
    );
  }
}

常用布局类容器

  • Row 子控件按行排列, 不会自动换行。
  • Column 子控件按列排列, 不会自动换行。
  • Wrap 和Row类似, 如果一行控件不够会自动换行。
  • Stack 子组件堆叠排列。往往于Positioned联合使用。
/// Wrap实例
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Wrap(
      children: <Widget>[
        Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
        Container(
          width: 100,
          height: 100,
          color: Colors.white,
        ),
        Container(
          width: 100,
          height: 100,
          color: Colors.amber,
        ),
        Container(
          width: 100,
          height: 100,
          color: Colors.red,
        ),
        Container(
          width: 100,
          height: 100,
          color: Colors.lightBlueAccent,
        ),
      ],
    );
  }
}
/// Stack和Positioned实例后续再给实例, 等学习了ConstrainedBox再给实例。

常用容器类控件

  • Container 前面我们使用过这个组件, 就是定义一个盒子类型的空间。
  • Padding 使用该控件包裹的子控件, 可以很方便的定义子控件与父控件的缩进距离。
  • ConstrainedBox 尺寸限制容器, 结合BoxConstraints可以限定出一个全屏或者其他类型的空间。
  • UnconstrainedBox 去除父类控件的大小限制, 被该控件包裹的子控件无视外部约束大小, 显示自己真实的大小。
  • DecoratedBox 装饰类容器, 可以定义一个渐变色的圆角控件。
  • Scaffold、TabBar、BottomNavigationBar这些放在后续路由章节结合讲解。
/// ConstrainedBox + Stack + Positioned
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.expand(), // 约定充满全屏
      child: Stack( 
        alignment: Alignment.center, //指定未定位或部分定位widget的对齐方式
        children: <Widget>[
          Container(
            child: Text("Hello world", style: TextStyle(color: Colors.white)),
            color: Colors.red,
          ),
          Positioned(
            left: 18.0,
            child: Text("I am Jack"),
          ),
          Positioned(
            top: 18.0,
            child: Text("Your friend"),
          )
        ],
      ),
    );
  }
}

/// 定义状态栏右侧进度条的宽高, 期望情况下是20*20, 但实际效果是一个椭圆进度条, 思考一下为什么?
/// 这就是我们说的被父类控件的ConstrainedBox约束了大小, 以后如果我们明确定义了控件的大小, 但是
/// 实际效果却不是我们想要的, 很大一部分情况就是父类控件约束了大小, 我们需要通过UnconstrainedBox
/// 控件进行去除父类控件的约束, 展示自定义的大小。
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text(
            // 文本控件
            "常用Widget介绍", // 文本内容
          ),
          actions: <Widget>[
            SizedBox( // 定义状态栏右侧进度条的宽高, 期望情况下是20*20
              width: 20,
              height: 20,
              child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation(Colors.white),
              ),
            )
          ],
        ),
        body: HomePage(),
      ),
    );
  }
}
/// 修改之后如下, 在SizeBox外层添加了UnconstrainedBox
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text(
            // 文本控件
            "常用Widget介绍", // 文本内容
          ),
          actions: <Widget>[
            UnconstrainedBox(
              child: SizedBox( // 定义状态栏右侧进度条的宽高, 期望情况下是20*20
                width: 20,
                height: 20,
                child: CircularProgressIndicator(
                  valueColor: AlwaysStoppedAnimation(Colors.white),
                ),
              ),
            )
          ],
        ),
        body: HomePage(),
      ),
    );
  }
}

可滚动组件

  • SingleChildScrollView 只能接收一个子组件, 类似于Android中的ScrollView。
  • ListView 列表组件。
  • GridView 网格组件。
  • CustomScrollView 自定义滚动组件, 实现类似于淘宝类型的界面, 上面有个图片, 往下是GridView, 最后是ListView等。自由度比较高。
  • 通过ScrollController进行滚动监听及相应控制。
/// SingleChildScrollView
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return Scrollbar( // 显示进度条
      child: SingleChildScrollView(
        padding: EdgeInsets.all(16.0), // 缩进
        child: Center(
          child: Column( //动态创建一个List<Widget>
            children: str
                .split("") //每一个字母都用一个Text显示,字体为原来的两倍
                .map((c) => Text(
                      c,
                      textScaleFactor: 3.0, // 缩放大小
                    ))
                .toList(),
          ),
        ),
      ),
    );
  }
}
/// ListView 通常使用ListView.builder和ListView.separated构造, 后者可以很方便的添加分隔线。
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scrollbar( // 显示进度条
      child: ListView.builder(
          itemCount: 20,
          itemExtent: 50.0, //强制高度为50.0
          itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text("$index"));
          }
      ),
    );
  }
}
/// GridView与ListView类似, 不思考难进步, 所以GridView作为思考题大家自行学习。
/// CustomScrollView
class CustomScrollViewTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //因为本路由没有使用Scaffold,为了让子级Widget(如Text)使用
    //Material Design 默认的样式风格,我们使用Material作为本路由的根。
    return Material(
      child: CustomScrollView(
        slivers: <Widget>[
          //AppBar,包含一个导航栏
          SliverAppBar( // 类似于Android的可收缩toolbar
            pinned: false, // 收起时不显示导航栏
            expandedHeight: 250.0,
            flexibleSpace: FlexibleSpaceBar(
              background: Image.network(
                "https://i.loli.net/2019/12/25/lXYknLjtTKOpaiJ.png",
                fit: BoxFit.cover,
              ),
            ),
          ),

          SliverPadding(
            padding: const EdgeInsets.all(8.0),
            sliver: new SliverGrid(
              //Grid
              gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2, //Grid按两列显示
                mainAxisSpacing: 10.0,
                crossAxisSpacing: 10.0,
                childAspectRatio: 4.0,
              ),
              delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  //创建子widget
                  return new Container(
                    alignment: Alignment.center,
                    color: Colors.cyan[100 * (index % 9)],
                    child: new Text('grid item $index'),
                  );
                },
                childCount: 20,
              ),
            ),
          ),
          //List
          new SliverFixedExtentList(
            itemExtent: 50.0,
            delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              //创建列表项
              return new Container(
                alignment: Alignment.center,
                color: Colors.lightBlue[100 * (index % 9)],
                child: new Text('list item $index'),
              );
            }, childCount: 50 //50个列表项
                ),
          ),
        ],
      ),
    );
  }
}
/// ScrollController作为思考题大家自行学习。

结尾

还有一些功能性的控件放到后续讲完页面路由之后再进行讲解会比较好理解。

愿景

欢乐的时间总是那么短暂, 如果你觉得本篇博客对你有用,可以点个赞,评个论,加个关注或者打个赏给博主增加动力哦。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!