Android Flutter 構建佈局UI實戰(二)
這幾天一直在學習, 今天有時間整理一下學習的內容用於記錄與分享,詳細的控制元件使用描述有興趣的可以去官網上看,我這邊自己寫了一個很簡單的小demo,包含了一些基礎的知識。

surprised.png
記錄的知識點:
· 1 底部選單導航
· 2 頁面的跳轉
· 3 ListView
· 4 吐司(這個是內部實現引用的,具體flutter自帶的框架,暫時不清楚)
· 5 涉及到一些佈局的書寫(屬性)
· 6 res資源的引用
· 7 涉及到的一些widget使用介紹,或在註解或在程式碼片段後。
專案效果圖:

home.png
一、頁面的跳轉
我自己寫了一個頁面就放了一個RaisedButton,跳轉到首頁。

transition.png
RaisedButton就是一個button,實現onPressed監聽btn事件。
Navigator這個是用來進行跳轉頁面的。
涉及到的一些需要介紹的控制元件我用都**來表示註解了, //看不清楚
import 'package:flutter/material.dart'; **基本多是這個包 import 'package:flutter_app/MainActivity.dart'; **我跳轉的首頁面 void main() => runApp(new MyApp()); class MyApp extends StatelessWidget{ @override Widget build(BuildContext context) { return new MaterialApp(**MaterialApp個人理解為程式的渲染入口 title: 'Hello World', theme: new ThemeData( **全域性主題只是由應用程式根MaterialApp建立的Theme來表示 primaryColor: Colors.lightBlue, ), home: new RandomWords(),**呼叫方法體 ); } } class RandomWordsStateextends State<RandomWords>{ **Scaffold 是 Material library 中提供的一個widget, 它提供了預設的導航欄、標題和包含主螢幕widget樹的body屬性。widget樹可以很複雜。 ** Center這個空間居中 ** RaisedButton= button ** Navigator頁面的跳轉,差不多都是這個寫法,固定 @override Widget build(BuildContext context) { return new Scaffold( body: new Center( child: new RaisedButton( child: new Text('登入'), onPressed: (){ Navigator.push(context, new MaterialPageRoute(builder: (context)=>new MainActivity())); }), ), ); } } class RandomWords extends StatefulWidget { @override ** =>單行函式或方法的簡寫 createState() => new RandomWordsState(); }
其中StatelessWidget它是表示所有的屬性都是最終的,可以理解為屬性不可變。
StatefulWidget 我覺得可以理解為android中某一個自定義的方法(程式碼書寫ui),方法體的內容是可變的,當然它也是一個widget。在flutter中書寫一個這樣的方法,就需要按照上述程式碼中的方式來書寫。
二、MainActivity頁面
- images檔案的引用。
頁面中包含了一些圖片資源,記錄一下images的引用方式。

專案結構圖.png
一開始Flutter是沒有images資料夾的,自己建立一個,跟android ios保持同級。
在pubspec.yaml
檔案中進行images的關聯,
pubspec.yaml
這個可以理解為build.gradle。

引用介面.png
在Flutter的節點下新增(引用全目錄 ,若單張全名稱包含字尾):
assets:
- images/
新增完畢後在右上角有同步按鈕,別忘了

image.png
package get 載入引入的包
package upgradle 升級包
flutter upgradle 整理升級 包括Dart SDK version等
flutter doctor 檢測需要安裝的東西
-
包的引用
當初在看官方的時候,引用了一個english_words的包,專案中沒有用,但是 這邊記錄一下引用包的方式。
image.png
在 lib/main.dart
中 import 'package:english_words/english_words.dart';
就可以了,需要注意的是,在 pubspec.yaml
中添加了之後記得package get,在彈出的message視窗中Process finished with exit code 0 表示引用成功。
- 底部導航 BottomNavigationBarItem
在看程式碼之前:
此處簡要一下程式碼的書寫邏輯。
因為頁面是可變可調整的,所以我肯定需要書寫StatefulWidget。
接著 初始了切換的圖片,文字等資源
在BottomNavigationBarItem中主要是通過 下標 切換圖片和文字的顯示,當然也包含切換頁面,切換頁面的書寫方式類似android中的fragment,屬於獨立頁面,配合使用IndexedStack進行切換頁面的顯示與隱藏。
具體的程式碼含義,我在註釋裡進行介紹。
import 'package:flutter/cupertino.dart'; **底部導航切換,需匯入 import 'package:flutter/material.dart'; **上面註解介紹過 import 'package:flutter_app/page/homeinfo.dart';**fragment頁面 import 'package:flutter_app/page/myinfo.dart';**fragment頁面 void main() => runApp(new MainActivity()); class MainActivity extends StatelessWidget { **這塊沒啥介紹的, 同上 @override Widget build(BuildContext context) { return new MaterialApp( title: 'Hello World', theme: new ThemeData( primaryColor: Colors.blue, ), home: new RandomWords(), ); } } class RandomWordsState extends State<RandomWords> { int _tabIndex = 0;** 預設當前頁 //static const double IMAGE_ICON_WIDTH = 30.0; 標題上的返回按鈕 //static const double ARROW_ICON_HEIGHT = 16.0; 標題上的返回按鈕 final normalTextColor = new TextStyle(color: const Color(0xff969696)); **預設的顏色 final selectTextColor = new TextStyle(color: const Color(0xff63ca6c)); **選擇的顏色 var tabImage;**切換的image var _body;**IndexedStack的物件 var tabNameList = ['首頁', '地圖', '我的']; **底部導航名稱 var titleNameList = ['動服務平臺', '地圖', '我的']; **標題名稱 //var leftIcon;標題上的返回按鈕 //RandomWordsState(){ //leftIcon = setImages("images/icon_left.png"); //} **統一設定image屬性 ,path為images的引用路徑 Image getImagePath(path) { return new Image.asset( path, width: 20.0, height: 20.0, ); } **切換圖片的初始化,包括切換頁面的body初始 , getImagePath為統一設定的images屬性。 void initData() { if (tabImage == null) { tabImage = [ [ getImagePath('images/activity_home_unchecked.png'), getImagePath('images/activity_home_checked.png') ], [ getImagePath('images/activity_map_unchecked.png'), getImagePath('images/activity_map_checked.png') ], [ getImagePath('images/activity_mine_unchecked.png'), getImagePath('images/activity_mine_checked.png') ], ]; } ** children這個我個人理解它是一個組合控制元件,像是一個容器,可以包含很多不同的Ui,然後拼湊到一起。 _body = new IndexedStack( children: <Widget>[new HomeInfo(), new MyInfo(), new MyInfo()], index: _tabIndex, ); } ** 根據下標返回 text的顏色值 TextStyle getTabTextStyle(int curIndex) { if (curIndex == _tabIndex) { return selectTextColor; } return normalTextColor; } ** 呼叫getTabTextStyle 根據下標設定text的顏色值 Text getTabTitle(int curIndex) { return new Text(tabNameList[curIndex], style: getTabTextStyle(curIndex)); } ** 返回當前下標的images中的 所選圖片 Image getTabIcon(int curIndex) { if (curIndex == _tabIndex) { return tabImage[curIndex][1]; } return tabImage[curIndex][0]; } //設定iamge的位置 //Widget setImages(path) { //return new Padding( //padding: const EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0), //child: new Image.asset(path, //width: IMAGE_ICON_WIDTH, height: ARROW_ICON_HEIGHT)); //} ** AppBar標題{title標題文字 {Center標題位置 child{標題內容} } } ** body切換的index頁面 ** bottomNavigationBar底部導航{items導航陣列{0,1,2}} **onTap點選(Index作為點選返回值) {setState通知框架狀態已經改變{_tabIndex 賦值當前Index}} @override Widget build(BuildContext context) { initData(); return new MaterialApp( home: new Scaffold( appBar: new AppBar( title: new Center( child: new Text(titleNameList[_tabIndex], style: new TextStyle(color: Colors.white)), //child: new Row( //children: <Widget>[ //leftIcon, //new Text(tabNameList[_tabIndex], //style: new TextStyle(color: Colors.white)) //], //), ), iconTheme: new IconThemeData(color: Colors.white)), body: _body, bottomNavigationBar: new CupertinoTabBar( items: <BottomNavigationBarItem>[ new BottomNavigationBarItem( icon: getTabIcon(0), title: getTabTitle(0)), new BottomNavigationBarItem( icon: getTabIcon(1), title: getTabTitle(1)), new BottomNavigationBarItem( icon: getTabIcon(2), title: getTabTitle(2)), ], currentIndex: _tabIndex, onTap: (index) { setState(() { _tabIndex = index; }); }, ), ), ); } } class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState(); }
底部切換基本就這些,你要是隻是想測試一下底部切換效果也可以像我indexedStack那樣一樣,除了首頁,剩下的複用。
-
HomeInfo.dart頁面
HomeInfo.png
考慮到頁面的佈局,我分成了2個層級,網格顯示是一個層級,報表是另外的一個層級,可以說是ListView 的兩個item,只不過是不同的item佈局。當然這只是我個人的想法,經過思考後在StatefulWidget,我返回的就是一個ListView .
var title = ["專案資訊", "農村公路建設統計報表", "路網結構改造統計報表"]; @override Widget build(BuildContext context) { var listview = new ListView.builder( itemCount: title.length, itemBuilder: (context, i) => renderRow(i)); return listview; }
ListView 初始化:
itemcount 網格是一個單獨的佈局,另外的兩個可以複用佈局。
renderRow是我自定義的方法
i 算是0 、1、 2,其中1、2佈局複用。
renderRow(int i) { if (i == 0) { ** 此處i=0初始化網格的樣式。 var projectInfo = new Container( //color: const Color.fromRGBO(255, 255, 255, 255.0), decoration: new BoxDecoration( color: Colors.white, ), child: new Center( child: new Column( children: <Widget>[ new Text( title[0], textAlign: TextAlign.left, ), new Container( color: const Color.fromRGBO(240, 248, 255, 200.0), child: new Row( children: <Widget>[ new Expanded( flex: 1, child: new Container( margin: const EdgeInsets.only( top: 10.0, right: 5.0, left: 10.0, bottom: 10.0), decoration: new BoxDecoration( border: new Border.all( width: 1.0, color: Colors.black12), borderRadius: const BorderRadius.all( const Radius.circular(10.0))), height: 100.0, child: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new IconButton( icon: new Image.asset( "images/icon_way.png", width: 50.0, height: 50.0, ), onPressed: () { showShort("農村公路建設類專案"); }), new Center(child: new Text("農村公路建設類專案")) ], ), ), )), new Expanded( flex: 1, child: new Container( margin: const EdgeInsets.only( top: 10.0, right: 10.0, left: 5.0, bottom: 10.0), decoration: new BoxDecoration( border: new Border.all( width: 1.0, color: Colors.black12), borderRadius: const BorderRadius.all( const Radius.circular(10.0))), height: 100.0, child: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new IconButton( icon: new Image.asset( "images/icon_reform.png", width: 50.0, height: 50.0, ), onPressed: () { showShort("農村公路建設類專案"); }), new Center(child: new Text("危橋改造類專案")) ], )), )) ], ), ), new Container( child: new Row( children: <Widget>[ new Expanded( flex: 1, child: new Container( margin: const EdgeInsets.only( top: 0.0, right: 5.0, left: 10.0, bottom: 10.0), decoration: new BoxDecoration( border: new Border.all( width: 1.0, color: Colors.black12), borderRadius: const BorderRadius.all( const Radius.circular(10.0))), height: 100.0, child: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new IconButton( icon: new Image.asset( "images/icon_security.png", width: 50.0, height: 50.0, ), onPressed: () { showShort("縣鄉安防工程類專案"); }), new Center(child: new Text("縣鄉安防工程類專案")) ], ), ), )), new Expanded( flex: 1, child: new Container( margin: const EdgeInsets.only( top: 0.0, right: 10.0, left: 5.0, bottom: 10.0), decoration: new BoxDecoration( border: new Border.all( width: 1.0, color: Colors.black12), borderRadius: const BorderRadius.all( const Radius.circular(10.0))), height: 100.0, child: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new IconButton( icon: new Image.asset( "images/icon_security_green.png", width: 50.0, height: 50.0, ), onPressed: () { showShort("村道安防工程類專案"); }), new Center(child: new Text("村道安防工程類專案")) ], )), )) ], ), ), ], ), ), ); return new GestureDetector( child: projectInfo, ); } new Container( child: new Text(title[i]), ); var listCountItem = new Padding( padding: const EdgeInsets.fromLTRB(15.0, 10.0, 0.0, 10.0), child: new Column( children: <Widget>[ new Container( child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ new Text(title[i]), new Container( height: 200.0, child: new ListView( children: <Widget>[ new ListTile( leading: new Icon(Icons.map), title: new Text('Maps'), ), new ListTile( leading: new Icon(Icons.photo_album), title: new Text('Album'), ), new ListTile( leading: new Icon(Icons.phone), title: new Text('Phone'), ), ], ), ) ], ), ), //new ListView( //children: <Widget>[ //new ListTile( //title: new Text('123123'), //) //], //) ], ), ); return new InkWell( child: listCountItem, onTap: () { showShort("1111"); }, ); }
部分控制元件屬性介紹:
- Container也是一個widget,允許自定義其子widget。
個人理解就是一個容器,可以新增一些別的widget組合成想要的ui樣式 。可新增填充,邊距,邊框或背景色。 - BoxDecoration這個類似shape可以操作內填充顏色,圓角等
- Expanded 這個可以理解為權重,flex表示當前包裹控制元件所在父佈局的權重比例。
- children: <Widget>[]這個屬性相當於多個Item一樣,陣列中的每一個值都可以看做成一個Item,具體什麼樣的ui樣式看你自己怎麼寫。
- mainAxisAlignment和crossAxisAlignment屬性用來對齊其子項。 對於行(Row)來說,主軸是水平方向,橫軸垂直方向。對於列(Column)來說,主軸垂直方向,橫軸水平方向。具體的詳細引數,對照官網。
- GestureDetector這個是用來檢測使用者做出的手勢,點選的時候會回撥onTap,我這邊沒有寫onTap,我是單獨在iconButton中做的點選處理。寫的有點問題,不過順帶的介紹一下這個。
- InkWell實現水波紋,邊框效果,跟BoxDecorationc差不多。
關於吐司showShort,分享實現方式(改的原生):
-
Android.png
new MethodChannel(getFlutterView(), "com.coofee.flutterdemoapp/sdk/toast") .setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { if ("show".equals(methodCall.method)) { String text = methodCall.argument("text"); int duration = methodCall.argument("duration"); Toast.makeText(MainActivity.this, text, duration).show(); } } });
-
IOS
image.png
#include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; FlutterMethodChannel* toastChannel = [FlutterMethodChannel methodChannelWithName:@"com.coofee.flutterdemoapp/sdk/toast" binaryMessenger:controller]; [toastChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { if ([@"show" isEqualToString:call.method]) { // 展示toast; NSLog(@"顯示toast....") } }]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end
- Flutter
import 'package:flutter/services.dart'; // 下劃線開頭的變數只在當前package中可見。 const _toast = const MethodChannel('com.coofee.flutterdemoapp/sdk/toast'); const int _LENGTH_SHORT = 0; const int _LENGTH_LONG = 1; void show(String text, int duration) async { try { await _toast.invokeMethod("show", {'text': text, 'duration': duration}); } on Exception catch (e) { print(e); } on Error catch (e) { print(e); } } void showShort(String text) { show(text, _LENGTH_SHORT); } void showLong(String text) { show(text, _LENGTH_LONG); }
-
MyInfo.dart頁面
MyInfo.png
MyInfo相對上一個頁面要簡單不少,主要是ListView,剩下的就是一些資源的初始化。
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_app/utils/toastdart.dart'; class MyInfo extends StatefulWidget { @override createState() => new MyInfoState(); } class MyInfoState extends State<MyInfo> { static const double IMAGE_ICON_WIDTH = 30.0; static const double ARROW_ICON_WIDTH = 16.0; var inons = []; var titleTextStyle = new TextStyle(fontSize: 16.0); var title = ["使用者指南", "地圖設定", "路網資料", "資料備份", "專案資料更新", "關於系統", "退出登入"]; var images = [ "images/one.png", "images/two.png", "images/three.png", "images/four.png", "images/five.png", "images/six.png", "images/senven.png", ]; var rightIcon = new Image.asset( "images/icon_right.png", width: IMAGE_ICON_WIDTH, height: ARROW_ICON_WIDTH, ); MyInfoState() { for (int i = 0; i < images.length; i++) { inons.add(setImages(images[i])); } } //設定iamge的位置 Widget setImages(path) { return new Padding( padding: const EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0), child: new Image.asset(path, width: IMAGE_ICON_WIDTH, height: ARROW_ICON_WIDTH)); } @override Widget build(BuildContext context) { var listview = new ListView.builder( itemCount: title.length , itemBuilder: (context, i) => renderRow(i)); return listview; } renderRow(int i) { String itemName =title[i]; var itemCount =new Padding( padding: const EdgeInsets.fromLTRB(25.0, 25.0, 25.0, 25.0), child: new Row( children: <Widget>[ inons[i], new Expanded( child: new Text( itemName, style: titleTextStyle, )), rightIcon ], ), ); return new InkWell( child: itemCount, onTap: (){ //toast showShort(itemName); //Navigator.of(context).push(new MaterialPageRoute( //builder: (context)=> new MainActivity())); }, ); } }
EdgeInsets類似Android裡面的margin。
總結:
萬物皆Widget。
若看的不太舒服,望見諒···