Flutter 入门指北(Part 5)之输入处理及实战

该文已授权公众号 「码个蛋」,转载请指明出处

前面提到基础部件的时候,忘了提输入内容处理部件,这里补上,然后顺带撸个实际的界面吧

TextField

const TextField({
    Key key,
    this.controller, // 定义一个 `TextEditingController` 实例,用来获取输入框内容等操作
    this.focusNode, // 定义一个 `FocusNode` 实例,判断当前输入框是否获取到焦点等操作
    this.decoration = const InputDecoration(), // 输入框样式,包括提醒字样,hint 等等
    TextInputType keyboardType, // 输入文本类型,例如 数字,email 等等
    this.textInputAction, // 键盘确认按钮的事件类型
    this.textCapitalization = TextCapitalization.none,
    this.style, // 文字样式
    this.textAlign = TextAlign.start, // 对齐方式
    this.textDirection, // 文字方向
    this.autofocus = false, // 是否自动获取焦点
    this.obscureText = false, // 文字是否隐藏,多用于密码
    this.autocorrect = true, 
    this.maxLines = 1, //
    this.maxLength, // 最大长度
    this.maxLengthEnforced = true, // 设置最大长度后,输入内容超出后是否强制不给输入
    this.onChanged, // 输入内容发生变化时候的回调
    this.onEditingComplete, // 输入完毕的回调
    this.onSubmitted, // 提交内容的回调
    this.inputFormatters, // 
    this.enabled, // 是否可输入,false 不可输入
    this.cursorWidth = 2.0, // 游标宽度
    this.cursorRadius, // 游标半径
    this.cursorColor, // 游标颜色
    this.keyboardAppearance, // 该属性只在 iOS 设备有效
    this.scrollPadding = const EdgeInsets.all(20.0),
    this.enableInteractiveSelection,
    this.onTap, // 点击事件
  })

那么,简单的来个输入框示例吧,然后通过 Text 展示结果

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // 可以传入初始值
  TextEditingController _editController = TextEditingController();
  FocusNode _editNode = FocusNode();
  // 保存按钮点击后的输入内容值
  String _content = '';
  // 监听输入内容变化的内容值
  String _spyContent = '';

  @override
  void initState() {
    super.initState();
    // 当输入框获取到焦点或者失去焦点的时候回调用
    _editNode.addListener(() {
      print('edit has focus? => ${_editNode.hasFocus}');
    });
  }

  @override
  void dispose() {
    // 记得销毁,防止内存溢出
    _editController.dispose();
    _editNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Input Content'),
      ),
      body: Container(
          padding: const EdgeInsets.symmetric(horizontal: 12.0),
          child: Column(
            children: <Widget>[
              TextField(
                controller: _editController,
                focusNode: _editNode,
                decoration: InputDecoration(
                    icon: Icon(Icons.phone_iphone, color: Theme.of(context).primaryColor),
                    labelText: '请输入手机号',
                    helperText: '手机号',
                    hintText: '手机号...在这儿输入呢'),
                keyboardType: TextInputType.number,
                // 输入类型为数字类型
                textInputAction: TextInputAction.done,
                style: TextStyle(color: Colors.redAccent, fontSize: 18.0),
                textDirection: TextDirection.ltr,
                maxLength: 11, // 最大长度为 11
                maxLengthEnforced: true, // 超过长度的不显示
                onChanged: (v) { // 输入的内容发生改变会调用
                  setState(() => _spyContent = v);
                },
                onSubmitted: (s) { // 点击确定按钮时候会调用
                  setState(() => _spyContent = _editController.value.text);
                },
              ),
              Padding(
                  padding: const EdgeInsets.symmetric(vertical: 8.0),
                  child: RaisedButton(
                      onPressed: () { 
                        // 获取输入的内容
                        setState(() => _content = _editController.value.text);
                        // 清理输入内容
                        _editController.clear();
                        setState(() => _spyContent = '');
                      },
                      child: Text('获取输入内容'))),
              // 展示输入的内容,点击按钮会显示
              Text(_content.isNotEmpty ? '获取到输入内容: $_content' : '还未获取到任何内容...'),
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 8.0),
                // 监听输入内容的变化,会跟随输入的内容进行改变
                child: Text('我是文字内容监听:$_spyContent'),
              )
            ],
          )),
    );
  }
}

这边需要提下的是 setState 方法,该方法只有 StatefulWidget 才有,当需要修改某个值的内容的时候,通过该方法进行修改,最后的效果图如下,当输入框文字发生变化的时候,监听的 Text 内容会随之改变,获取内容的 Text 当点击按钮了才发生变化

textField 展示1.png
textField 展示2.png

该部分代码查看 text_field_main.dart 文件

那么如果有个需求,在点击按钮的时候需要对输入的内容的合理性进行检测,当然可以通过 TextEditingController 的结果进行检测,但是还有个更加方便的方法,可以直接使用部件 TextFormField 来实现,不过需要我们在外层加一个 Form 部件,接下来,就要准备通过 TextFormField 来撸一个登录界面,但是这之前,前面有个坑需要先解决下

导入自定义的图标

在这之前,涉及到 Icon 部件,都是使用的系统自带的图标,那么如何导入第三方自定义图标呢,马上为你揭晓答案,首先我们需要打开「阿里妈妈」也就是 iconfont,不知道的小伙伴通过链接打开,然后需要注册个账户,也可以直接通过 Github 等三方登录,然后就可以搜索我们需要的图标了,接下来需要撸一个登录,那我们就找一个 用户密码 的图标吧,选择喜欢的图标,然后鼠标放到图标会出现三个按钮,直接点击 购物车 那个按钮,然后就可以通过顶部的 购物车 按钮查看添加的图标,点击下载代码,把资源文件下载到本地。

iconfont.png

解压后,需要用到的文件有两个,别的可以忽略

  1. demo_index.html 这边用来查看图标的 unicode
  2. iconfont.ttf 这边就是图标资源文件了

回到项目,创建一个文件夹 fonts ,和 images 同级,将 iconfont.ttf 文件放到该文件夹下,然后打开 pubspec.ymal 文件,注册下导入的资源,可以自己命名 iconfont.ttf 文件名,便于自己发现就行,例如我命名为 third_part_icon.ttf,在注册图片下面继续添加

fonts:
  - family: ThirdPartIcons
    fonts:
    - asset: fonts/third_part_icon.ttf

注册完了记得点击 Package get,否则会找不到资源。接着新建个 third_icons.dart 文件

import 'package:flutter/material.dart';

class ThirdIcons {
  // codePoint 值通过打开 `demo_index.html` 获取
  // 会在相应 icon 下带有相应的 code,把 `&#` 替换成 `0`,然后去掉最后的 `;` 即可
  // 例如 &#xe672; 对应我们需要的图标就是 0xe672
  static const IconData username = ThirdIconData(0xe672);
  static const IconData password = ThirdIconData(0xe62f);
}

class ThirdIconData extends IconData {
  // fontFamily 就是我们在 `pubspec.yaml` 中注册的 family 值
  const ThirdIconData(int codePoint) : super(codePoint, fontFamily: 'ThirdPartIcons');
}

接下来就可以通过该类导入需要的第三方图标了。

导入第三方插件

其实 Flutter 中缺少很多功能,需要通过导入第三方插件来实现功能,插件就是 Flutter 和原生交互的桥梁,也就是说,要写 Flutter 的插件,需要写 AndroidiOS 两端代码才可,否则只有在其中一个端能够实现功能。好在有很多现成的插件已经开源,可以通过 FlutterPackage 搜索到,例如等会我们会需要用到 FlutterToast 这个插件,用来做提醒用,在 FlutterPackage 中搜索到插件后,打开项目中的 pubspec.ymal 文件,在 dependencies 类目下将 fluttertoast 插件引入,如图:

插件导入展示.png

然后点击 Package get 让其导入即可,别的插件也是这样导入。做好准备工作,我们就可以撸一个登录界面了~

撸一个登录界面

在开撸之前,我们先看下最终的效果图吧,虽然是比较常用的界面(因为图都是自己搞的,比较丑请不要介意)

登录展示.png
注册展示.png

因为两个界面比较相似,所以这边只贴外层的代码和登录的代码,具体的代码,可以查看源码,已经推到 Github

void main() {
  runApp(LoginApp());

  if (Platform.isAndroid) {
    var style = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
    SystemChrome.setSystemUIOverlayStyle(style);
  }
}

/// 外层界面,包裹登录界面和注册界面,使用的都是前面讲过的,忘记可以查看之前的章节
class LoginApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Login Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.lightBlue),
      home: LoginHomePage(),
    );
  }
}

class LoginHomePage extends StatefulWidget {
  @override
  _LoginHomePageState createState() => _LoginHomePageState();
}

class _LoginHomePageState extends State<LoginHomePage> with SingleTickerProviderStateMixin {
  TabController _tabController;
  List<String> _pageIndicators = ['登录', '注册'];
  List<Widget> _pages = [];
  int _position = 0;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _pageIndicators.length, vsync: this);
    // 将登录界面和注册界面添加到列表,用于放到 IndexStack 的 children 属性
    _pages..add(LoginPage())..add(RegisterPage());

    _tabController.addListener(() {
      // 当 tab 切换的时候,联动 IndexStack 的 child 页面也进行修改,通过 setState 来修改值
      if (_tabController.indexIsChanging) setState(() => _position = _tabController.index);
    });
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 先忽略...
    return Theme(
        data: ThemeData(primarySwatch: Colors.pink, iconTheme: IconThemeData(color: Colors.pink)),
        child: Scaffold(
          body: Container(
            padding: const EdgeInsets.all(20.0),
            alignment: Alignment.center,
            decoration:
                BoxDecoration(image: DecorationImage(image: AssetImage('images/login_bg.png'), fit: BoxFit.cover)),
            
            // 先忽略...下面会讲,主要是解决软键盘弹出的时候,界面内容会溢出的问题
            child: SingleChildScrollView(
              child: SafeArea(
                  child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
                // 顶部页面切换指示器,代码可以参考 `app_bar_main.dart` 文件
                TabBar(
                    indicatorSize: TabBarIndicatorSize.label,
                    controller: _tabController,
                    indicatorWeight: 4.0,
                    indicatorColor: Colors.white,
                    // 返回 tab 列表
                    tabs: _pageIndicators
                        .map((v) => Text(v, style: TextStyle(color: Colors.white, fontSize: 24.0)))
                        .toList()),
                Padding(
                    padding: const EdgeInsets.only(top: 30.0),
                    child: SizedBox(
                        // 切换界面列表
                        child: IndexedStack(children: _pages, index: _position),
                        // 指定高度
                        height: MediaQuery.of(context).size.height / 2))
              ])),
            ),
          ),
        ));
  }
}
/// 登录界面
class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  // 用于后面判断表单内容是否有效
  GlobalKey<FormState> _formKey = GlobalKey();
  // 用于获取输入框的内容
  TextEditingController _usernameController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    // 防止内存溢出,记得销毁..销毁..销毁
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  _login() {
    // 取消焦点
    FocusScope.of(context).requestFocus(FocusNode());

    // 判断表单是否有效
    if (_formKey.currentState.validate()) {
      // 获取输入框内容
      var username = _usernameController.value.text;
      var password = _passwordController.value.text;

      // 判断登录条件
      if (username == 'kuky' && password == '123456')
        // 引入的三方插件方法,`Flutter` 没有自带的 `Taost`
        Fluttertoast.showToast(msg: '登录成功');
      else
        Fluttertoast.showToast(msg: '登录失败');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
        // 将 key 设置给表单,用于判断表单是否有效
        key: _formKey,
        child: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 4.0),
              // 表单输入框,参数同 TextField 基本类似
              child: TextFormField(
                controller: _usernameController,
                style: TextStyle(color: Colors.white, fontSize: 16.0),
                decoration: InputDecoration(
                    icon: Icon(ThirdIcons.username, size: 24.0, color: Colors.white),
                    labelText: '请输入用户名',
                    labelStyle: TextStyle(color: Colors.white),
                    helperStyle: TextStyle(color: Colors.white)),
                // 有效条件(为空不通过,返回提示语,通过返回 null)
                validator: (value) => value.trim().isEmpty ? '用户名不能为空' : null,
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 4.0),
              child: TextFormField(
                obscureText: true,
                controller: _passwordController,
                style: TextStyle(color: Colors.white, fontSize: 16.0),
                decoration: InputDecoration(
                    icon: Icon(ThirdIcons.password, size: 24.0, color: Colors.white),
                    labelText: '请输入密码',
                    labelStyle: TextStyle(color: Colors.white),
                    helperStyle: TextStyle(color: Colors.white)),
                validator: (value) => value.trim().length < 6 ? '密码长度不能小于6位' : null,
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(top: 20.0),
              child: SizedBox(
                // 主要用于使 RaisedButton 和上层容器同宽
                width: MediaQuery.of(context).size.width,
                child: RaisedButton(
                    color: Colors.pink,
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))),
                    onPressed: _login,
                    child: Text(
                      '登录',
                      style: TextStyle(color: Colors.white, fontSize: 20.0),
                    )),
              ),
            )
          ],
        ));
  }
}

撸完界面后,可以试下登录效果,如果输入框的内容,和 TextFormFieldvalidator 的条件不符合,则会显示错误文字的提示

登录错误展示.png

如果按照条件用户名为 kuky 密码为 123456 (条件可以根据自己进行修改)则会显示登录成功的逻辑

登录成功展示.png

以上代码查看 login_home_page.dart 文件

注册界面的逻辑和登录界面的逻辑几乎一样,算是第一次实战了,望小伙伴能够好好的写一遍

最后代码的地址还是要的:

  1. 文章中涉及的代码:demos

  2. 基于郭神 cool weather 接口的一个项目,实现 BLoC 模式,实现状态管理:flutter_weather

  3. 一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,但是还是做了自己的修改,很多地方看着不舒服,然后就改成自己的实现方式了):flutter_shop

如果对你有帮助的话,记得给个 Star,先谢过,你的认可就是支持我继续写下去的动力~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容