原文:Flutter學(xué)習(xí)之旅——實(shí)用入坑指南 以上原文中沒有目錄索引,,不太好找,所以把原文整理了下,。
開篇
一如前端深似海,從此節(jié)操是路人,,從此再無安寧日,,從此紅塵是路人。要說技術(shù)更迭速度,,還有比前端更快的么?根本停不下來,。這不,Google剛發(fā)布Flutter不到一年時(shí)間,,1.0正式版發(fā)布不到兩個(gè)月,。阿里系的閑魚老大哥,已經(jīng)率先用Flutter重構(gòu)了閑魚,,雖然沒完全重構(gòu),,但高頻的重度頁面都是Flutter的了。這一幕似曾相識,,當(dāng)初RN出來的時(shí)候不也是閑魚團(tuán)隊(duì)先吃的螃蟹嗎,,在這里向閑魚團(tuán)隊(duì)的老哥們致敬?。
既然老大哥都出動(dòng)了,,也側(cè)面驗(yàn)證了這項(xiàng)技術(shù)的可行性,。當(dāng)小弟的也不能落后嘛,每天抽時(shí)間斷斷續(xù)續(xù)的學(xué)了兩周時(shí)間,,仿部分知乎的客戶端,,擼了一套客戶端出來。前一周主要是熟悉Dart語言和常規(guī)的客戶端布局方式,,后一周主要是掌握使用HTTP的請求,、下拉上拉,、左滑右滑,、長按等常用手勢、相機(jī)調(diào)用、video播放等進(jìn)階用法,。 兩周下來,,基本上可以開發(fā)80%以上常見的客戶端需求。
前期一直在用simulator開發(fā),,略有卡頓,,心中難免有些疑惑。結(jié)果最后release打包到手機(jī)后,,竟然如絲般順滑?。?!簡直喜出望外,,完全可以睥睨原生開發(fā),在這一點(diǎn)上的確要優(yōu)于目前的RN,。最重要的是作為Materail Design極簡又有質(zhì)感風(fēng)格的鴨狗血粉絲,,F(xiàn)lutter造出來的界面簡直倍爽。至此正式入坑Flutter開發(fā),。Google萬歲,!
Flutter是谷歌的移動(dòng)UI框架,可以快速在iOS和Android上構(gòu)建高質(zhì)量的原生用戶界面,。 Flutter可以與現(xiàn)有的代碼一起工作,。在全世界,F(xiàn)lutter正在被越來越多的開發(fā)者和組織使用,,并且Flutter是完全免費(fèi),、開源的。Beta1版本于2018年2月27日在2018 世界移動(dòng)大會公布,。 Beta2版本2018年3月6日發(fā)布,。 1.0版本于2018年12月5日(北京時(shí)間)發(fā)布
這里把學(xué)習(xí)過程中一些常用高頻的東西總結(jié)出來,基本能滿足大多數(shù)情況下的開發(fā)需求,。
完整的代碼: https://github.com/flute/zhih...
歡迎加入Flutter開拓交流,,群聊號碼:236379502
Scaffold 主要的屬性說明
- appBar:顯示在界面頂部的一個(gè) AppBar
- body:當(dāng)前界面所顯示的主要內(nèi)容
- floatingActionButton: 在 Material 中定義的一個(gè)功能按鈕。
- persistentFooterButtons:固定在下方顯示的按鈕,。https://material.google.com/c...
- drawer:側(cè)邊欄控件
- bottomNavigationBar:顯示在底部的導(dǎo)航欄按鈕欄,。可以查看文檔:Flutter學(xué)習(xí)之制作底部菜單導(dǎo)航
- backgroundColor:背景顏色
- resizeToAvoidBottomPadding: 控制界面內(nèi)容 body 是否重新布局來避免底部被覆蓋了,,比如當(dāng)鍵盤顯示的時(shí)候,,重新布局避免被鍵盤蓋住內(nèi)容。默認(rèn)值為 true,。
底部菜單 bottomNavigationBar,,Tab欄切換 TabBar
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('大學(xué)', 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屬性 頂部搜索欄
@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(
"諾獎(jiǎng)得主為上課推遲發(fā)布會",
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),
),
),
)
],
),
);
}
圖片圓角
Container(
margin: EdgeInsets.only(right: 5),
decoration: new BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
image: new NetworkImage(avatarUrl),
)
),
width: 30,
height: 30,
),
數(shù)組里動(dòng)態(tài)添加組件
https://github.com/flutter/fl...
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顯示指定行數(shù),,超出后顯示省略號
Text(
content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontSize: 14, color: Colors.black54),
),
margin 負(fù)值
https:///que...
return Container(
width: 40,
height:40,
// flutter中的margin沒有負(fù)值的說法
// https:///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./50/d2af1b6b1_s.jpg')
)
),
);
圖片自適應(yīng)填滿container
https:///que...
new Container(
height: 200,
decoration: new BoxDecoration(
image: new DecorationImage(
image: NetworkImage('https://pic3./50/v2-f9fd4b13a46f2800a7049a5724e5969f_400x224.jpg'),
fit: BoxFit.fill
)
),
),
布局方式
justify-content: mainAxisAlignment align-items: crossAxisAlignment
column 設(shè)置crossAxisAlignment: stretch后子元素寬度為100%,如果想讓子元素寬度不為100%,, 將其包裹在Row元素中即可,。
flutter row and column https:///jlouage/fl...
捕捉點(diǎn)擊事件
使用GestureDetector包裹widget即可。
child: new GestureDetector(
onTap: click,
child: Text(
name,
style: TextStyle(color: Colors.black87),
),
),
Dart 數(shù)組方法
https:///top-10-a...
PopupMenuButton 下拉彈窗菜單
https:///que...
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('默認(rèn)排序'),
),
),
),
new PopupMenuButton(
icon: Icon(Icons.keyboard_arrow_down),
offset: Offset(0, 50),
key: _menuKey,
itemBuilder: (_) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
child: const Text('默認(rèn)排序'), value: 'default'),
new PopupMenuItem<String>(
child: const Text('按時(shí)間排序'), value: 'timeline'),
],
onSelected: (_) {}
)
],
),
分割線
水平分割線 Divider 垂直分割線 VerticalDivider (無效,?,??)
swiper
https://pub./pack...
import 'package:flutter_swiper/flutter_swiper.dart';
...
var images = [
'https://pic3./v2-5806d9e33e36fa772c8da56c931bb416_b.jpg',
'https://pic1./50/v2-f355ca177e011626938b479f0e2e3e03_hd.jpg',
'https://pic2./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(),
),
floatingActionButton 浮動(dòng)button
https:///a-d...
floatingActionButton 配合 Scaffold 使用最佳
Scaffold(
floatingActionButton: new FloatingActionButton(
onPressed: (){},
child: Icon(Icons.edit),
//mini: true,
),
// 默認(rèn)右下角,,可設(shè)置位置,。
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
)
滑動(dòng)視圖
SingleChildScrollView
水平方向滑動(dòng) scrollDirection: Axis.horizontal
高斯模糊
https:///que...
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./flutt...
AlertDialog
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();
},
),
],
);
},
);
}
// 調(diào)用
....
onPressed: (){
_showDialog(context);
},
....
HTTP 請求、JSON編碼解碼
https:///net...
// 加載庫
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';
}
// 保存返回的數(shù)據(jù)
// error: setState() called after dispose()
// If the widget was removed from the tree while the message was in flight,
// we want to discard the reply rather than calling setState to update our
// non-existent appearance.
if (!mounted) return;
setState(() {
_ipAddress = result;
});
時(shí)間控制:延時(shí)
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
new RefreshIndicator(
onRefresh: _onRefresh,
child: new SingleChildScrollView(
child: new Container(
padding: EdgeInsets.all(10),
child: new Text(_jsonData),
),
),
)
上拉加載更多
https:///post/5b3abf...
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運(yùn)行模式
https://www.jianshu.com/p/4db...
flutter學(xué)習(xí)資源
http:///
? 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-
報(bào)錯(cuò)解決
1,、在安卓真機(jī)release后 ios simulator無法編譯
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)
解決
cd ios
pod install
常見問題: https://www.jianshu.com/p/bf3...
Flutter scroll animation
https:///flutter-co...
布局指南
在scrollView的滾動(dòng)布局中,,如果使用column組件,并為其添加Expanded擴(kuò)展子組件的話,,這兩者會存在沖突,。 如果堅(jiān)持要使用此布局,在column設(shè)置mainAxisSize: MainAxisSize.min,,同時(shí)子組件由Expanded改為Flexible即可,。
表單、校驗(yàn)
https://www.cnblogs.com/pengs...
1,、單行文本輸入框 TextFormField
new TextFormField(
maxLength: 32,
onSaved: (val)=> this._config = val,
validator: (v)=>(v == null || v.isEmpty)?"請選擇配置": null,
decoration: new InputDecoration(
labelText: '配置',
),
),
3,、多行輸入框 keyboardType: TextInputType.multiline,
new TextField(
keyboardType: TextInputType.multiline,
maxLines: 3,
maxLength: 100,
),
4、單選Radio
new Radio(
groupValue: this.radio,
activeColor: Colors.blue,
value: 'aaa',
onChanged: (String val) {
// val 與 value 的類型對應(yīng)
this.setState(() {
this.radio = val; // aaa
});
},
),
5,、復(fù)選 CheckBox
new Checkbox(
value: flutter,
activeColor: Colors.blue,
onChanged: (val) {
setState(() {
flutter = val;
});
},
),
6,、switch
new Switch(
activeColor: Colors.green,
value: flutter,
onChanged: (val) {
setState(() {
flutter = val;
});
},
),
7、slider
new Slider(
value: _slider,
min: 0.0,
max: 100.0,
onChanged: (val) {
setState(() {
_slider = val;
});
},
),
8,、DateTimePicker
// 設(shè)置存儲日期的變量
DateTime _dateTime = new DateTime.now();
// 顯示文字Text,,設(shè)置點(diǎn)擊事件,點(diǎn)擊后打開日期選擇器
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;
});
}
}
9,、TimePIcker
TimeOfDay _time = new TimeOfDay.now();
// text顯示當(dāng)前時(shí)間
new GestureDetector(
onTap: _showTimePicker,
child: new Text(_time.format(context)),
),
// 顯示timpicker
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:///design/co...
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/PonnamKart...
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
);
}
Popover/popup
popup: CupertinoActionSheet組件 -- Actionsheet in flutter
http://flatteredwithflutter.c...
import 'package:flutter/cupertino.dart';
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風(fēng)格組件
https:///widgets...
Dismissible 滑動(dòng)刪除
https:///docs/cookb...
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/flu...
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
new GestureDetector(
onTap: (){_showToast('點(diǎn)擊: $i');},
onDoubleTap: (){_showToast('連點(diǎn): $i');},
onLongPress: (){_showToast('長按: $i');},
)
flutter 常用組件
https://github.com/flutter/pl...
camera / image_picker
https://github.com/flutter/plugins/tree/master/packages/image_picker
image_picker: (最常用場景,,從相冊選擇或手機(jī)拍照得到照片)
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: (高階用法,打開相機(jī),,實(shí)時(shí)獲取相機(jī)流,,可以定制拍照、錄像等按鈕,??捎糜谙鄼C(jī)掃碼、實(shí)時(shí)識別,、直播等場景)
https://pub./pack...
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/pl...
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./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/aud...
|