开篇: 一如前端深似海,从此节操是路人,从此再无安宁日,从此红尘是路人。要说技术更迭速度,还有比前端更快的么 😂 根本停不下来。这不,Google 刚发布 Flutter 不到一年时间,1.0 正式版发布不到两个月。阿里系的闲鱼老大哥,已经率先用 Flutter 重构了闲鱼,虽然没完全重构,但高频的重度页面都是 Flutter 的了。这一幕似曾相识,当初 RN 出来的时候不也是闲鱼团队先吃的螃蟹吗,在这里向闲鱼团队的老哥们致敬 🐣。
既然老大哥都出动了,也侧面验证了这项技术的可行性。当小弟的也不能落后嘛,每天抽时间断断续续的学了两周时间,仿部分知乎的客户端,撸了一套客户端出来。前一周主要是熟悉 Dart 语言和常规的客户端布局方式,后一周主要是掌握使用 HTTP 的请求、下拉上拉、左滑右滑、长按等常用手势、相机调用、video 播放等进阶用法。 两周下来,基本上可以开发 80%以上常见的客户端需求。
前期一直在用 simulator 开发,略有卡顿,心中难免有些疑惑。结果最后 release 打包到手机后,竟然如丝般顺滑!!!简直喜出望外,完全可以睥睨原生开发,在这一点上的确要优于目前的 RN。最重要的是作为 Materail Design 极简又有质感风格的鸭狗血粉丝,Flutter 造出来的界面简直倍爽。至此正式入坑 Flutter 开发。Google 万岁!
Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。 Flutter 可以与现有的代码一起工作。在全世界,Flutter 正在被越来越多的开发者和组织使用,并且 Flutter 是完全免费、开源的。
Beta1 版本于 2018 年 2 月 27 日在 2018 世界移动大会公布。 Beta2 版本 2018 年 3 月 6 日发布。 1.0 版本于 2018 年 12 月 5 日(北京时间)发布
这里把学习过程中一些常用高频的东西总结出来,基本能满足大多数情况下的开发需求。
完整的代码: https://github.com/flute/zhihu_flutter
欢迎加入 Flutter 开拓交流,群聊号码:236379502
Scaffold 主要的属性说明
底部菜单 bottomNavigationBar,Tab 栏切换 TabBar 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 TabController controller; @override void initState() { super.initState(); // initialize the tab controller // vsync ?? controller = new TabController(length: 5, vsync: this); } @override void dispose() { // dispose of tab controller controller.dispose(); super.dispose(); } ... body: new TabBarView( children: <Widget>[new HomeTab(), new IdeaTab(), new ColleagueTab(), new MessageTab(), new MeTab()], controller: controller, ), bottomNavigationBar: new Material( // background color of bottom navigation bar color: Colors.white, textStyle: new TextStyle( color: Colors.black45 ), child: new TabBar( unselectedLabelColor: Colors.black45, labelColor: Colors.blue, controller: controller, tabs: <Tab>[ new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.home, size: 25,), Text('首页', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.access_alarm, size: 25,), Text('想法', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.access_time, size: 25,), Text('大学', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.account_balance_wallet, size: 25,), Text('消息', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.adb, size: 25,), Text('我的', style: TextStyle(fontSize: 10),) ], ), ), ), ], ), ),
顶栏自定义 appbar:title 属性 顶部搜索栏:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: searchBar(), backgroundColor: Colors.white, bottom: new Text('bottom'), ), body: new Container() ); } /** * 顶部搜索栏 */ Widget searchBar() { return new Container( child: new Row( children: <Widget>[ new Expanded( child: new FlatButton.icon( color:Color.fromRGBO(229, 229, 229, 1.0), onPressed: (){ Navigator.of(context).push(new MaterialPageRoute(builder: (context){ return new SearchPage(); })); }, icon: new Icon( Icons.search, color: Colors.black38, size: 16.0, ), label: new Text( "诺奖得主为上课推迟发布会", style: new TextStyle(color: Colors.black38) ), ), ), new Container( child: new FlatButton.icon( onPressed: (){ Navigator.of(context).push(new MaterialPageRoute(builder: (context){ return new AskPage(); })); }, icon: new Icon( Icons.border_color, color: Colors.blue, size: 14.0 ), label: new Text( '提问', style: new TextStyle(color: Colors.blue), ), ), ) ], ), ); }
图片圆角 1 2 3 4 5 6 7 8 9 10 11 Container( margin: EdgeInsets.only(right: 5), decoration: new BoxDecoration( shape: BoxShape.circle, image: new DecorationImage( image: new NetworkImage(avatarUrl), ) ), width: 30, height: 30, ),
数组里动态添加组件 https://github.com/flutter/flutter/issues/3783
1 2 3 4 5 6 7 8 9 10 bool notNull(Object o) => o != null; Widget build() { return new Column( children: <Widget>[ new Title(), new Body(), shouldShowFooter ? new Footer() : null ].where(notNull).toList(), ); }
Text 显示指定行数,超出后显示省略号 1 2 3 4 5 6 Text( content, maxLines: 3, overflow: TextOverflow.ellipsis, style: new TextStyle(fontSize: 14, color: Colors.black54), ),
margin 负值 https://stackoverflow.com/questions/42257668/the-equivalent-of-wrap-content-and-match-parent-in-flutter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 return Container( width: 40, height:40, // flutter中的margin没有负值的说法 // https://stackoverflow.com/questions/42257668/the-equivalent-of-wrap-content-and-match-parent-in-flutter transform: Matrix4.translationValues(-20.0, 0.0, 0.0), decoration: new BoxDecoration( border: Border.all(width: 3, color: Colors.white), color: Colors.black, shape: BoxShape.circle, image: new DecorationImage( image: new NetworkImage('https://pic3.zhimg.com/50/d2af1b6b1_s.jpg') ) ), );
图片自适应填满 container https://stackoverflow.com/questions/45745448/how-do-i-stretch-an-image-to-fit-the-whole-background-100-height-x-100-width
1 2 3 4 5 6 7 8 9 new Container( height: 200, decoration: new BoxDecoration( image: new DecorationImage( image: NetworkImage('https://pic3.zhimg.com/50/v2-f9fd4b13a46f2800a7049a5724e5969f_400x224.jpg'), fit: BoxFit.fill ) ), ),
布局方式 justify-content: mainAxisAlignment align-items: crossAxisAlignment
column 设置 crossAxisAlignment: stretch 后子元素宽度为 100%,如果想让子元素宽度不为 100%, 将其包裹在 Row 元素中即可。
flutter row and column https://medium.com/jlouage/flutter-row-column-cheat-sheet-78c38d242041
捕捉点击事件 使用GestureDetector
包裹widget
即可。
1 2 3 4 5 6 7 child: new GestureDetector( onTap: click, child: Text( name, style: TextStyle(color: Colors.black87), ), ),
Dart 数组方法 https://codeburst.io/top-10-array-utility-methods-you-should-know-dart-feb2648ee3a2
https://stackoverflow.com/questions/43349013/how-to-open-a-popupmenubutton
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class DetailPage extends StatefulWidget { @override DetailPageState createState() => DetailPageState(); } class DetailPageState extends State<DetailPage> { final GlobalKey _menuKey = new GlobalKey(); .... .... .... child: new Row( children: <Widget>[ new Container( child: new GestureDetector( onTap: () { dynamic state = _menuKey.currentState; state.showButtonMenu(); }, child: new Container( child: new Text('默认排序'), ), ), ), new PopupMenuButton( icon: Icon(Icons.keyboard_arrow_down), offset: Offset(0, 50), key: _menuKey, itemBuilder: (_) => <PopupMenuItem<String>>[ new PopupMenuItem<String>( child: const Text('默认排序'), value: 'default'), new PopupMenuItem<String>( child: const Text('按时间排序'), value: 'timeline'), ], onSelected: (_) {} ) ], ),
分割线 水平分割线 Divider 垂直分割线 VerticalDivider (无效???)
swiper https://pub.dartlang.org/packages/flutter_swiper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import 'package:flutter_swiper/flutter_swiper.dart'; ... var images = [ 'https://pic3.zhimg.com/v2-5806d9e33e36fa772c8da56c931bb416_b.jpg', 'https://pic1.zhimg.com/50/v2-f355ca177e011626938b479f0e2e3e03_hd.jpg', 'https://pic2.zhimg.com/v2-d8e47ed961b93b875ad814104016bdfd_b.jpg' ]; child: new Swiper( itemBuilder: (BuildContext context,int index){ return new Image.network(images[index], fit: BoxFit.cover,); }, itemCount: 3, pagination: new SwiperPagination(), //control: new SwiperControl(), ),
https://proandroiddev.com/a-deep-dive-into-floatingactionbutton-in-flutter-bf95bee11627
floatingActionButton 配合 Scaffold 使用最佳
1 2 3 4 5 6 7 8 9 Scaffold( floatingActionButton: new FloatingActionButton( onPressed: (){}, child: Icon(Icons.edit), //mini: true, ), // 默认右下角,可设置位置。 floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, )
滑动视图 SingleChildScrollView
水平方向滑动 scrollDirection: Axis.horizontal
高斯模糊 https://stackoverflow.com/questions/43550853/how-do-i-do-the-frosted-glass-effect-in-flutter
1 2 3 4 5 6 import 'dart:ui'; new BackdropFilter( filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), child: Text(desc, style: TextStyle(color: Colors.white),), ),
对话框弹窗 https://docs.flutter.io/flutter/material/AlertDialog-class.html
AlertDialog
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void _showDialog(BuildContext context) { // flutter defined function showDialog( context: context, builder: (BuildContext context) { // return object of type Dialog return AlertDialog( title: Text('Rewind and remember'), content: SingleChildScrollView( child: ListBody( children: <Widget>[ Text('You will never be satisfied.'), Text('You\’re like me. I’m never satisfied.'), ], ), ), actions: <Widget>[ // usually buttons at the bottom of the dialog new FlatButton( child: new Text("Close"), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } // 调用 .... onPressed: (){ _showDialog(context); }, ....
HTTP 请求、JSON 编码解码 https://flutterchina.club/networking/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import 'dart:convert' ;import 'dart:io' ;try { var request = await httpClient.getUrl(Uri .parse(url)); var response = await request.close(); if (response.statusCode == HttpStatus.OK) { var json = await response.transform(UTF8.decoder).join(); var data = JSON.decode(json); result = data['origin' ]; } else { result = 'Error getting IP address:\nHttp status ${response.statusCode} ' ; } } catch (exception) { result = 'Failed getting IP address' ; } if (!mounted) return ;setState(() { _ipAddress = result; });
时间控制:延时 1 2 3 4 5 6 7 8 9 10 11 import 'dart:async'; Future<Null> _onRefresh() { Completer<Null> completer = new Completer<Null>(); new Timer(new Duration(seconds: 3), () { print("timer complete"); completer.complete(); }); return completer.future; }
下拉刷新 RefreshIndicator 1 2 3 4 5 6 7 8 9 new RefreshIndicator( onRefresh: _onRefresh, child: new SingleChildScrollView( child: new Container( padding: EdgeInsets.all(10), child: new Text(_jsonData), ), ), )
上拉加载更多 https://juejin.im/post/5b3abfc4518825622c14a6f1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ScrollController _controller = new ScrollController(); @override void initState() { super.initState(); _controller.addListener((){ if(_controller.position.pixels == _controller.position.maxScrollExtent) { print('下拉加载'); _getMoreData(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } ... scroll controller: _controller ...
flutter 运行模式 https://www.jianshu.com/p/4db65478aaa3
flutter 学习资源 http://flutter.link/
1 2 3 4 ➜ zh git:(master) ✗ flutter clean Deleting 'build/'. ➜ zh git:(master) ✗ rm -rf ios/Flutter/App.framework ios/Flutter/Flutter.framework ➜ zh git:(master) ✗ rm -rf /Users/ludis/Library/Developer/Xcode/DerivedData/Runner-
报错解决 1、在安卓真机 release 后 ios simulator 无法编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Launching lib/main.dart on iPhone X in debug mode... Xcode build done. 1.0s Failed to build iOS app Error output from Xcode build: ↳ ** BUILD FAILED ** Xcode's output: ↳ === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Debug === diff: /Users/ludis/Desktop/opt/flutter/zh/ios/Pods/Manifest.lock: No such file or directory error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. Could not build the application for the simulator. Error launching application on iPhone X. Exited (sigterm)
解决
常见问题: https://www.jianshu.com/p/bf3002de6a5e
https://medium.com/flutter-community/scrolling-animation-in-flutter-6a6718b8e34f
布局指南 在 scrollView 的滚动布局中,如果使用 column 组件,并为其添加 Expanded 扩展子组件的话,这两者会存在冲突。 如果坚持要使用此布局,在 column 设置mainAxisSize: MainAxisSize.min
,同时子组件由Expanded
改为Flexible
即可。
表单、校验 https://www.cnblogs.com/pengshaomin/p/8945720.html
1、单行文本输入框 TextFormField
1 2 3 4 5 6 7 8 new TextFormField( maxLength: 32, onSaved: (val)=> this._config = val, validator: (v)=>(v == null || v.isEmpty)?"请选择配置": null, decoration: new InputDecoration( labelText: '配置', ), ),
2、多行输入框 keyboardType: TextInputType.multiline,
1 2 3 4 5 new TextField( keyboardType: TextInputType.multiline, maxLines: 3, maxLength: 100, ),
2、单选 Radio
1 2 3 4 5 6 7 8 9 10 11 new Radio( groupValue: this.radio, activeColor: Colors.blue, value: 'aaa', onChanged: (String val) { // val 与 value 的类型对应 this.setState(() { this.radio = val; // aaa }); }, ),
3、复选 CheckBox
1 2 3 4 5 6 7 8 9 new Checkbox( value: flutter, activeColor: Colors.blue, onChanged: (val) { setState(() { flutter = val; }); }, ),
4、switch
1 2 3 4 5 6 7 8 9 new Switch( activeColor: Colors.green, value: flutter, onChanged: (val) { setState(() { flutter = val; }); }, ),
5、slider
1 2 3 4 5 6 7 8 9 10 new Slider( value: _slider, min: 0.0, max: 100.0, onChanged: (val) { setState(() { _slider = val; }); }, ),
6、DateTimePicker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 DateTime _dateTime = new DateTime .now(); new GestureDetector( onTap: (){ _showDatePicker(); }, child: new Container( child: new Text(_dateTime.toLocal().toString()), ), ), void _showDatePicker() { _selectDate(context); } Future<Null > _selectDate(BuildContext context) async { final DateTime _picked = await showDatePicker( context: context, initialDate: _dateTime, firstDate: new DateTime (2016 ), lastDate: new DateTime (2050 ) ); if (_picked != null ) { print (_picked); setState(() { _dateTime = _picked; }); } }
7、TimePIcker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 TimeOfDay _time = new TimeOfDay.now(); new GestureDetector( onTap: _showTimePicker, child: new Text(_time.format(context)), ), void _showTimePicker(){ _selectTime(context); } Future<Null > _selectTime(BuildContext context) async { final TimeOfDay _picker = await showTimePicker( context: context, initialTime: _time, ); if (_picker != null ) { print (_picker); setState(() { _time = _picker; }); } }
Toast/showSnackBar showSnackBar:
https://material.io/design/components/snackbars.html#usage
1 2 3 4 5 6 7 8 9 10 11 12 void _showToast(BuildContext context) { final scaffold = Scaffold.of(context); scaffold.showSnackBar( SnackBar( content: const Text('Added to favorite'), action: SnackBarAction( label: 'UNDO', onPressed: scaffold.hideCurrentSnackBar ), ), ); }
Toast:
https://github.com/PonnamKarthik/FlutterToast
1 2 3 4 5 6 7 8 9 10 11 void _showToast(String title) { Fluttertoast.showToast( msg: title, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIos: 1 , backgroundColor: Color.fromRGBO(0 , 0 , 0 , 0.85 ), textColor: Colors.white ); }
popup: CupertinoActionSheet
http://flatteredwithflutter.com/actionsheet-in-flutter/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 new MaterialButton( onPressed: () { _showActionSheet(); }, child: new Text('show ActionSheet' , style: TextStyle(color: Colors.white),), color: Colors.greenAccent, ), void _showActionSheet() { showCupertinoModalPopup( context: context, builder: (BuildContext context) => actionSheet(), ).then((value) { Scaffold.of(context).showSnackBar(new SnackBar( content: new Text('You clicked $value ' ), )); }); } Widget actionSheet(){ return new CupertinoActionSheet( title: new Text('title' ), message: const Text('your options are' ), actions: <Widget>[ CupertinoActionSheetAction( child: const Text('yes' ), onPressed: (){ Navigator.pop(context, 'yes' ); }, ), CupertinoActionSheetAction( child: const Text('no' ), onPressed: (){ Navigator.pop(context, 'no' ); }, ) ], cancelButton: CupertinoActionSheetAction( child: new Text('cancel' ), onPressed: () { Navigator.pop(context, 'Cancel' ); }, ), ); }
IOS 风格组件 https://flutter-es.io/widgets/cupertino/
Dismissible 滑动删除 https://flutter.io/docs/cookbook/gestures/dismissible
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 new Dismissible( // Each Dismissible must contain a Key. Keys allow Flutter to // uniquely identify Widgets. key: Key(item), onDismissed: (direction) { setState(() { items.removeAt(index); }); // Then show a snackbar! Scaffold.of(context) .showSnackBar(SnackBar(content: Text("$item dismissed"))); }, // Show a red background as the item is swiped away background: Container(color: Colors.red), child: ListTile(title: Text('$item')), );
Swipe 左滑右滑删除 https://github.com/letsar/flutter_slidable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 Widget _swipe(int i, String title, String desc) { return new Slidable( delegate: new SlidableDrawerDelegate(), actionExtentRatio: 0.25 , child: new Container( color: Colors.white, child: new GestureDetector( onTap: (){}, onDoubleTap: (){}, onLongPress: (){}, child: new ListTile( leading: new CircleAvatar( backgroundColor: Colors.grey[200 ], child: new Text( '$i ' , style: TextStyle(color: Colors.orange), ), foregroundColor: Colors.white, ), title: new Text( '$title ' , maxLines: 1 , overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.black87, fontSize: 16 ), ), subtitle: new Text( '$desc ' , style: TextStyle(color: Colors.blue[300 ]), ), ), ) ), actions: <Widget>[ new IconSlideAction( caption: 'Archive' , color: Colors.blue, icon: Icons.archive, onTap: () => _showSnackBar('Archive' ), ), new IconSlideAction( caption: 'Share' , color: Colors.indigo, icon: Icons.share, onTap: () => _showSnackBar('Share' ), ), ], secondaryActions: <Widget>[ new IconSlideAction( caption: 'More' , color: Colors.black45, icon: Icons.more_horiz, onTap: () => _showSnackBar('More' ), ), new IconSlideAction( caption: 'Delete' , color: Colors.red, icon: Icons.delete, onTap: () => _showSnackBar('Delete' ), ), ], ); }
常用手势 GestureDetector 1 2 3 4 5 new GestureDetector( onTap: (){_showToast('点击: $i');}, onDoubleTap: (){_showToast('连点: $i');}, onLongPress: (){_showToast('长按: $i');}, )
flutter 常用组件 https://github.com/flutter/plugins
camera / image_picker https://medium.com/flutter-community/implementing-camera-feature-in-flutter-f7f6a7a5e6dd
image_picker: (最常用场景,从相册选择或拍照得到照片)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 dynamic _picture;dynamic _gallery;new FlatButton.icon( icon: Icon(Icons.camera), label: Text('选择头像' ), onPressed: (){ _optionsDialogBox(); }, ), Future<void > _optionsDialogBox() { return showDialog(context: context, builder: (BuildContext context) { return AlertDialog( content: new SingleChildScrollView( child: new ListBody( children: <Widget>[ GestureDetector( child: new Text('Take a picture' ), onTap: openCamera, ), Padding( padding: EdgeInsets.all(8.0 ), ), GestureDetector( child: new Text('Select from gallery' ), onTap: openGallery, ), ], ), ), ); }); } void openCamera() async { Navigator.of(context).pop(); var picture = await ImagePicker.pickImage( source: ImageSource.camera, ); setState(() { _picture = picture; }); } void openGallery() async { Navigator.of(context).pop(); var gallery = await ImagePicker.pickImage( source: ImageSource.gallery, ); setState(() { _gallery = gallery; }); }
camera: (高阶用法,打开相机,实时获取相机流,可以定制拍照、录像等按钮。可用于相机扫码、实时识别、直播等场景)
https://pub.dartlang.org/packages/camera
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 camera: ^0.2.9 import 'package:camera/camera.dart'; class _CameraState extends State<CameraWidget> { List<CameraDescription> cameras; CameraController controller; bool _isReady = false; @override void initState() { super.initState(); _setupCameras(); } Future<void> _setupCameras() async { try { // initialize cameras. cameras = await availableCameras(); // initialize camera controllers. controller = new CameraController(cameras[0], ResolutionPreset.medium); await controller.initialize(); } on CameraException catch (_) { // do something on error. } if (!isMounted) return; setState(() { _isReady = true; }); } Widget build(BuildContext context) { if (!_isReady) return new Container(); return new Container( height: 200, child: AspectRatio( aspectRatio: controller.value.aspectRatio, child: CameraPreview(controller), ), ) } }
video player https://github.com/flutter/plugins/tree/master/packages/video_player
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 video_player: ^0.8.0 import 'package:video_player/video_player.dart'; VideoPlayerController _controller; bool _isPlaying = false; @override void initState() { super.initState(); _controller = VideoPlayerController.network( 'https://www.quirksmode.org/html5/videos/big_buck_bunny.mp4', ) ..addListener(() { final bool isPlaying = _controller.value.isPlaying; if (isPlaying != _isPlaying) { setState(() { _isPlaying = isPlaying; }); } }) ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. setState(() {}); }); } @override void dispose() { _controller?.dispose(); super.dispose(); } // 显示、控制 _controller.value.initialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: new Container( padding: EdgeInsets.all(10), color: Colors.black, child: VideoPlayer(_controller), ), ) : Container( child: new Text('视频加载中~'), ), new FlatButton.icon( label: Text('播放/暂停'), icon: Icon( _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, ), onPressed: _controller.value.isPlaying ? _controller.pause : _controller.play, )
AudioPlayer https://github.com/rxlabz/audioplayer
Flutter 的学习还在路上,列了个 To Do List。继续加油: