Flutter開發(fā)實戰(zhàn) 高仿微信(1)首頁
flutter 開發(fā)微信項目 (1)首頁
源碼地址:flutter_wetchat
1. 開發(fā)HomePage頁
-
運行效果:
在這里插入圖片描述 - 功能介紹
- 代碼講解
- KYLRootPage是根頁面
class KYLRootPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _RootPageState();
}
}
class _RootPageState extends State<KYLRootPage> {
int _currentIndex = 0;
List<Widget> pages = [Scaffold(
appBar: AppBar(
title: Text('微信'),
),
body: Center(
child: Text('微信主頁'),
),
),
Scaffold(
appBar: AppBar(
title: Text('通訊錄'),
),
body: Center(
child: Text('通訊錄列表'),
),
),
Scaffold(
appBar: AppBar(
title: Text('發(fā)現(xiàn)'),
),
body: Center(
child: Text('發(fā)現(xiàn)列表'),
),
),
Scaffold(
appBar: AppBar(
title: Text('我'),
),
body: Center(
child: Text('我的頁面'),
),
)
];
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
child: Scaffold(
bottomNavigationBar: BottomNavigationBar(
onTap: (int index) {
_currentIndex = index;
},
type: BottomNavigationBarType.fixed,
fixedColor: Colors.green,
currentIndex: _currentIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.chat),
title: Text('微信'),
),
BottomNavigationBarItem(
icon: Icon(Icons.bookmark),
title: Text('通訊錄'),
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
title: Text('發(fā)現(xiàn)'),
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
title: Text('我'),
),
]),
body: pages[_currentIndex],
),
);
}
}
- main.dart
import 'package:flutter/material.dart';
import 'KYLRootPage.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: KYLRootPage(),
);
}
}
2. 用到的知識點講解
2.1 BottomNavigationBar
相當于是一個自定義的Button,用來放在BottomNavigationBar上,它實現(xiàn)了Material(Android)和Cupertino(iOS)兩種風格。
Scaffold是Root Widget- MaterialApp的腳手架。封裝了Material Design App會用到的AppBar,Drawer,SnackBar,BottomNavigationBar等。BottomNavigationBarType有fixed 和shifting兩種樣式,超過3個才會有區(qū)別,一般為了體驗一致,我們會用fixed type。
BottomNavigationBar是一個StatefulWidget,可以按以下步驟分析這種組件:
1,先看它持有的狀態(tài);
2,看下他的生命周期實現(xiàn);
3,再仔細分析它的build方法.
- 持有狀態(tài)
List<AnimationController> _controllers = <AnimationController>[];
List<CurvedAnimation> _animations;
// A queue of color splashes currently being animated.
final Queue<_Circle> _circles = Queue<_Circle>();
// Last splash circle's color, and the final color of the control after
// animation is complete.
Color _backgroundColor;
前面三個屬性都和動畫相關,第四個是設背景。
這里有個疑問:BottomNavigationBar為什么沒有變量標記當前哪個item選中?
函數(shù)式編程一個原則是要函數(shù)盡量純,currentIndex這個屬性依賴外邊傳入,每次變化重新觸發(fā)Render。如果自己維護,則還需要提供一個回調(diào)方法供外部調(diào)用,返回最新的currentIndex值。
- 生命周期方法
// 初始化操作,具體實現(xiàn)再resetState里,對上面的這些狀態(tài)屬性初始化操作
@override
//initState里有個操作比較隱蔽:_controllers[widget.currentIndex].value = 1.0;
void initState() {
super.initState();
_resetState();
}
// 回收資源操作,一般用到動畫都需要的
@override
void dispose() {
for (AnimationController controller in _controllers)
controller.dispose();
for (_Circle circle in _circles)
circle.dispose();
super.dispose();
}
// 當屬性變化時Flutter系統(tǒng)回調(diào)該方法。當item數(shù)量變化時直接重新初始化;當index變化,做相應動畫。
@override
void didUpdateWidget(BottomNavigationBar oldWidget) {
super.didUpdateWidget(oldWidget);
// No animated segue if the length of the items list changes.
if (widget.items.length != oldWidget.items.length) {
_resetState();
return;
}
if (widget.currentIndex != oldWidget.currentIndex) {
switch (widget.type) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
_pushCircle(widget.currentIndex);
break;
}
_controllers[oldWidget.currentIndex].reverse();
_controllers[widget.currentIndex].forward();
}
if (_backgroundColor != widget.items[widget.currentIndex].backgroundColor)
_backgroundColor = widget.items[widget.currentIndex].backgroundColor;
}
// 下面分析
@override
Widget build(BuildContext context) {}
- 分析build方法
@override
Widget build(BuildContext context) {
// debug 檢查
assert(debugCheckHasDirectionality(context));
assert(debugCheckHasMaterialLocalizations(context));
// Labels apply up to _bottomMargin padding. Remainder is media padding.
final double additionalBottomPadding = math.max(MediaQuery.of(context).padding.bottom - _kBottomMargin, 0.0);
// 根據(jù)BottomNavigationBarType設背景色,shifting才會有
Color backgroundColor;
switch (widget.type) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
backgroundColor = _backgroundColor;
break;
}
return Semantics( // Semantics用來實現(xiàn)無障礙的
container: true,
explicitChildNodes: true,
child: Stack(
children: <Widget>[
Positioned.fill(
child: Material( // Casts shadow.
elevation: 8.0,
color: backgroundColor,
),
),
ConstrainedBox(
constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding),
child: Stack(
children: <Widget>[
Positioned.fill( // 點擊時的圓形類波紋動畫
child: CustomPaint(
painter: _RadialPainter(
circles: _circles.toList(),
textDirection: Directionality.of(context),
),
),
),
Material( // Splashes.
type: MaterialType.transparency,
child: Padding(
padding: EdgeInsets.only(bottom: additionalBottomPadding),
child: MediaQuery.removePadding(
context: context,
removeBottom: true,
// tiles就是_BottomNavigationTile,里面放BottomNavigationBarItem
child: _createContainer(_createTiles()),
)))]))]));
}}
- _BottomNavigationTile看下
Widget _buildIcon() {
...
// 構(gòu)建Icon
}
Widget _buildFixedLabel() {
....
// 騷操作,用矩陣來給文字作動畫,更平滑
// The font size should grow here when active, but because of the way
// font rendering works, it doesn't grow smoothly if we just animate
// the font size, so we use a transform instead.
child: Transform(
transform: Matrix4.diagonal3(
Vector3.all(
Tween<double>(
begin: _kInactiveFontSize / _kActiveFontSize,
end: 1.0,
).evaluate(animation),
),
),
alignment: Alignment.bottomCenter,
child: item.title,
),
),
),
);
}
Widget _buildShiftingLabel() {
return Align(
.....
// shifting的label是fade動畫,只有當前選中的才會顯示label
child: FadeTransition(
alwaysIncludeSemantics: true,
opacity: animation,
child: DefaultTextStyle.merge(
style: const TextStyle(
fontSize: _kActiveFontSize,
color: Colors.white,
),
child: item.title,
),
),
),
);
}
@override
Widget build(BuildContext context) {
int size;
Widget label;
// 生成不同的label
switch (type) {
case BottomNavigationBarType.fixed:
size = 1;
label = _buildFixedLabel();
break;
case BottomNavigationBarType.shifting:
size = (flex * 1000.0).round();
label = _buildShiftingLabel();
break;
}
return Expanded(
....
children: <Widget>[
_buildIcon(),
label,
],
),
),
Semantics(
label: indexLabel,
}
2.2 Container
2.2.1. 簡介
Container在Flutter中太常見了。官方給出的簡介,是一個結(jié)合了繪制(painting)、定位(positioning)以及尺寸(sizing)widget的widget。
可以得出幾個信息,它是一個組合的widget,內(nèi)部有繪制widget、定位widget、尺寸widget。后續(xù)看到的不少widget,都是通過一些更基礎的widget組合而成的。
2.2.2. 組成
- Container的組成如下:
最里層的是child元素;
child元素首先會被padding包著;
然后添加額外的constraints限制;
最后添加margin。
- Container的繪制的過程如下:
首先會繪制transform效果;
接著繪制decoration;
然后繪制child;
最后繪制foregroundDecoration。
- Container自身尺寸的調(diào)節(jié)分兩種情況:
Container在沒有子節(jié)點(children)的時候,會試圖去變得足夠大。除非constraints是unbounded限制,在這種情況下,Container會試圖去變得足夠小。
帶子節(jié)點的Container,會根據(jù)子節(jié)點尺寸調(diào)節(jié)自身尺寸,但是Container構(gòu)造器中如果包含了width、height以及constraints,則會按照構(gòu)造器中的參數(shù)來進行尺寸的調(diào)節(jié)。
2.2.3. Container的屬性
key:Container唯一標識符,用于查找更新。
alignment:控制child的對齊方式,如果container或者container父節(jié)點尺寸大于child的尺寸,這個屬性設置會起作用,有很多種對齊方式。
padding:decoration內(nèi)部的空白區(qū)域,如果有child的話,child位于padding內(nèi)部。padding與margin的不同之處在于,padding是包含在content內(nèi),而margin則是外部邊界,設置點擊事件的話,padding區(qū)域會響應,而margin區(qū)域不會響應。
color:用來設置container背景色,如果foregroundDecoration設置的話,可能會遮蓋color效果。
decoration:繪制在child后面的裝飾,設置了decoration的話,就不能設置color屬性,否則會報錯,此時應該在decoration中進行顏色的設置。
foregroundDecoration:繪制在child前面的裝飾。
width:container的寬度,設置為double.infinity可以強制在寬度上撐滿,不設置,則根據(jù)child和父節(jié)點兩者一起布局。
height:container的高度,設置為double.infinity可以強制在高度上撐滿。
constraints:添加到child上額外的約束條件。
margin:圍繞在decoration和child之外的空白區(qū)域,不屬于內(nèi)容區(qū)域。
transform:設置container的變換矩陣,類型為Matrix4。
child:container中的內(nèi)容widget。
實例:
new Container(
constraints: new BoxConstraints.expand(
height:Theme.of(context).textTheme.display1.fontSize * 1.1 + 200.0,
),
decoration: new BoxDecoration(
border: new Border.all(width: 2.0, color: Colors.red),
color: Colors.grey,
borderRadius: new BorderRadius.all(new Radius.circular(20.0)),
image: new DecorationImage(
image: new NetworkImage('http://h.hiphotos.baidu.com/zhidao/wh%3D450%2C600/sign=0d023672312ac65c67506e77cec29e27/9f2f070828381f30dea167bbad014c086e06f06c.jpg'),
centerSlice: new Rect.fromLTRB(270.0, 180.0, 1360.0, 730.0),
),
),
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: new Text('Hello World',
style: Theme.of(context).textTheme.display1.copyWith(color: Colors.black)),
transform: new Matrix4.rotationZ(0.3),
)
2.2.4. Container使用
Container算是目前項目中,最經(jīng)常用到的一個widget。在實際使用過程中,筆者在以下情況會使用到Container,當然并不是絕對的,也可以通過其他widget來實現(xiàn)。
- 需要設置間隔(這種情況下,如果只是單純的間隔,也可以通過Padding來實現(xiàn));
- 需要設置背景色;
- 需要設置圓角或者邊框的時候(ClipRRect也可以實現(xiàn)圓角效果);
- 需要對齊(Align也可以實現(xiàn));
- 需要設置背景圖片的時候(也可以使用Stack實現(xiàn))。
2.2.5. Container源碼分析
decoration = decoration ?? (color != null ? new BoxDecoration(color: color) : null),
可以看出,對于顏色的設置,最后都是轉(zhuǎn)換為decoration來進行繪制的。如果同時包含decoration和color兩種屬性,則會報錯。
@override
Widget build(BuildContext context) {
Widget current = child;
if (child == null && (constraints == null || !constraints.isTight)) {
current = new LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: new ConstrainedBox(constraints: const BoxConstraints.expand())
);
}
if (alignment != null)
current = new Align(alignment: alignment, child: current);
final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
current = new Padding(padding: effectivePadding, child: current);
if (decoration != null)
current = new DecoratedBox(decoration: decoration, child: current);
if (foregroundDecoration != null) {
current = new DecoratedBox(
decoration: foregroundDecoration,
position: DecorationPosition.foreground,
child: current
);
}
if (constraints != null)
current = new ConstrainedBox(constraints: constraints, child: current);
if (margin != null)
current = new Padding(padding: margin, child: current);
if (transform != null)
current = new Transform(transform: transform, child: current);
return current;
}
Container的build函數(shù)不長,繪制也是一個線性的判斷的過程,一層一層的包裹著widget,去實現(xiàn)不同的樣式。
最里層的是child,如果為空或者其他約束條件,則最里層包含的為一個LimitedBox,然后依次是Align、Padding、DecoratedBox、前景DecoratedBox、ConstrainedBox、Padding(實現(xiàn)margin效果)、Transform。
Container的源碼本身并不復雜,復雜的是它的各種布局表現(xiàn)。我們謹記住一點,如果內(nèi)部不設置約束,則按照父節(jié)點盡可能的擴大,如果內(nèi)部有約束,則按照內(nèi)部來。
2.3 Scaffold
Scaffold 實現(xiàn)了基本的 Material 布局。只要是在 Material 中定義了的單個界面顯示的布局控件元素,都可以使用 Scaffold 來繪制。
提供展示抽屜(drawers,比如:左邊欄)、通知(snack bars) 以及 底部按鈕(bottom sheets)。
我們可以將 Scaffold 理解為一個布局的容器??梢栽谶@個容器中繪制我們的用戶界面。
-
Scaffold源碼分析
在這里插入圖片描述 Scaffold 主要的屬性說明
- appBar:顯示在界面頂部的一個 AppBar
相關連接:https://flutterchina.club/catalog/samples/ - body:當前界面所顯示的主要內(nèi)容
- floatingActionButton: 在 Material 中定義的一個功能按鈕。
- persistentFooterButtons:固定在下方顯示的按鈕。https://material.google.com/components/buttons.html#buttons-persistent-footer-buttons
- drawer:側(cè)邊欄控件
- bottomNavigationBar:顯示在底部的導航欄按鈕欄??梢圆榭次臋n:Flutter學習之制作底部菜單導航
- backgroundColor:背景顏色
- resizeToAvoidBottomPadding: 控制界面內(nèi)容 body
是否重新布局來避免底部被覆蓋了,比如當鍵盤顯示的時候,重新布局避免被鍵盤蓋住內(nèi)容。默認值為 true。
- 代碼示例
class Scaffold extends StatefulWidget {
/// Creates a visual scaffold for material design widgets.
const Scaffold({
Key key,
this.appBar, //橫向水平布局,通常顯示在頂部(*)
this.body, // 內(nèi)容(*)
this.floatingActionButton, //懸浮按鈕,就是上圖右下角按鈕(*)
this.floatingActionButtonLocation, //懸浮按鈕位置
//懸浮按鈕在[floatingActionButtonLocation]出現(xiàn)/消失動畫
this.floatingActionButtonAnimator,
//在底部呈現(xiàn)一組button,顯示于[bottomNavigationBar]之上,[body]之下
this.persistentFooterButtons,
//一個垂直面板,顯示于左側(cè),初始處于隱藏狀態(tài)(*)
this.drawer,
this.endDrawer,
//出現(xiàn)于底部的一系列水平按鈕(*)
this.bottomNavigationBar,
//底部持久化提示框
this.bottomSheet,
//內(nèi)容背景顏色
this.backgroundColor,
//棄用,使用[resizeToAvoidBottomInset]
this.resizeToAvoidBottomPadding,
//重新計算布局空間大小
this.resizeToAvoidBottomInset,
//是否顯示到底部,默認為true將顯示到頂部狀態(tài)欄
this.primary = true,
//
this.drawerDragStartBehavior = DragStartBehavior.down,
}) : assert(primary != null),
assert(drawerDragStartBehavior != null),
super(key: key);
- Scaffold.of 使用說明
關于 Scaffold.of 函數(shù)的說明:https://docs.flutter.io/flutter/material/Scaffold/of.html
顯示 snackbar 或者 bottom sheet 的時候,需要使用當前的 BuildContext 參數(shù)調(diào)用 Scaffold.of 函數(shù)來獲取 ScaffoldState 對象,然后使用 ScaffoldState.showSnackBar 和 ScaffoldState.showBottomSheet 函數(shù)來顯示。
來自官方源碼上面的例子。使用 SnackBar 的寫法。
@override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text('SHOW A SNACKBAR'),
onPressed: () {
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text('Hello!'),
));
},
);
}
當 Scaffold 實際上是在同一個構(gòu)建函數(shù)中創(chuàng)建時,構(gòu)建函數(shù)的 BuildContext 參數(shù)不能用于查找 Scaffold(因為它位于返回的小部件的“上方”)。 因為在源碼中 使用的是 return new Scaffold(app:xxxx),在這種情況下面,通過在 Scaffold 中使用一個 Builder 來提供一個新的 BuildContext:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Demo')
),
body: new Builder(
// Create an inner BuildContext so that the onPressed methods
// can refer to the Scaffold with Scaffold.of().
builder: (BuildContext context) {
return new Center(
child: new RaisedButton(
child: new Text('SHOW A SNACKBAR'),
onPressed: () {
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text('Hello!'),
));
},
),
);
},
),
);
}
按照官方的說法,可以將我們的構(gòu)建函數(shù)拆分到多個 Widgets中。分別引入新的 BuildContext 來獲取 Scaffold.