1.
自定义组件开发: * 看是否是继承statelesswidget还是stafulwidget * 如果是纯展示的,没有和用户交互的就用statelesswidget, * 但是例如和用户交互例如搜索框,就用stafulwidget
2.
StatelessWidget继承自widget,而@immutable abstract class Widget extends DiagnosticableTree, 其中@immutable注解标识了其子类不可变,所以下面的属性用final
3.
当2层会有重叠,用普通的BoxDecrotion会被上面的视图盖住,所以会看不出圆角的效果。 解决这个问题就使用PhysicalModel,这是flutter专门为我们提供的。@override Widget build(BuildContext context) { return PhysicalModel( child: Column( children: _gridNavItems(context), ), color: Colors.transparent,//透明 borderRadius: BorderRadius.circular(6), clipBehavior: Clip.antiAlias,//裁切 ); }
4.
stack布局是后面的一个覆盖在前面的一个上面
5.
Dart语法中,forEach不要返回值。map要返回值。
_items(BuildContext context) { if (localNavList == null) return null; Listitems = []; localNavList.forEach((model) { //forEach不要返回值。map要返回值 items.add(_item(context, model)); });
6.
mainAxisAlignment: MainAxisAlignment.spaceAround,//spaceAround是平均排列
7.下划线效果:
decoration: BoxDecoration( //下划线 border: Border( bottom: BorderSide(width: 1, color: Color(0xfff2f2f2)))),
8.获取手机屏幕的宽度:
child: Image.network( model.icon, fit: BoxFit.fill, /** * 获取手机屏幕的宽度:MediaQuery.of(context).size.width */ width: MediaQuery.of(context).size.width/2-10,//减去10实际上是减去padding,减去多一点防止屏幕右边溢出 height: big? 129 : 80, ),
9.封装webview这个Widget:
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; /** * @date on 2019/5/11 * @author Yinlei * @packagename * @email [email protected] * @describe 引入webview插件,并重新封装他。. */ //自定义url白名单[协程的首页] const CATCH_URLS = ['m.ctrip.com/', 'm.ctrip.com/html5/', 'm.ctrip.com/html5']; class WebView extends StatefulWidget { final String url; final String statusBarColor; final String title; final bool hideAppBar; final bool backForbid;//可空的,所以在构造函数中应该为它增加默认值 WebView( {this.url, this.statusBarColor, this.title, this.hideAppBar, this.backForbid = false}); @override _WebViewState createState() => _WebViewState(); } class _WebViewState extends State{ final webviewReference = FlutterWebviewPlugin(); StreamSubscription _onUrlChanged; StreamSubscription _onStateChanged; StreamSubscription _onHttpError; bool exiting = false;//是否退出当前网页 @override void initState() { super.initState(); webviewReference.close(); _onUrlChanged=webviewReference.onUrlChanged.listen((String url){ }); _onStateChanged=webviewReference.onStateChanged.listen((WebViewStateChanged state){ switch(state.type){ case WebViewState.startLoad: //这里是混合开发|。如果跳转到webview后,加载的页面如果按顶部appbar的返回键的时候会返回到携程网站的首页,所以要拦截让他返回我们的首页 if(_isToMain(state.url)&&!exiting){ if(widget.backForbid){//根据接口的字段,如果他是禁止返回就还是加载当前页面 //意思就是它的上一级的url没有可以返回的或者说是禁止返回的就还是重复加载当前页面 webviewReference.launch(widget.url); }else{ Navigator.pop(context); exiting =true;//防止重复返回 } } break; default: break; } }); _onHttpError=webviewReference.onHttpError.listen((WebViewHttpError error){ print(error); }); } /** * 判断跳转的html的url是不是携程网的一些首页。 */ _isToMain(String url){ bool contain = false;//默认不在白名单 for(final value in CATCH_URLS){ if(url?.endsWith(value)??false){//url不为空且在白名单里面 contain =true; break; } } return contain; } @override void dispose() { _onHttpError.cancel(); _onUrlChanged.cancel(); _onStateChanged.cancel(); webviewReference.dispose(); super.dispose(); } @override Widget build(BuildContext context) { //颜色的定义 String statusBarColorStr = widget.statusBarColor ?? 'ffffff'; Color backButtonColor; if(statusBarColorStr == 'ffffff'){ backButtonColor = Colors.black; }else{ backButtonColor = Colors.white; } return Scaffold( body: Column( children: [ //一个完整的颜色有8位,后4位是RGB,0x代表十六进制,0x后的2位数字代表alpha透明度 _appBar(Color(int.parse('0xff'+statusBarColorStr)),backButtonColor), //webview要展示的内容,让他充满整个界面 Expanded(child: WebviewScaffold( userAgent: 'null', url: widget.url, withZoom: true,//缩放 withLocalStorage: true,//缓存 hidden: true, //默认为隐藏 initialChild: Container( color: Colors.white, child: Center( child: Text('你瞅啥,我就要让你等一会儿,不服打我啊~'), ), ),//默认隐藏的时候初始化的界面 )) ], ), ); } /** * 自定义appbar */ _appBar(Color backgroundColor,Color backButtonColor){ if(widget.hideAppBar??false){//是否隐藏这个appbar,默认是false return Container(//隐藏就返回一个container color: backgroundColor, height: 30, ); } //非隐藏appbar的情况下 return Container( color: backgroundColor, //解决刘海等手机挡住自定义appbar的返回键 padding: EdgeInsets.fromLTRB(0, 40, 0, 10), child: FractionallySizedBox(//撑满屏幕的宽度 widthFactor: 1,//宽度的充满 child: Stack( children: [ GestureDetector( onTap: (){ //返回按钮的事件处理 Navigator.pop(context); }, child: Container( margin: EdgeInsets.only(left: 10), child: Icon( Icons.close, color: backButtonColor, size: 26, ), ), ), //设置标题:用绝对定位 Positioned( left: 0, right: 0, child: Center( child: Text(widget.title??'',style: TextStyle(color: backButtonColor,fontSize: 20),),//widget.title??''表示title为空就返回空字符串 ), ) ], ), ), ); } }
10.异步
//利用promise方式 // HomeDao.fetch().then((result){ // setState(() { // resultString = json.encode(result); // }); // }).catchError((e){ // setState(() { // resultString=e.toString(); // }); // }); //方式2:利用async await
11.
直接用ListView的话,对于刘海手机,listview自身会对它有个padding作为预留空间。但是此处为了好看,不想预留它,就用MediaQuery.removePadding包裹listview 因为系统自带的appbar不便于扩展,所以就采用自定义appbar来实现滚动渐变。此处还要设置一个层叠的布局效果,让appbar层叠在轮播图上。 stack布局中,后一个元素会层叠在上一个元素的上面
12.
child: NotificationListener( //监听列表的滚动 onNotification: (scrollNotification) { //这里的scrollNotification还会监听子元素banner轮播图的滚动,所以打印scrollNotification.metrics.pixels是一个较大的值。 //为了过滤掉轮播图的滚动,用scrollNotification.depth ==0表示只监听深度为0,即它的直接子元素的滚动 if (scrollNotification is ScrollUpdateNotification && scrollNotification.depth == 0) { //防止滚动是0他也触发监听事件 //滚动且是列表滚动的时候 _onScroll(scrollNotification.metrics.pixels); } }, child: _listView, ))),
13.自定义appbar实现透明度的渐变:
Widget get _appBar { return Column( children:[ Container( decoration: BoxDecoration( gradient: LinearGradient( //appbar渐变遮罩背景 colors: [Color(0x66000000), Colors.transparent], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: Container( padding: EdgeInsets.fromLTRB(0, 20, 0, 0), height: 80.0, decoration: BoxDecoration( color: Color.fromARGB((appBarAlpha * 255).toInt(), 255, 255, 255)), child: SearchBar( searchBarType: appBarAlpha > 0.2 ? SearchBarType.homLight : SearchBarType.home, inputBoxClick: _jumpToSearch, speakClick: _jumpToSpeak, defaultText: SEARCH_BAR_DEFAULT_TEXT, leftButtonClick: () {}, ), ), ), //设置container的底部阴影 Container( height: appBarAlpha > 0.2 ? 0.5 : 0, decoration: BoxDecoration( boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 0.5)]), ) ], ); }
14.
/** * 这里会出现这种情况:listview和自定义的appbar有一段空白。这是Listview造成的。 * 解决这个问题: * MediaQuery.removePadding( removeTop: true, */ MediaQuery.removePadding( removeTop: true, context: context, /** * 这里为什么要用Expanded呢:? * 因为父布局是Column是列表,listview也是列表,形成了列表套|列表 * 所以listView是垂直列表的话一定要有明确的高度。Row和水平listviEw同理。 * 所以为了让listview有个明确的宽度和高度,用expanded,flex:1让列表自适应里面的高度 */ /** * 如何让ListView占据除其他布局以外的所有空间: * Expanded 填充flex:1 * 这样就可以自适应宽度,填满高度 */ child: Expanded( flex: 1, child: //使用listview展示搜索结果【使用listview的动态生成】 ListView.builder( itemCount: searchModel?.data?.length ?? 0, itemBuilder: (BuildContext context, int position) { return _item(position); }), ), ),
15.富文本效果的实现与搜索框关键字的结合
_keywordTextSpans(String word, String keyword) { Listspans = []; if (word == null || word.length == 0) return spans; //搜索关键字高亮忽略大小写 String wordL = word.toLowerCase(), keywordL = keyword.toLowerCase(); List arr = wordL.split(keywordL); TextStyle normalStyle = TextStyle(fontSize: 16, color: Colors.black87); TextStyle keywordStyle = TextStyle(fontSize: 16, color: Colors.blueAccent); //'wordwoc'.split('w') -> [, ord, oc] @https://www.tutorialspoint.com/tpcg.php?p=wcpcUA int preIndex = 0; for (int i = 0; i < arr.length; i++) { if ((i + 1) % 2 == 0) { //搜索关键字高亮忽略大小写 preIndex = wordL.indexOf(keywordL, preIndex); spans.add(TextSpan( text: word.substring(preIndex, preIndex + 1), style: keywordStyle)); } String val = arr[i]; if (val != null && val.length > 0) { spans.add(TextSpan(text: val, style: normalStyle)); } } return spans; }
16.语音识别按钮的动画效果
const double MIC_SIZE =80; class AnimatedMic extends AnimatedWidget{ //定义2个补间动画 static final _opacityTween = Tween(begin: 1,end: 0.5);//透明度动画 static final _sizeTween =Tween (begin: MIC_SIZE,end: MIC_SIZE-20);//大小动画 AnimatedMic({Key key,Animation animation}) :super(key: key,listenable: animation); @override Widget build(BuildContext context) { final Animation animation =listenable; return Opacity( opacity: _opacityTween.evaluate(animation), child: Container( height: _sizeTween.evaluate(animation), width: _sizeTween.evaluate(animation), decoration: BoxDecoration( color: Colors.purple, borderRadius: BorderRadius.circular(MIC_SIZE/2), ), child: Icon(Icons.mic,color: Colors.white,size: 30,), ), ); } }
17.动画
Animationanimation; AnimationController controller; @override void initState() { controller =AnimationController(vsync: this,duration: Duration(milliseconds: 1000));//vsync还需要加上SingleTickerProviderStateMixin。在页面不显示的时候动画就会停止。 animation =CurvedAnimation(parent: controller, curve: Curves.easeIn) ..addStatusListener((status){//这个..addListener表示给animation添加监听器,其写法等效于上面的语句加分号然后再另起一行加上animation.addListener() if(status == AnimationStatus.completed){//动画执行结束的时候再反向执行达到了循环执行动画的效果 controller.reverse(); }else if(status == AnimationStatus.dismissed){//动画被关闭的状态才启动动画 controller.forward(); } }); super.initState(); }
18.
/** * tab下的具体每个页面tabBarview配合TabBar使用 * 用Flexible布局解决tab布局白屏。帮我们撑开水平方向上没有撑开的地方, * 这样就可以解决渲染的时候丢失我们的宽度和高度的问题 */ Flexible( child: TabBarView( controller: _controller, children: tabs.map((TravelTab tab) { return TravelTabPage(travelUrl: travelTabModel.url,params: travelTabModel.params,groupChannelCode: tab.groupChannelCode,); }).toList()))
19.
/** * : super(key: key);构造方法后跟这个是dart的语法。 * 如果这个构造方法没方法体它可以通过 : super(key: key);这种方式调用父类的构造方法。且把当前的key传给了父类的key, * 所以可以看到super(key:key)中的2个key都是小写。其实第一个key是父类的key属性,第二个key就说 我们当前的构造方法的参数中传给父类的key。 * const TravelTabPage({Key key, this.travelUrl, this.groupChannelCode}) : super(key: key); * 上面的构造方法的写法等价于下面的代码: * TravelTabPage({Key key, this.travelUrl, this.groupChannelCode}) : super(key: key){ } */
20.AutomaticKeepAliveClientMixin实现界面不重绘
class _TravelTabPageState extends Statewith AutomaticKeepAliveClientMixin { //AutomaticKeepAliveClientMixin是实现界面不重绘的解决方案,每翻动一下tab就会重绘。解决这个问题就用它。 /** * AutomaticKeepAliveClientMixin是实现界面不重绘的解决方案,每翻动一下tab就会重绘。解决这个问题就用它。 * 好处:在内存中保存 * 坏处,页面变多,缓存就更多,占用内存更大 */ @override bool get wantKeepAlive => true; //返回true表示当前页面保活
21.
/** * 默认会发现卡片和tabbar底部有很大的间隙,移除就用 * body: MediaQuery.removePadding(context: context, removeTop: true,child: null) */
22.
/** * LimitedBox:|对于这里显示文字,如果文字过长就用跑马灯效果展示 * 它可以控制最大宽度和最大高度 */ LimitedBox( maxWidth: 130, child: Text( _poiName(), maxLines: 1, //单行显示再加上其宽度的约束就可以将多余的文字变为省略号了配合下面的overflow属性 overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.white, fontSize: 12), ), )
23.BottomNavigationBarItem的简单封装
bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { _controller.jumpToPage(index); setState(() { _currentIndex = index; }); }, type: BottomNavigationBarType.fixed, items: [ _bottomItem('首页', Icons.home, 0), _bottomItem('搜索', Icons.search, 1), _bottomItem('旅拍', Icons.camera_alt, 2), _bottomItem('我的', Icons.account_circle, 3), ]),_bottomItem(String title, IconData icon, int index) { return BottomNavigationBarItem( icon: Icon( icon, color: _defalutColor, ), activeIcon: Icon( icon, color: _activeColor, ), title: Text( title, style: TextStyle( color: _currentIndex != index ? _defalutColor : _activeColor), )); }
如果不封装:
//底部导航按钮传统写法:没封装。【封装后对比哈可见代码量】 // bottomNavigationBar: BottomNavigationBar( // currentIndex: _currentIndex, // onTap: (index){ // _controller.jumpToPage(index); // setState(() { // _currentIndex=index; // }); // }, // type: BottomNavigationBarType.fixed, // items:[ // BottomNavigationBarItem( // icon: Icon(Icons.home, color: _defalutColor), // activeIcon: Icon(Icons.home, color: _activeColor), // title: Text('首页',style: TextStyle( // color: _currentIndex != 0 ? _defalutColor : _activeColor // ),) // ), // // BottomNavigationBarItem( // icon: Icon(Icons.search, color: _defalutColor), // activeIcon: Icon(Icons.search, color: _activeColor), // title: Text('搜索',style: TextStyle( // color: _currentIndex != 1 ? _defalutColor : _activeColor // ),) // ), // // BottomNavigationBarItem( // icon: Icon(Icons.camera_alt, color: _defalutColor), // activeIcon: Icon(Icons.camera_alt, color: _activeColor), // title: Text('旅拍',style: TextStyle( // color: _currentIndex != 2 ? _defalutColor : _activeColor // ),) // ), // // BottomNavigationBarItem( // icon: Icon(Icons.account_circle, color: _defalutColor), // activeIcon: Icon(Icons.account_circle, color: _activeColor), // title: Text('我的',style: TextStyle( // color: _currentIndex != 3 ? _defalutColor : _activeColor // ),) // ), // ] // ),
24.【PageView类似于Android中的ViewPager。PageView控制方向还可以实现抖音整体页面上滑下滑的效果】PageView中的physics: NeverScrollableScrollPhysics(),//禁止pageview的滑动,避免和tab的滑动冲突
body: PageView( controller: _controller, children:[ HomePage(), SearchPage(hideLeft: true,), TravelPage(), MyPage(), ], physics: NeverScrollableScrollPhysics(),//禁止pageview的滑动,避免和tab的滑动冲突 ),
25.Utf8Decoder在Convert包中。目的是修复中文乱码
Utf8Decoder utf8decoder = Utf8Decoder();//在Convert包中。目的是修复中文乱码 var result = json.decode(utf8decoder.convert(response.bodyBytes));