5

上一篇文章中 ListView 嵌套 ListView 滚动问题提到 大家平时都做过问卷、试卷,知道题目列表包含:大题的题目,小题的题目,小题的选项等,做这种布局需要用到不少组件,首先是列表ListView来显示题目列表和选项列表,选项中包括单选和多选,这就用到了Radio和Checkbox,题目中有些是简答题,这就用到了文本框TextField,可以设置多行或者单行。

具体实现

具体逻辑用语言文字描述起来太费劲,直接上代码吧,有些逻辑承接上一篇文章,这里就省略掉了。

  // 详细信息 初始化默认为""
  Map questionnaireDetail = {
    "title": '',
    "startDate": '',
    "endDate": '',
    "remark": '',
  };

  // 列表视图(`ListView`)中要显示的数据。
  List questionList = new List();

  ScrollController _scrollController = new ScrollController();
 
  // 调用接口获取详情和题目选项数据,具体代码逻辑略
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("问卷详情"),
      ),
      body: GestureDetector(
        behavior: HitTestBehavior.translucent,
        onTap: () {
          //点击空白处取消TextField焦点 触摸收起键盘
          FocusScope.of(context).requestFocus(FocusNode());
        },
        child: new ListView(
          shrinkWrap: true, //是否根据子widget的总长度来设置ListView的长度,默认值为false
          controller: _scrollController,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                   new Text(
                    questionnaireDetail["title"],
                    overflow: TextOverflow.ellipsis, // 文字超长时显示为省略号...
                    maxLines: 2, // 设置最多显示两行文字
                    style: TextStyle(
                      color: Color.fromRGBO(0, 0, 0, 1.0), //opacity:不透明度
                      fontFamily: 'PingFangBold',
                      fontSize: 15.0,
                    ),
                  ),
                  Container(
                    child: _buildList(),
                  ),
                 ],
              ),
            ),
         ],
        ),
      ),
    );
  }
Widget _buildList() {
    return ListView.builder(
      shrinkWrap: true, //是否根据子widget的总长度来设置ListView的长度,默认值为false
      physics: new NeverScrollableScrollPhysics(), // 禁用问题列表子组件的滚动事件
      //itemCount +1 为了显示加载中和暂无数据progressbar
      itemCount: questionList.length + 1,
      itemBuilder: (context, index) {
          // 列表显示
          return Container(
            padding: new EdgeInsets.fromLTRB(10, 5, 10, 5),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                 new Text(
                  questionList[index]["title"]),
                Offstage(
                  // 控制 最多可选maxChoice项组件 是否隐藏
                  offstage: questionList[index]['questionType'] == "多选题" &&
                          questionList[index]["maxChoice"] != 0
                      ? false
                      : true,
                  child: new Text('(最多可选:' +
                      questionList[index]["maxChoice"].toString() +
                      "项)"),
                ),
                questionList[index]['questionType'] == "单选题"
                    ? _buildRadioChoiceRow(questionList[index])
                    : questionList[index]['questionType'] == "多选题"
                        ? _buildCheckboxChoiceRow(questionList[index],
                            questionList[index]["maxChoice"])
                        : _buildTextControllerRow(questionList[index]),
              ],
            ),
          );
          },
    );
  }
  // 构建 单选框Radio 单选题选项列表 组件
  Widget _buildRadioChoiceRow(question) {
    return new ListView.builder(
      physics: new NeverScrollableScrollPhysics(), // 禁用选项列表子组件的滚动事件
      shrinkWrap: true, //是否根据子widget的总长度来设置ListView的长度,默认值为false
      itemCount: question['options'].length,
      itemBuilder: (context, index) {
        var optionContent = question['options'][index]["optionContent"];
        if (optionContent.indexOf("#OTHER#") == -1) {
          // print('不是其他: ' + optionContent.indexOf("#OTHER#").toString());
          return _radioListItem(question, optionContent, index, optionContent);
        } else {
          // 其他选项 带输入框
          var radioTitle = optionContent.replaceAll("#OTHER#", "");
          // print('其他的文字: ' + radioTitle);
          // print('其他: ' + optionContent.indexOf("#OTHER#").toString());
          return Row(
            children: <Widget>[
              Container(
                width: 150,
                child:
                    _radioListItem(question, optionContent, index, radioTitle),
              ),
              Container(
                width: MediaQuery.of(context).size.width - 200,
                color: const Color(0xFFFFFFFF),
                // 其他选项  输入框
                child: _buildTextOtherController(question),
              )
            ],
          );
        }
      },
    );
  }
  Widget _radioListItem(question, optionContent, optionIndex, radioTitle) {
    return new Row(
      children: <Widget>[
        // 此处也可以使用RadioListTile,但是这个组件不满足我们这边的需求,所以自己后来写了布局
        new Radio(
          value: question['options'][optionIndex]['id'], // 该值为string类型
          groupValue: question['groupValue'], // 与value一样是选中
          onChanged: (val) {
            // 收起键盘
            FocusScope.of(context).requestFocus(FocusNode());
            setState(() {
              question['groupValue'] = val;
              // print('选中了: ' + val.toString());
            });
          },
        ),
        Expanded( // Row的子元素Text实现换行 需要加Expanded
          child: Text(
            radioTitle,
            softWrap: true, // 自动换行
          ),
        ),
      ],
    );
  }
  // 构建 复选框Checkbox 多选题选项列表 组件
  Widget _buildCheckboxChoiceRow(question, maxChoice) {
    return new ListView.builder(
      physics: new NeverScrollableScrollPhysics(), // 禁用选项列表子组件的滚动事件
      shrinkWrap: true, //是否根据子widget的总长度来设置ListView的长度,默认值为false
      itemCount: question['options'].length,
      itemBuilder: (context, index) {
        var optionContent = question['options'][index]["optionContent"];

        if (optionContent.indexOf("#OTHER#") == -1) {
          return _checkboxListItem(
              question, maxChoice, optionContent, index, optionContent);
        } else {
          // 其他选项 带输入框
          var checkboxTitle = optionContent.replaceAll("#OTHER#", "");
          // print('其他的文字: ' + checkboxTitle);
          return new Row(
            children: <Widget>[
              Container(
                width: 150,
                child: _checkboxListItem(
                    question, maxChoice, optionContent, index, checkboxTitle),
              ),
              Container(
                width: MediaQuery.of(context).size.width - 200,
                color: const Color(0xFFFFFFFF),
                // 其他选项  输入框
                child: _buildTextOtherController(question),
              )
            ],
          );
        }
      },
    );
  }
  Widget _checkboxListItem(
      question, maxChoice, optionContent, optionIndex, checkboxTitle) {
    return new Row(
      children: <Widget>[
        // 此处也可以使用CheckboxListTile,但是这个组件不满足我们这边的需求,所以后来自己写了布局
        Checkbox(
          value: question['options'][optionIndex]
              ['isCheck'], // 该值为bool类型 false即不选中
          onChanged: (isCheck) {
            // 收起键盘
            FocusScope.of(context).requestFocus(FocusNode());
            _checkMaxChoise(question, maxChoice, optionIndex, isCheck);
          },
        ),       
        Expanded( // Row的子元素Text实现换行 需要加Expanded
          child: Text(
            checkboxTitle,
            softWrap: true, // 自动换行
          ),
        ),
      ],
    );
  }
  // 多选题 判断maxChoice最多选择项的逻辑
  void _checkMaxChoise(question, maxChoice, optionIndex, isCheck) {
    setState(() {
      var optionId = question['options'][optionIndex]['id'];
      question['options'][optionIndex]['isCheck'] = isCheck;
      if (isCheck) {
        // print('选中了: ' + optionId);
        question['checked'].add(optionId);
        if (maxChoice != 0 && question['checked'].length > maxChoice) {
          question['checked'].remove(optionId);
          question['options'][optionIndex]['isCheck'] = false;
          showToast("当前选中数已超过本题的最大选项数");
        }
        // print('选中的: ' + question['checked'].toString());
      } else {
        question['checked'].remove(optionId);
        // print('选中的: ' + question['checked'].toString());
      }
    });
  }
  // 构建 输入框行 简答题 组件
  Widget _buildTextControllerRow(question) {
    return new Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        color: const Color(0xFFFFFFFF),
        padding: EdgeInsets.only(left: 8.0),
        child: _buildTextField(question['textController']),
    );
  }
  // 构建 选项的其他输入框 组件
  Widget _buildTextOtherController(question) {
    return _buildTextField(question['textOtherController']);
  }
  // 构建 输入框 组件
  Widget _buildTextField(controller) {
    // 文本字段(`TextField`)组件,允许用户使用硬件键盘或屏幕键盘输入文本。
    return new TextField(
      cursorColor: const Color(0xFFFE7C30),
      cursorWidth: 2.0,
      keyboardType: TextInputType.multiline, //多行
      decoration: InputDecoration(
        contentPadding: EdgeInsets.all(10.0),
        // 圆角矩形的边框
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(10.0),
        ),
      ),
      controller: controller, // 控制正在编辑的文本
    );
  }

效果截屏

1.png
3.jpg
4.jpg

参考资料

TextField class
Radio<T> class
Checkbox class
flutter笔记(九)-----单选框Radio、RadioListTile
Flutter学习之旅——实用入坑指南

Flutter 点击空白处取消TextField焦点并收起键盘

相关文章

Flutter 常用ListView和RefreshIndicator组件实现列表页面的上拉加载更多下拉刷新


北堂棣
6.6k 声望116 粉丝

2016年应届毕业生。