Flutter劫富济贫计划(一)——flutter布局实战

mac2025-10-29  14

本计划 就是帮助 大家学习Flutter 的计划, 在这里我会分享出一些APP中常用到的页面,记得点击关注~

登录页面

效果截图

import 'dart:async'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:project/plugins/Plugins.dart'; // import 'package:project/plugins/timer.dart'; class LoginPage extends StatefulWidget { @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { TextEditingController _phone = new TextEditingController(); // 手机号、验证码 TextEditingController _verification = new TextEditingController(); // 手机号、验证码 Timer _timer; int _countdownTime = 0; bool _isget; // 验证码Widget bool _verification_available; // 输入框是否可用 Widget getVerification(){ if(_isget){ return Text('获取验证码'); }else{ return Text('${_countdownTime}秒后获取'); } } // 倒计时 void startCountdownTimer() { const oneSec = const Duration(seconds: 1); var callback = (timer) => { setState(() { if (_countdownTime < 1) { _timer.cancel(); setState(() { _isget = true; }); } else { _countdownTime = _countdownTime - 1; // 倒计时 自减 } }) }; _timer = Timer.periodic(oneSec, callback); } // 初始化 @override void initState() { super.initState(); // _phone.addListener((){ // print(_phone.text); // }); _verification_available = false; _isget = true; } // 销毁时 @override void dispose() { super.dispose(); if (_timer != null) { _timer.cancel(); } } @override Widget build(BuildContext context) { return Scaffold( // appBar: AppBar( // backgroundColor:Colors.white, // leading:IconButton( // icon: Icon( // Theme.of(context).platform == TargetPlatform.iOS ? Icons.arrow_back_ios : Icons.arrow_back, // color: Theme.of(context).accentColor, // ), // onPressed: (){ // Navigator.of(context).pop(); // }, // ) // ), body: Container( width:double.infinity, margin: EdgeInsets.symmetric(horizontal: 25.0), padding: EdgeInsets.fromLTRB(0, 150.0, 0, 0), child: Column( children: <Widget>[ Container( child: Text('欢迎登录',style: TextStyle( fontSize: 30.0, fontWeight: FontWeight.bold )), margin: EdgeInsets.fromLTRB(0, 0, 0, 40.0), ), TextField( // autofocus: true, decoration: InputDecoration( hintText: "请输入手机号", // errorText: , ), maxLength: 11, controller: _phone, // 表示默认值 onChanged:(value){ _phone.text = value; // 判断如果手机号可用才能进行验证码操作 if(Plugins.isChinaPhoneLegal(value)){ setState(() { _verification_available = true; }); }else{ setState(() { _verification_available = false; }); } // 将光标一直置于末尾 _phone.selection = TextSelection.fromPosition( TextPosition(offset: _phone.text.length) ); }, onSubmitted: (val){ print(_phone.text); print("点击了键盘上的动作按钮,当前输入框的值为:${val}"); }, keyboardType:TextInputType.number ), SizedBox(height: 20.0), Row( children: <Widget>[ Expanded( child: TextField( decoration: InputDecoration( hintText: '输入验证码', ), enabled:_verification_available ? true : false, controller: _verification, onChanged: (val){ _verification.text = val; _verification.selection = TextSelection.fromPosition( TextPosition(offset: _verification.text.length) ); }, onSubmitted: (val){ print("点击了键盘上的动作按钮,当前输入框的值为:${val}"); }, keyboardType:TextInputType.number ), flex: 2, ), Expanded( child: RaisedButton( child: getVerification(), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25) ), color: Theme.of(context).accentColor, textColor: Colors.white, onPressed: (){ _verification_available ? Fluttertoast.showToast( msg: '手机号是${_phone.text}' ) : Fluttertoast.showToast( msg: '请输入正确的手机号' ); if (_verification_available && _countdownTime == 0) { setState(() { _countdownTime = 60; _isget = false; }); //开始倒计时 startCountdownTimer(); } // print('手机号是${_phone.text}, 验证码是${_verification.text}'); }, ), flex: 1, ) ], ), SizedBox(height: 30.0), Row( children: <Widget>[ Expanded( child: Container( height: 50.0, child: RaisedButton( child: Text('登录',style: TextStyle(),), color: Theme.of(context).accentColor, textTheme: ButtonTextTheme.primary, onPressed: () { _verification_available ? Fluttertoast.showToast( msg: '手机号是${_phone.text}, 验证码是${_verification.text}' ) : Fluttertoast.showToast( msg: '请输入正确手机号或输入正确验证码' ); }, elevation: 10, shape: RoundedRectangleBorder( borderRadius:BorderRadius.circular(10) ), ), ), ) ], ) ], ), ), ); } }

数据展示页面

数据是我随便编的。。。 不要举报我 o(╥﹏╥)o

效果截图

Widget recommendWidget(){ // var str = '{"img":"http://img2.imgtn.bdimg.com/it/u=1953606852,863263430&fm=26&gp=0.jpg", "title":"你好,我是司机,我会开汽车,火车,飞机", "price":160, "rommond":16}'; var itemWidth = (ScreenAdaper.getScreenWidth() - 20) /2;// 每一个网格的宽度 List<Map> data = [ {"img":"https://www.itying.com/images/flutter/list1.jpg", "title":"你好,我是素馨,我会开汽车,火车,飞机", "price":560, "rommond":12}, {"img":"https://www.itying.com/images/flutter/list2.jpg", "title":"你好,我是梓潼,我会开火车,轮船", "price":360, "rommond":35}, {"img":"https://www.itying.com/images/flutter/list3.jpg", "title":"你好,我是疏影,我会开赛车,巡洋舰", "price":130, "rommond":13}, {"img":"https://www.itying.com/images/flutter/list4.jpg", "title":"你好,我是青岚,我会开坦克,航母,航天飞行器", "price":999, "rommond":5}, {"img":"https://www.itying.com/images/flutter/list5.jpg", "title":"你好,我是若兰,我会开大炮,骑行车", "price":420, "rommond":32}, {"img":"https://www.itying.com/images/flutter/list6.jpg", "title":"你好,我是甜甜,我会开客机,快艇", "price":500, "rommond":160}, {"img":"https://www.itying.com/images/flutter/list7.jpg", "title":"你好,我是萱萱,我会吃鸡", "price":700, "rommond":1850}, ]; return Container( padding: EdgeInsets.symmetric(horizontal: 5), // 两边内边距 margin: EdgeInsets.only(bottom: ScreenAdaper.setWidth(10)), child: Wrap( spacing: 10, // 平行距离 runSpacing: 10, // 上下距离 children: data.map((value){ return Container( width: itemWidth, padding: EdgeInsets.all(ScreenAdaper.setWidth(10)), decoration: BoxDecoration( border: Border.all( color:Colors.black12, width: 1 ) ), child: Column( children: <Widget>[ Container( width: double.infinity, // 自适应占满全部 child: AspectRatio( aspectRatio: 1/1, // 图片宽高比 1/1 child: Image.network(value['img'], fit: BoxFit.cover,), // 图片自适应 ) ), Padding( padding: EdgeInsets.all(ScreenAdaper.setWidth(5)), child:Text( value['title'], maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(color:Colors.black45), ), ), Padding( padding: EdgeInsets.all(ScreenAdaper.setWidth(5)), child: Row( children: <Widget>[ Expanded( child: Text('¥'+value['price'].toString()+'/夜', textAlign:TextAlign.left,style: TextStyle(color: Colors.redAccent, fontSize: 15.0),), flex: 1, ), Expanded( child: Text('已完成'+value['rommond'].toString()+'单',textAlign:TextAlign.right,style: TextStyle(fontSize: 12.0)), flex: 1, ) ], ), ) ], ), ); }).toList() ), ); }

新闻展示页面

Widget Article(){ List<Map> data = [ {'title':'今晚油价要涨了!加满一箱多花4元', 'source':'人民日报', 'time':'17分钟前', 'img':'https://p3.pstatp.com/list/190x124/pgc-image/RgqIEDmFZZCSg6'}, {'title':'清华线性代数改为英文教材,网友:中文版我都看不懂', 'source':'中国教育报', 'time':'25分钟前', 'img':'https://p9.pstatp.com/list/190x124/pgc-image/53731d7bf38541f48572e561e7ea0e24'}, {'title':'央视评机长被停职:将女乘客“升舱”,谁给的胆子?', 'source':'中国教育报', 'time':'33分钟前', 'img':'https://p1.pstatp.com/list/190x124/pgc-image/RgqMRKyAA8aZ0K'}, {'title':'孩子不睡觉,为什么打骂哭完后就睡了,真相可能跟你想的不一样', 'source':'光明网', 'time':'49分钟前', 'img':'https://p9.pstatp.com/list/190x124/pgc-image/RgpWgJGCPor2Yr'}, {'title':'回锅肉一份400元?商家回应:一份要用四斤多肉', 'source':'中国网', 'time':'1小时前', 'img':'https://p3.pstatp.com/list/190x124/pgc-image/Rgomoy1CnbRosl'}, {'title':'一年级小学生的“散装”拼音,让人笑破肚皮,家长:不会教', 'source':'光明网', 'time':'1小时前', 'img':'https://p1.pstatp.com/list/190x124/pgc-image/RgpWdrkC7LcYO'}, {'title':'夫妻双双膝盖坏掉,竟因这个坚持10年的习惯', 'source':'中国经济网', 'time':'1小时前', 'img':'https://p1.pstatp.com/list/190x124/pgc-image/RgrGGZBEjH2HFa'}, {'title':'公安部A级通缉令通缉20名重大黑恶在逃人员', 'source':'法制日报', 'time':'1小时前', 'img':'https://p3.pstatp.com/list/190x124/pgc-image/RgrLxiwDY0Z84u'}, ]; return Padding( padding: EdgeInsets.all(10), child: Column( children: data.map((value){ return Container( margin: EdgeInsets.only(bottom: ScreenAdaper.setHeight(20)), padding: EdgeInsets.all(5), decoration: BoxDecoration( border: Border( bottom:BorderSide( width: 0.5, color: Colors.black12 ) ) ), child:Row( children: <Widget>[ Expanded( flex: 1, child: Container( height: ScreenAdaper.setHeight(180), margin: EdgeInsets.only(left:10), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, //表示元素之间的间距一样(和flex布局中的between一样) crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( value['title'], maxLines: 2, overflow: TextOverflow.ellipsis, ), Row( children: <Widget>[ Container( height: ScreenAdaper.setHeight(36), margin: EdgeInsets.only(right: 10), padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // 如果Container里面加上decoration属性,这个使用color一定要放到decoration当中 decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color.fromRGBO(230, 230, 230, 0.9) ), child: Text(value['source']), ) ], ), Text( value['time'], style: TextStyle(color:Colors.black12, fontSize: 16), ) ], ), ), ), Container( width: ScreenAdaper.setWidth(180), height: ScreenAdaper.setHeight(180), child: Image.network(value['img'], fit:BoxFit.cover), ), ], ) ); }).toList() ) ); }

搜索页面

因为该页面牵扯到本地存储,如果不清楚的同学,请看我另外一篇配置本地存储的文章文章地址 另外引入了屏幕适配screenUtils,具体可看这篇文章文章地址

完成的功能有:

本地存储搜索数据关键字搜索匹配搜索历史删除清空全部搜索历史点击后一键搜索

import 'package:flutter/material.dart'; import 'package:project/plugins/ScreenAdapter.dart'; // 屏幕适配 import 'package:project/plugins/PublicStorage.dart'; // 本地存储搜索数据 import 'package:fluttertoast/fluttertoast.dart'; // 提示 import 'asset.dart'; // 模拟数据 class SearchPage extends StatefulWidget { @override _SearchPageState createState() => _SearchPageState(); } class _SearchPageState extends State<SearchPage> { // var _keywords = ''; TextEditingController _keywords = TextEditingController(); List _historyListData = []; var isSearch = false; List data = ['lorem', 'loremasd', 'HelloWorld', '睡觉觉啊', '袭击我去呜呜呜', '上世纪']; @override void initState() { super.initState(); this._getHistoryData(); } // 获取本地存储 _getHistoryData() async { var _historyListData = await PublicStorage.getHistoryList('searchList'); // print('获取本地存储${_historyListData}'); setState(() { this._historyListData = _historyListData; }); } // 弹出框 _alertDialog(keywords) async { var result = await showDialog( barrierDismissible: false, // 表示点击灰色背景的时候是否消失弹出框 context: context, builder: (context) { return AlertDialog( title: Text('提示信息'), content: Text('您确定要删除吗?'), actions: <Widget>[ FlatButton( child: Text('取消'), onPressed: () { print('点击了取消'); Navigator.pop(context, 'Cancle'); }, ), FlatButton( child: Text('确定'), onPressed: () async { // 只有异步才能删掉 await PublicStorage.removeHistoryData('searchList', keywords); this._getHistoryData(); Navigator.pop(context, 'Ok'); }, ) ], ); }); } // 历史记录控件 Widget _historyListWidget() { if (_historyListData.length > 0) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( child: Text( '历史记录', style: TextStyle(fontSize: ScreenAdapter.size(30)), ), ), Divider(), Column( children: this._historyListData.map((value) { return Column( children: <Widget>[ ListTile( title: Text("${value}"), // 长按 onLongPress: () { this._alertDialog("${value}"); }, ), Divider() ], ); }).toList()), SizedBox( height: ScreenAdapter.setHeight(20), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ InkWell( child: Container( width: ScreenAdapter.setWidth(500), height: ScreenAdapter.setHeight(60), decoration: BoxDecoration( border: Border.all( color: Color.fromRGBO(233, 233, 233, 0.9), width: 1)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Icon(Icons.delete_outline), Text('清空历史记录') ], ), ), onTap: () { PublicStorage.clearHistoryList('searchList'); _getHistoryData(); }, ) ], ) ], ); } else { return Text(""); } } // 是否展示搜索推荐 Widget _isShowRecommend(){ // 如果没有数据,就返回推荐数组,如果有数据就返回数组当中查找到的 final suggestionList = _keywords.text.isEmpty ? recentSuggest : searchList.where((input) => input.startsWith(_keywords.text)).toList(); if(!isSearch){ // 没有输入搜索内容 return Padding( padding: EdgeInsets.all(ScreenAdapter.setWidth(15)), child: ListView( children: <Widget>[ Container( child: Text( '热搜', style: TextStyle(fontSize: ScreenAdapter.size(30)), ), ), Divider(), Wrap( children: data.map((value) { return InkWell( child: Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), decoration: BoxDecoration( color: Color.fromRGBO(233, 233, 233, 0.9), borderRadius: BorderRadius.circular(20)), child: Text(value), ), onTap: (){ // print(value); setState(() { _keywords.text = value; isSearch = true; searchMetod(); print(_keywords.text); }); }, ); }).toList()), SizedBox( height: ScreenAdapter.setHeight(20), ), // 历史记录 _historyListWidget() ], ), ); }else{ // 输入搜索内容时 return ListView.builder( itemCount: suggestionList.length, itemBuilder: (context, index){ return InkWell( child: ListTile( title: RichText( text: TextSpan( text: suggestionList[index].substring(0, _keywords.text.length), style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold), children: [ TextSpan( text: suggestionList[index].substring(_keywords.text.length), style: TextStyle(color: Colors.grey)) ])), ), onTap: (){ // print(suggestionList[index]); setState(() { _keywords.text = suggestionList[index]; isSearch = true; searchMetod(); print(_keywords.text); }); }, ); } ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Container( height: ScreenAdapter.setHeight(60), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(30)), padding: EdgeInsets.only(left: 10), child: TextField( controller: _keywords, autofocus: false, // 去掉boder的默认边框 decoration: InputDecoration( icon: Icon(Icons.search), border: OutlineInputBorder(borderSide: BorderSide.none), hintText: '关键字', contentPadding: EdgeInsets.fromLTRB(0, 10, 0, 10)), onChanged: (value) { if(value!= ''){ setState(() { isSearch = true; this._keywords.text = value; }); }else{ setState(() { isSearch = false; }); } // 如果绑定了控制器,可利用此方法避免文本框的Bug _keywords.selection = TextSelection.fromPosition( TextPosition(offset: _keywords.text.length) ); }, )), actions: <Widget>[ InkWell( child: Container( height: ScreenAdapter.setHeight(60), width: ScreenAdapter.setWidth(80), child: Row( children: <Widget>[Text('搜索')], ), ), onTap: searchMetod ) ], ), body: _isShowRecommend() ); } searchMetod() async { if(isSearch){ // 等待本地存储 await PublicStorage.setHistoryList('searchList',this._keywords.text); // 本地存储过后,在搜索页面再一次获取一遍数据 await this._getHistoryData(); Navigator.pushNamed(context, '/register'); }else{ Fluttertoast.showToast( msg: '请输入内容' ); } } }
最新回复(0)