Flutter學習之旅——實用入坑指南

原文:Flutter學習之旅——實用入坑指南
以上原文中沒有目錄索引,不太好找,因此把原文整理了下。html

開篇

一如前端深似海,今後節操是路人,今後再無安寧日,今後紅塵是路人。要說技術更迭速度,還有比前端更快的麼😂根本停不下來。這不,Google剛發佈Flutter不到一年時間,1.0正式版發佈不到兩個月。阿里系的閒魚老大哥,已經率先用Flutter重構了閒魚,雖然沒徹底重構,但高頻的重度頁面都是Flutter的了。這一幕似曾相識,當初RN出來的時候不也是閒魚團隊先吃的螃蟹嗎,在這裏向閒魚團隊的老哥們致敬🐣。前端

既然老大哥都出動了,也側面驗證了這項技術的可行性。當小弟的也不能落後嘛,天天抽時間斷斷續續的學了兩週時間,仿部分知乎的客戶端,擼了一套客戶端出來。前一週主要是熟悉Dart語言和常規的客戶端佈局方式,後一週主要是掌握使用HTTP的請求、下拉上拉、左滑右滑、長按等經常使用手勢、相機調用、video播放等進階用法。 兩週下來,基本上能夠開發80%以上常見的客戶端需求。html5

前期一直在用simulator開發,略有卡頓,心中不免有些疑惑。結果最後release打包到手機後,居然如絲般順滑!!!簡直喜出望外,徹底能夠睥睨原生開發,在這一點上的確要優於目前的RN。最重要的是做爲Materail Design極簡又有質感風格的鴨狗血粉絲,Flutter造出來的界面簡直倍爽。至此正式入坑Flutter開發。Google萬歲!android

Flutter是谷歌的移動UI框架,能夠快速在iOS和Android上構建高質量的原生用戶界面。 Flutter能夠與現有的代碼一塊兒工做。在全世界,Flutter正在被愈來愈多的開發者和組織使用,而且Flutter是徹底免費、開源的。

Beta1版本於2018年2月27日在2018 世界移動大會公佈。
Beta2版本2018年3月6日發佈。
1.0版本於2018年12月5日(北京時間)發佈ios

這裏把學習過程當中一些經常使用高頻的東西總結出來,基本能知足大多數狀況下的開發需求。git

完整的代碼: https://github.com/flute/zhih...github

歡迎加入Flutter開拓交流,羣聊號碼:236379502json

Scaffold 主要的屬性說明

  • appBar:顯示在界面頂部的一個 AppBar
  • body:當前界面所顯示的主要內容
  • floatingActionButton: 在 Material 中定義的一個功能按鈕。
  • persistentFooterButtons:固定在下方顯示的按鈕。https://material.google.com/c...
  • drawer:側邊欄控件
  • bottomNavigationBar:顯示在底部的導航欄按鈕欄。能夠查看文檔:Flutter學習之製做底部菜單導航
  • backgroundColor:背景顏色
  • resizeToAvoidBottomPadding: 控制界面內容 body 是否從新佈局來避免底部被覆蓋了,好比當鍵盤顯示的時候,從新佈局避免被鍵盤蓋住內容。默認值爲 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('大學', 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),)
             ],
           ),
         ),
       ),
     ],
   ),
 ),

效果:
1.png數組

頂欄自定義 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(
                "諾獎得主爲上課推遲發佈會",
                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),
              ),
            ),
          )
        ],
      ),
    );
  }

2.png

圖片圓角

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/fl...app

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顯示指定行數,超出後顯示省略號

Text(
    content,
    maxLines: 3,
    overflow: TextOverflow.ellipsis,
    style: new TextStyle(fontSize: 14, color: Colors.black54),
),

margin 負值

https://stackoverflow.com/que...

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/que...

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/fl...

捕捉點擊事件

使用GestureDetector包裹widget便可。

child: new GestureDetector(
    onTap: click,
    child: Text(
      name,
      style: TextStyle(color: Colors.black87),
    ),
  ),

Dart 數組方法

https://codeburst.io/top-10-a...

PopupMenuButton 下拉彈窗菜單

https://stackoverflow.com/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('默認排序'),
          ),
        ),
      ),
      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: (_) {}
      )
      
    ],
  ),

3.png

分割線

水平分割線 Divider
垂直分割線 VerticalDivider (無效???)

swiper

https://pub.dartlang.org/pack...

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(),
  ),

floatingActionButton 浮動button

https://proandroiddev.com/a-d...

floatingActionButton 配合 Scaffold 使用最佳

Scaffold(
    floatingActionButton: new FloatingActionButton(
        onPressed: (){},
        child: Icon(Icons.edit),
        //mini: true,
      ),
    // 默認右下角,可設置位置。
    floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
)

滑動視圖

SingleChildScrollView

水平方向滑動 scrollDirection: Axis.horizontal

高斯模糊

https://stackoverflow.com/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.flutter.io/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();
              },
            ),
          ],
        );
      },
    );
  }
  
// 調用
....
onPressed: (){
  _showDialog(context);
},
....

4.png

HTTP 請求、JSON編碼解碼

https://flutterchina.club/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';
}

// 保存返回的數據
// 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;
});

時間控制:延時

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://juejin.im/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運行模式

https://www.jianshu.com/p/4db...

flutter學習資源

http://flutter.link/

➜  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-

報錯解決

一、在安卓真機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://medium.com/flutter-co...

佈局指南

在scrollView的滾動佈局中,若是使用column組件,併爲其添加Expanded擴展子組件的話,這二者會存在衝突。
若是堅持要使用此佈局,在column設置mainAxisSize: MainAxisSize.min,同時子組件由Expanded改成Flexible便可。

表單、校驗

https://www.cnblogs.com/pengs...

一、單行文本輸入框 TextFormField

new TextFormField(
    maxLength: 32,
    onSaved: (val)=> this._config = val,
    validator: (v)=>(v == null || v.isEmpty)?"請選擇配置": null,
    decoration: new InputDecoration(
      labelText: '配置',
    ),
  ),

5.png

三、多行輸入框 keyboardType: TextInputType.multiline,

new TextField(
    keyboardType: TextInputType.multiline,
    maxLines: 3,
    maxLength: 100,
  ),

6.png

四、單選Radio

new Radio(
    groupValue: this.radio,
    activeColor: Colors.blue,
    value: 'aaa',
    onChanged: (String val) {
      // val 與 value 的類型對應
      this.setState(() {
        this.radio = val;  // aaa
      });
    },
  ),

7.png

五、複選 CheckBox

new Checkbox(
    value: flutter,
    activeColor: Colors.blue,
    onChanged: (val) {
      setState(() {
        flutter = val;
      });
    },
  ),

8.png

六、switch

new Switch(
    activeColor: Colors.green,
    value: flutter,
    onChanged: (val) {
      setState(() {
        flutter = val;
      });
    },
  ),

9.png

七、slider

new Slider(
    value: _slider,
    min: 0.0,
    max: 100.0,
    onChanged: (val) {
      setState(() {
        _slider = val;
      });
    },
  ),

10.png

八、DateTimePicker

// 設置存儲日期的變量
DateTime _dateTime = new DateTime.now();

// 顯示文字Text,設置點擊事件,點擊後打開日期選擇器
  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;
      });
    }
  }

11.png

九、TimePIcker

TimeOfDay _time = new TimeOfDay.now();
  
// text顯示當前時間
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;
      });
    }
  }

12.png

Toast/showSnackBar

showSnackBar:

https://material.io/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');
        },
      ),
    );
  }

13.png

IOS風格組件

https://flutter-es.io/widgets...

Dismissible 滑動刪除

https://flutter.io/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'),
        ),
      ],
    );
  }

14.png

經常使用手勢 GestureDetector

new GestureDetector(
  onTap: (){_showToast('點擊: $i');},
  onDoubleTap: (){_showToast('連點: $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: (最經常使用場景,從相冊選擇或手機拍照獲得照片)

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;      
    });    
  }

15.png
16.png
17.png
18.png

camera: (高階用法,打開相機,實時獲取相機流,能夠定製拍照、錄像等按鈕。可用於相機掃碼、實時識別、直播等場景)

https://pub.dartlang.org/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),
        ),
      )
  }
}

19.png

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.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,
  )

20.png

AudioPlayer

https://github.com/rxlabz/aud...

21.png

相關文章
相關標籤/搜索