谷歌移動UI框架Flutter教程之Widget

引言

在之間我已經介紹了關於Flutter的下載安裝以及配置,還有開發工具Android Studio的配置,還不知道的同學可以看看我這篇博客——谷歌移動UI框架Flutter入門。這裡為什麼非要用Android Studio,我可以解釋一下。Android Studio是Google的親兒子,由谷歌一手開發,而Flutter也是谷歌推出的技術,所以在支持和兼容問題上,Android Studio是非常有優勢的。老話說得好,肥水不流外人田,谷歌內部肯定是將Android Studio對Flutter的優化做到最佳的。

Widget基本組件

那麼話不多說,我們先來熟悉一下關於Flutter的Widget組件,在Flutter中,一切皆組件,TextView、Image、Row、Column等等,都統稱組件。

1.文本組件(Text)

首先,我們就來了解一下文本組件(Text)。學過前端的同學對UI部分應該都很了解,那Flutter當然也沒有什麼特別的,無非也就是文本內容、大小、字體樣式、顏色等等的設置,那麼首先我們就先來編寫一個案例。找到lib目錄下的main.dart,我們將在這個文件中編寫代碼。

import 'package:flutter/material.dart';    void main() {    runApp(MyTextApp());  }    /**   * 文本組件(Text)的使用   */  class MyTextApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'Text Demo',        home: Scaffold(          appBar: AppBar(title: Text('文本控制項的使用'),),          body: Center(            child: Text(              '這是一個文本控制項', //文本內容              textAlign: TextAlign.center, //居中              maxLines: 1, //最大顯示行數              style: TextStyle(                fontSize: 25.0, //字體大小                color: Colors.lightBlue, //字體顏色              ), //樣式            ),          ),        ),      );    }  }

有語言基礎的同學相信可以很好理解這些代碼,第一行導入了Material相關的類庫。程序會先執行main()方法,該方法又執行了runApp()方法,並將MyTextApp類作為參數傳遞。而MyTextApp類就是我們自定義的一個類,該類需要去繼承StatelessWidget,並重寫build()方法,該方法需要返回一個組件。具體的代碼我就不一一介紹了,可以先不用理解每一行代碼的意思。其中的Text便是文本組件,只需將值寫入括弧,便可以在文本框中顯示,然後是文本框的一些屬性。接下來我們運行起來看一下。

2.圖片組件(Image)

接下來是圖片組件,圖片組件的作用無非就是顯示圖片,在Flutter中,Image有四種方式顯示圖片,我只介紹一種,就是顯示網路圖片,其它三種方式沒有太大差別。

import 'package:flutter/material.dart';    void main() {    runApp(MyImageApp());  }    /**   * 圖片組件(Image)的使用   */  class MyImageApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'Text Demo',        home: Scaffold(          appBar: AppBar(title: Text('圖片組件的使用')),          body: Center(            child: Image.network(              'https://www.baidu.com/img/baidu_jgylogo3.gif', //圖片地址              scale: 1.0, //縮放比            ),          ),        ),      );    }  }

運行效果如下:

3.列表組件(ListView)

列表組件在移動端的開發中使用非常頻繁,那麼在Flutter中,該如何使用ListView呢?

import 'package:flutter/material.dart';    void main() {    runApp(MyListViewApp());  }    /**   * 列表組件(List)的使用   */  class MyListViewApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'Text Demo',        home: Scaffold(          appBar: AppBar(title: Text('圖片組件的使用')),          body: Center(            child: Container(            	height: 200.0,              child: ListView(                scrollDirection: Axis.horizontal,,//列表方向(縱向)                children: <Widget>[                  Container(                    width: 180.0,                    color: Colors.lightBlue,                  ), Container(                    width: 180.0,                    color: Colors.amber,                  ), Container(                    width: 180.0,                    color: Colors.deepOrange,                  ), Container(                    width: 180.0,                    color: Colors.deepPurple,                  ), //Container                ], //Widget[]              ),            ),          ),        ),      );    }  }

有些同學看到這樣的代碼可能驚呆了,這麼多層的嵌套維護起來豈不是很麻煩,其實這也是Dart語法的特點,避免不了,但是還是有辦法的,我們可以把ListView單獨抽出來,這樣主體的代碼將會簡潔很多。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp());  }    class MyApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'FlutterApp',        home: Scaffold( //主頁          appBar: AppBar(title: Text('FlutterDemo')), //標題          body: Center(            child: Container(              height: 200.0,              child: MyList(), //ListView            ), //Container          ), //主體        ), //Scaffold      ); //MaterialApp    }  }    class MyList extends StatelessWidget {      @override    Widget build(BuildContext context) {      return ListView(        scrollDirection: Axis.horizontal,        children: <Widget>[          Container(            width: 180.0,            color: Colors.lightBlue,          ), Container(            width: 180.0,            color: Colors.amber,          ), Container(            width: 180.0,            color: Colors.deepOrange,          ), Container(            width: 180.0,            color: Colors.deepPurple,          ), //Container        ], //Widget[]      );    }  }

現在運行看一下效果。

當然,這樣編寫列表在實際開發中是不現實的,我們應該讓列表活起來,所以,下面介紹如何實現動態列表。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp(        items: List < String>.generate(1000, (i) => "Item $i")    )    );  }    class MyApp extends StatelessWidget {      final List<String> items;      MyApp({Key key, @required this.items}) :super(key: key);      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'FlutterApp',        home: Scaffold( //主頁          appBar: AppBar(title: Text('FlutterDemo')), //標題          body: ListView.builder(            itemCount: items.length,            itemBuilder: (context, index) {              return ListTile(                title: Text('${items[index]}'),              );            },          ),        ), //Scaffold      ); //MaterialApp    }  }

這樣就實現了動態列表,只不過這個數據還是自己提供的,只需要後期通過網路獲取數據再封裝成集合然後傳遞即可。不懂Dart語法的同學對於裡面的某些代碼可能會覺得難以理解,但是不用擔心。即使沒有一點Dart語言基礎的同學也是可以很容易地學會Flutter的,只不過在某些Dart語法上就只能死記了,記住它,不用管為什麼。那麼現在來運行看下效果。

4.列表組件(GridView)

第二個列表組件,網格組件,該組件在如今的移動應用中也非常常見,最典型的便是系統相冊。那麼我們關心的是在Flutter中該如何去使用GridView呢?通過一個例子來了解一下。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp());  }    class MyApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'FlutterApp',        home: Scaffold( //主頁            appBar: AppBar(title: Text('FlutterDemo')), //標題            body: GridView(              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(                  crossAxisCount: 3,                  mainAxisSpacing: 2.0, //縱軸邊距                  crossAxisSpacing: 2.0, //橫軸邊距                  childAspectRatio: 0.7 //縮放比例(寬高比)              ),              children: <Widget>[                Image.network(                    'http://img5.mtime.cn/mg/2019/05/31/163641.36482297_270X405X4.jpg',                    fit: BoxFit.cover),                Image.network(                    'http://img5.mtime.cn/mg/2019/07/01/091243.35485139_270X405X4.jpg',                    fit: BoxFit.cover),                Image.network(                    'http://img5.mtime.cn/mg/2019/06/28/141449.40971533_270X405X4.jpg',                    fit: BoxFit.cover),                Image.network(                    'http://img5.mtime.cn/mg/2019/05/31/163641.36482297_270X405X4.jpg',                    fit: BoxFit.cover),                Image.network(                    'http://img5.mtime.cn/mg/2019/07/01/091243.35485139_270X405X4.jpg',                    fit: BoxFit.cover),                Image.network(                    'http://img5.mtime.cn/mg/2019/06/28/141449.40971533_270X405X4.jpg',                    fit: BoxFit.cover),              ], //Widget[]            ) //GridView        ), //Scaffold      ); //MaterialApp    }  }

網格組件其實也非常的簡單,和ListView其實沒有什麼差別,最主要的就是它獨特的屬性,這些屬性在官網文檔中都有解釋和示例。那麼這段代碼運行的效果如何呢?我們看一下:

布局

Flutter中基本的一些組件就介紹完了,但是光知道如何編寫組件可遠遠不夠,UI設計中的布局管理也尤為重要,那麼,我們繼續深入,了解一下Flutter中的布局。

1.水平布局(Row)

經過前面基本組件的學習,會發現Flutter無非就是一些組件的嵌套,但注意嵌套級別,不要被自己的代碼搞暈了,那麼布局其實是一樣的。我們看一個例子。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp());  }    class MyApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'Row Widget Demo',        home: Scaffold(          appBar: AppBar(            title: Text('水平方向布局'),          ), //AppBar          body: Row(            children: <Widget>[              RaisedButton(                onPressed: () {},                color: Colors.redAccent,                child: Text('Red Button'),              ),RaisedButton(                onPressed: () {},                color: Colors.orangeAccent,                child: Text('Orange Button'),              ),RaisedButton(                onPressed: () {},                color: Colors.lightBlue,                child: Text('Blue Button'),              ),            ], //Widget[]          ), //Row        ), //Scaffold      ); //MaterialApp    }  }

Row即是水平布局,那麼水平布局中我們放置了三個按鈕,現在,運行看效果。

會發現 ,這個按鈕的右邊空出了一塊,這是為什麼呢?其實是因為我們使用的是一個不靈活的水平布局,那麼既然有不靈活的水平布局,那就肯定會有靈活的水平布局。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp());  }    class MyApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'Row Widget Demo',        home: Scaffold(          appBar: AppBar(            title: Text('水平方向布局'),          ), //AppBar          body: Row(            children: <Widget>[              Expanded(child: RaisedButton(                onPressed: () {},                color: Colors.redAccent,                child: Text('Red Button'),              )), Expanded(child: RaisedButton(                onPressed: () {},                color: Colors.orangeAccent,                child: Text('Orange Button'),              )), Expanded(child: RaisedButton(                onPressed: () {},                color: Colors.lightBlue,                child: Text('Blue Button'),              )),            ], //Widget[]          ), //Row        ), //Scaffold      ); //MaterialApp    }  }

我們並沒有對代碼進行過多的修改,只是在每個按鈕外部包了一個Expanded組件,那麼現在我們來看一下運行效果:

會發現,按鈕成功自適應屏幕了,這才是我們想要的效果。

2.垂直布局(Column)

既然有水平布局,當然就有垂直布局。現在通過一個例子來理解一下垂直布局。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp());  }    class MyApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      return MaterialApp(        title: 'Row Widget Demo',        home: Scaffold(            appBar: AppBar(              title: Text('水平方向布局'),            ), //AppBar            body: Column(              children: <Widget>[                Text('Column 1'),                Text('This is Column 2'),                Text('Column 3'),              ], //Widget            ) //Column        ), //Scaffold      ); //MaterialApp    }  }

應該不難理解,道理是一樣的,現在看一下效果:

細心的同學會發現,它默認會有一個居中的對齊方式。但有同學提出疑問了,這也沒居中啊,這不還是在屏幕的左側嗎?其實這個對齊是相對Column來說的,這個Column的大小是由最長的Text組件決定的。通過crossAxisAlignment屬性可以設置Column的對齊方式。

3.層疊布局(Stack)

使用水平布局和垂直布局雖然可以實現大部分的布局效果,但是如果要在一張圖片上顯示一段文字,這兩種布局將無法實現。所以,這裡我們學習一種層疊布局,它能夠很輕鬆地實現這個效果。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp());  }    class MyApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      var stack = Stack(        alignment: const FractionalOffset(0.5, 0.8),        children: <Widget>[          CircleAvatar(            backgroundImage: NetworkImage(                'https://i0.hdslb.com/bfs/archive/[email protected]_440h.jpg'),            radius: 100.0,          ), //CircleAvatar          Container(            decoration: BoxDecoration(                color: Colors.lightBlue            ),            padding: EdgeInsets.all(5.0),            child: Text('層疊布局'),          )        ], //Widget[]      ); //Stack      return MaterialApp(        title: 'Row Widget Demo',        home: Scaffold(            appBar: AppBar(              title: Text('水平方向布局'),            ), //AppBar            body: Center(              child: stack,            )        ), //Scaffold      ); //MaterialApp    }  }

我們首先創建一個組件變數,將我們的圖片和文字都定義在裡面,然後通過alignment屬性可以決定文本組件的相對位置,看一下效果:

4.卡片布局(Card)

最後一個布局,卡片布局。來看例子。

import 'package:flutter/material.dart';    void main() {    runApp(MyApp());  }    class MyApp extends StatelessWidget {      @override    Widget build(BuildContext context) {      var card = Card(        child: Column(          children: <Widget>[            ListTile(              title: Text(                '江西省南昌市青雲譜區', style: TextStyle(fontWeight: FontWeight.w500),),              subtitle: Text('Temptation:123456789'),              leading: Icon(Icons.account_box, color: Colors.lightBlue,),            ),            new Divider(),            ListTile(              title: Text(                '北京市海淀區中國科技大學', style: TextStyle(fontWeight: FontWeight.w500),),              subtitle: Text('Temptation:123456789'),              leading: Icon(Icons.account_box, color: Colors.lightBlue,),            ),            new Divider(),            ListTile(              title: Text(                '河南省濮陽市百姓辦公樓', style: TextStyle(fontWeight: FontWeight.w500),),              subtitle: Text('Temptation:123456789'),              leading: Icon(Icons.account_box, color: Colors.lightBlue,),            ) //ListTile          ], //Widget[]        ), //Column      ); //Card        return MaterialApp(        title: 'Row Widget Demo',        home: Scaffold(            appBar: AppBar(              title: Text('水平方向布局'),            ), //AppBar            body: Center(                child: card            )        ), //Scaffold      ); //MaterialApp    }  }

運行看效果:

篇幅有限,關於Flutter的組件和布局就介紹到這裡,接下來還會有一篇關於Flutter的進階博客,感興趣的同學可以看一看。