Flutter入門篇(二)
在上一篇文章中以簡單的方式對Flutter自己提供的演示進行了一個簡單的分析,當然那是遠遠不夠。本來打算為大家帶來官網上的無限下拉重新整理的案例,但是發現這裡的有些東西實在是太超前了,作為Flutter入門篇,當然不能這麼隨意,以為了讓大家都能夠學有所得,所以今天給大家帶來了自己手擼的一個登入。

登入演示
簡單分析佈局
我們都知道,一個簡單的登入需要至少需要3步:
- 輸入賬號
- 輸入密碼
- 點選登入
那麼我們的佈局也就至少需要3個 widget
,為什麼說至少呢?因為往往佈局使用的 widget
都是大於操作步驟的。這裡跟大家分享我的佈局大概有這麼幾個:
widget widget widget
Scaffold
為什麼要講解這個呢?這是因為它是實現了 Mataril Design
的一種簡單的“腳手架”,有些也叫“支架”,通過這個翻譯也就知道了,其實它就是向我們提供了簡單的框架,我們直接使用它就行了。那麼問題來了,我們可不可以不使用它呢?當然是可以的,但是不建議這樣做,因為我們後面需要使用的很多 widget
(比如 TextField
)必須要在它的支援下才能執行,不然就會報錯了。
class Scaffold extends StatefulWidget { /// Creates a visual scaffold for material design widgets. const Scaffold({ Key key, this.appBar, //橫向水平佈局,通常顯示在頂部(*) this.body, // 內容(*) this.floatingActionButton, //懸浮按鈕,就是上圖右下角按鈕(*) this.floatingActionButtonLocation, //懸浮按鈕位置 //懸浮按鈕在[floatingActionButtonLocation]出現/消失動畫 this.floatingActionButtonAnimator, //在底部呈現一組button,顯示於[bottomNavigationBar]之上,[body]之下 this.persistentFooterButtons, //一個垂直面板,顯示於左側,初始處於隱藏狀態(*) this.drawer, this.endDrawer, //出現於底部的一系列水平按鈕(*) this.bottomNavigationBar, //底部持久化提示框 this.bottomSheet, //內容背景顏色 this.backgroundColor, //棄用,使用[resizeToAvoidBottomInset] this.resizeToAvoidBottomPadding, //重新計算佈局空間大小 this.resizeToAvoidBottomInset, //是否顯示到底部,預設為true將顯示到頂部狀態列 this.primary = true, // this.drawerDragStartBehavior = DragStartBehavior.down, }) : assert(primary != null), assert(drawerDragStartBehavior != null), super(key: key);
從這裡面,我們可以看出 Scaffold
提供了很多的方式方法,去實現 Mataril Design
的佈局:
AppBar
一般就用於 Scaffold.appBar
,是一個置於螢幕頂部的橫向佈局,為什麼是橫向呢?可以如下中看出:

AppBar-橫向佈局
我在它其中的 anctions
屬性中設定了多個 widget
,然後就向這樣後面那三就一溜的按順序排好了。
AppBar( title: Text('Sample Code'), leading: IconButton( icon: Icon(Icons.view_quilt), tooltip: 'Air it', onPressed: () {}, ), bottom: TabBar(tabs: tabs.map((e) => Tab(text: e)).toList(),controller: _tabController), actions: <Widget>[ IconButton( icon: Icon(Icons.playlist_play), tooltip: 'Air it', onPressed: () {}, ), IconButton( icon: Icon(Icons.playlist_add), tooltip: 'Restitch it', onPressed: () {}, ), IconButton( icon: Icon(Icons.playlist_add_check), tooltip: 'Repair it', onPressed: () {}, ) ], )
對於上述中 leading
需要說明一下,一般我們用它來顯示一個按鈕去關閉當前頁面或者開啟一個 drawer
。有興趣的可以去試試~~
在 AppBar
眾多的屬性中,還有一個是我們比較常用的,那就是 bottom
,這個顯示於工具欄的下方,注意不是螢幕底部哦!一般使用 TabBar
來實現一個頁面包含中多個不同頁面的切換。

AppBar-使用tabBar
當然還有其他一些方式方法,這裡就不多佔用篇幅了,就簡單聊聊:
title drawer centerTitle
如果想看完整的實現方式,就 跟我來吧 !
BottomNavigationBar
這個屬性也是相當重要的,如果我們想要實現多個,不同頁面的切換,就可以使用這個。咦?這個不是說過了麼?
BottomNavigationBar與AppBar裡面的TabBar是不同的,一個是用來顯示於頂部,一個用來顯示與底部

BottomNavigationBar
在我們國內的應用中很少向這樣出現可以浮動選擇項,所以如果想讓你的App不進行浮動的話,可以使用裡面的一個 type
屬性。
type: BottomNavigationBarType.fixed,
BottomNavigationBarType有兩值,就是fixed,還有一個就是shifting,預設是shifting。這樣設定之後仍然存在一個問題:就是選中的按鈕的字型仍然會比未選中的大一點,有興趣的可以自己去驗證一下。

BottomNavigationBar-選中
那麼這個問題改怎麼辦呢?很遺憾,在最新穩定版(Flutter Stable 1.2.1)SDK中並沒有處理這個問題的方式方法。如果想要解決這個問題的話,更換Flutter SDK到最新的開發版本(Flutter Dev 1.3.8),就可以使用它的屬性去解決這個問題。
selectedItemColor: colorRegular, //選中顏色 unselectedItemColor: colorBlack,//未選擇顏色 selectedFontSize: 12,//選中字型大小 unselectedFontSize: 12,//未選中字型大小
FloatingActionButton
個人覺得這個 FloatingActionButton
還是需要說明一下的,畢竟用的時候還是比較多的。 FloatingActionButton
是一個浮動按鈕,也就是上面那個帶“+”的按鈕,這個可以用來新增,分享,或者是導航。可以與 Scaffold
中兩個屬性配合使用
- FloatingActionButtonLocation
- FloatingActionButtonAnimator
FloatingActionButtonLocation
屬性可以移動浮動按鈕的位置,有如下幾個位置可以移動:
FloatingActionButtonLocation.endDocked //右側bottomNagivationBar遮蓋 FloatingActionButtonLocation.centerDocked //居中bottomNagivationBar上遮蓋 FloatingActionButtonLocation.endFloat //bottomNagivationBar上方右側顯示 FloatingActionButtonLocation.centerFloat //bottomNagivationBar上方居中顯示
自己可以試一試,這裡就不一一演示,只演示一下這個 centerDocked

浮動居中
FloatingActionButtonAnimator
就是 FloatingActionButton
在出現位置 FloatingActionButtonLocation
的動畫效果~~
需要注意以下幾點:
- 如果一個頁面有多個
FloatingActionButtonLocation
,那麼就需要讓每一個浮動按鈕都有自己且唯一的heroTag
。 - 如果
onPressed
返回了null,那麼它將不會對你的觸控進行任何反應,不推薦這樣去展示一個無任何響應的浮動按鈕。
SnackBar
經常在我們的應用中會使用到資訊提示,那麼我們就可以使用showSnackBar的方式去顯示一個簡短的提示,預設顯示4s。

SnackBar
class SnackTest extends StatelessWidget{ @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Demo') ), body: Center( child: RaisedButton( child: Text('SHOW A SNACKBAR'), onPressed: () { Scaffold.of(context).showSnackBar(SnackBar( content: Text('Hello!'), )); }, ), ) ); } }
一般我們會向如上方式處理,但是可能會丟擲一個 Scaffold.of() called with a context that does not contain a Scaffold.
的異常,也不會顯示出 snackBar
。
這是因為, Scaffold.of()
所需的context是Scaffold的,並不是Scaffold上方的build(BuildContext context)中的,這兩個並不是一個。
正確的方式是,建立自己的context:
class SnackTest extends StatelessWidget{ @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Demo') ), body: Builder( // Create an inner BuildContext so that the onPressed methods // can refer to the Scaffold with Scaffold.of(). builder: (BuildContext context) { return Center( child: RaisedButton( child: Text('SHOW A SNACKBAR'), onPressed: () { Scaffold.of(context).showSnackBar(SnackBar( content: Text('Hello!'), )); }, ), ); }, ), ); } }
當然還可以使用 GlobalKey
的方式:
class ScaffoldTestState extends State<ScaffoldTest> { final _scaffoldKey = GlobalKey<ScaffoldState>(); void showSnackBar() { _scaffoldKey.currentState .showSnackBar(new SnackBar(content: Text("SnackBar is Showing!"))); } return new Scaffold( key: _scaffoldKey, body: Center( child: RaisedButton( child: Text('SHOW A SNACKBAR'), onPressed: () { showSnackBar(), )); }, ), ) } }
還有另一種也可以作為提示,就是bottomSheet:

BottomSheet
snackBar
的區別就是,雖然彈出了提示,但是不會自動消失,需要手動下拉才會消失。
class SnackTest extends StatelessWidget{ void showBottomSheet(BuildContext context) { Scaffold.of(context).showBottomSheet((BuildContext context) { return new Container( constraints: BoxConstraints.expand(height: 100), color: Color(0xFFFF786E), alignment: Alignment.center, child: new Text( "BottomSheet is Showing!", style: TextStyle(color: Color(0xFFFFFFFF)), ), ); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Demo') ), body: Builder( // Create an inner BuildContext so that the onPressed methods // can refer to the Scaffold with Scaffold.of(). builder: (BuildContext context) { return Center( child: RaisedButton( child: Text('SHOW A SNACKBAR'), onPressed: () { showBottomSheet(context); }, ), ); }, ), ); } }
實現登入
前面講了那麼多都是為我們接下來的演示做準備的,那先來看看登入程式碼:

登入演示
class LoginPageState extends State<LoginPage> { Color colorRegular = Color(0xFFFF786E); Color colorLight = Color(0xFFFF978F); Color colorInput = Color(0x40FFFFFF); Color colorWhite = Colors.white; TextStyle defaultTextStyle = TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16); BorderRadius radius = BorderRadius.all(Radius.circular(21)); void login() { } @override Widget build(BuildContext context) { // TODO: implement build return new Scaffold( body: Container( constraints: BoxConstraints.expand(), decoration: BoxDecoration( gradient: LinearGradient( colors: [colorLight, colorRegular], begin: Alignment.topCenter, end: Alignment.bottomCenter)), child: Column( children: <Widget>[ Container ( margin: EdgeInsets.only(top: 110, bottom: 39, left: 24, right: 24), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(21)), color: colorInput), child: TextField( decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 15,vertical: 9), border: InputBorder.none, hintText: "輸入手機號", hintStyle: TextStyle(color: Colors.white, fontSize: 16), labelStyle: TextStyle(color: Colors.black, fontSize: 16)), maxLines: 1, cursorColor: colorRegular, keyboardType: TextInputType.phone, ), ), Container( margin: EdgeInsets.only(bottom: 58, left: 24, right: 24), decoration: BoxDecoration( borderRadius: radius, color: colorInput), child: TextField( decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 15,vertical: 9), border: InputBorder.none, hintText: "輸入密碼", hintStyle: TextStyle(color: Colors.white, fontSize: 16), labelStyle: TextStyle(color: Colors.black, fontSize: 16)), maxLines: 1, cursorColor: colorRegular, keyboardType: TextInputType.number, obscureText: true, ), ), Container( height: 42, width: 312, margin: EdgeInsets.only(left: 24, right: 24), decoration: BoxDecoration ( borderRadius: radius, color: colorWhite), child: RaisedButton(onPressed: login, elevation: 1, highlightElevation: 1, textColor: colorRegular, shape: RoundedRectangleBorder( borderRadius: radius ), child: new Text("立即登入", style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold), )), ), Padding( padding: EdgeInsets.only(top: 10), child: Text( "登入/註冊即代表您已同意《會員協議》", style: TextStyle(color: Colors.white, fontSize: 13), ), ), ], ), ), ); } }
在上一章就講過,如果在整個生命週期中,狀態如果改變,那麼我們就是用 StatefulWidget
來呈現,並且 StatefulWidget
的實現需要兩步:一個是需要建立繼承 StatefulWidget
的類;另一個就是建立繼承 State
的類,一般在 State
中控制整個狀態。所以此處就是如此:
class LoginPage extends StatefulWidget { @override State<StatefulWidget> createState() => LoginPageState(); } class LoginPageState extends State<LoginPage> { @override Widget build(BuildContext context) { // TODO: implement build return new Scaffold( body: Container( //省略程式碼 ... ) ); } }
並且當前登入介面是沒有工具欄的,所以去掉了 AppBar
。將所有內容直接寫在了 body
中。可以看到整個登入介面的背景是一個漸變,上面淺一點,下面深一點,所以就需要一個容器去包裹整個內容,並且這個容器可以實現背景顏色的漸變的,所以我選用了 Container
,因為它是所有容器佈局中屬性最全面的。
Container({ Key key, this.alignment,//子佈局的排列方式 this.padding,//內部填充 Color color,//背景顏色 Decoration decoration,//用於裝飾容器 this.foregroundDecoration,//前景裝飾 double width, //容器寬 double height, //容器高 BoxConstraints constraints, //約束 this.margin, //外部填充 this.transform, //對容器進行變換 this.child, })
提示:如果處於 body
下的 container
不論是否設定寬高,它將都會撲滿全屏。
那麼最外層的漸變我們就是使用 BoxDecoration
:
const BoxDecoration({ this.color, this.image, 圖片 this.border, //邊框 this.borderRadius, //圓角 this.boxShadow, //陰影 this.gradient, //漸變 this.backgroundBlendMode, //背景模式,預設BlendMode.srcOver this.shape = BoxShape.rectangle, //形狀 }) : assert(shape != null), assert( backgroundBlendMode == null || color != null || gradient != null, 'backgroundBlendMode applies to BoxDecoration\'s background color or ' 'gradient, but no color or gradient was provided.' );
提示:在對形狀的處理中,以下是可以互換的:
- CircleBorder === BoxShape.circle
- RoundedRectangleBorder == BoxShape.rectangle
所以從上可以完成我們的漸變:
decoration: BoxDecoration( gradient: LinearGradient( colors: [colorLight, colorRegular], begin: Alignment.topCenter, end: Alignment.bottomCenter) )
實現了漸變的過程,那麼就是輸入框,可以從設計上來說,這些內容都是縱向排列的,所以內容使用了佈局 Column
,用於縱向佈局,當然相對的橫向佈局 Row
。
Column({ Key key, //主軸排列方式,這裡的主軸就是縱向,實際就是縱向的佈局方式 MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, //Column在主軸(縱向)佔有的控制元件,預設儘可能大 MainAxisSize mainAxisSize = MainAxisSize.max, //交叉軸排列方式,那麼就是橫向 CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, //橫向子widget的佈局順序 TextDirection textDirection, //交叉軸的佈局對齊方向 VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline textBaseline, List<Widget> children = const <Widget>[], })
在 Column
中包含了三個 Container
,前兩個中是輸入佈局 TextField
,最後一個是 RaisedButton
。這裡回答在文章開始開始的時候提出的問題:為什麼要用 Container
去包裹 TextField
?
- 需要實現圓角 (decoration)
- 要實現間距 (marin 和 padding)
所有需要使用 Container
去完成這樣的樣式裝飾。
TextField
應該是我們比較常用的widget了:
TextField( decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 15,vertical: 9), border: InputBorder.none, hintText: "輸入手機號", hintStyle: TextStyle(color: Colors.white, fontSize: 16), labelStyle: TextStyle(color: Colors.black, fontSize: 16) ), maxLines: 1, cursorColor: colorRegular, keyboardType: TextInputType.phone, ),
這裡只是使用可 decoration
,對 TextField
裝飾,比如其中的 contentPadding
,對內容留白填補。
cursorColor
游標顏色,輸入型別 keyboardType
,這裡是手機號型別。此外還有很多的屬性,這裡就不一一贅述,可以自行到官網去檢視。
最後被 container
包裹是的 RaisedButton
:
RaisedButton( onPressed: login, elevation: 1, highlightElevation: 1, textColor: colorRegular, shape: RoundedRectangleBorder( borderRadius: radius ), child: new Text("立即登入", style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold), ))
我也修飾一下
- 到處登入介面的佈局就算完成了,然後執行之後就會出現在文章開頭的登入介面,但是當我們點選
TextField
進行輸入的時候,會發現整個佈局會被頂上去了,這是為什麼呢?。
答:這是因為 Scaffold
會填充整個可用空間,當軟鍵盤從 Scaffold
佈局中出現,那麼在這種情況下,可用空間變少 Scaffold
就會重新計算大小,這也就是為什麼 Scaffold
會將我們的佈局全部上移的根本原因,為了避免這種情況,可以使用 resizeToAvoidBottomInset
並將其
置為 false
就可以了。
- 怎麼去掉螢幕右上角的
debug
標籤?
答:將MaterialApp中的 debugShowCheckedModeBanner
置為false
class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primaryColor: Color(0xFFFF786E), primaryColorLight: Color(0xFFFF978F), accentColor: Color(0xFFFFFFFF) ), home: LoginPage(), debugShowCheckedModeBanner: false, ); } }
- 怎麼讓狀態列顯示沉浸式,不出現灰濛濛的趕腳呢?
runApp(new MyApp()); if (Platform.isAndroid) { // 以下兩行 設定android狀態列為透明的沉浸。 //寫在元件渲染之後,是為了在渲染後進行set賦值, //覆蓋狀態列,寫在渲染之前MaterialApp元件會覆蓋掉這個值。 SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent); SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); }
最後給大家推薦一本Flutter書,詳細介紹了Flutter的使用方式方法,都提供了演示案例: Flutter實戰 :

Flutter實戰