在上一篇文章中 ListView 嵌套 ListView 滚动问题提到 你们平时都作过问卷、试卷,知道题目列表包含:大题的题目,小题的题目,小题的选项等,作这种布局须要用到很多组件,首先是列表ListView来显示题目列表和选项列表,选项中包括单选和多选,这就用到了Radio和Checkbox,题目中有些是简答题,这就用到了文本框TextField,能够设置多行或者单行。html
具体逻辑用语言文字描述起来太费劲,直接上代码吧,有些逻辑承接上一篇文章,这里就省略掉了。segmentfault
// 详细信息 初始化默认为"" 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, // 控制正在编辑的文本 ); }
TextField class
Radio<T> class
Checkbox class
flutter笔记(九)-----单选框Radio、RadioListTile
Flutter学习之旅——实用入坑指南api
Flutter 点击空白处取消TextField焦点并收起键盘app