Flutter开发实战 高仿微信(1)首页
flutter 开发微信项目 (1)首页
源码地址:flutter_wetchat
1. 开发HomePage页
-
运行效果:
- 功能介绍
- 代码讲解
- KYLRootPage是根页面
class KYLRootPage extends StatefulWidget {
@override
State createState() {
// TODO: implement createState
return _RootPageState();
}
}
class _RootPageState extends State {
int _currentIndex = 0;
List pages = [Scaffold(
appBar: AppBar(
title: Text('微信'),
),
body: Center(
child: Text('微信主页'),
),
),
Scaffold(
appBar: AppBar(
title: Text('通讯录'),
),
body: Center(
child: Text('通讯录列表'),
),
),
Scaffold(
appBar: AppBar(
title: Text('发现'),
),
body: Center(
child: Text('发现列表'),
),
),
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(
icon: Icon(Icons.chat),
title: Text('微信'),
),
BottomNavigationBarItem(
icon: Icon(Icons.bookmark),
title: Text('通讯录'),
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
title: Text('发现'),
),
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上,它实现了Material(Android)和Cupertino(iOS)两种风格。
Scaffold是Root Widget- MaterialApp的脚手架。封装了Material Design App会用到的AppBar,Drawer,SnackBar,BottomNavigationBar等。BottomNavigationBarType有fixed 和shifting两种样式,超过3个才会有区别,一般为了体验一致,我们会用fixed type。
BottomNavigationBar是一个StatefulWidget,可以按以下步骤分析这种组件:
1,先看它持有的状态;
2,看下他的生命周期实现;
3,再仔细分析它的build方法.
- 持有状态
List _controllers = [];
List _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选中?
函数式编程一个原则是要函数尽量纯,currentIndex这个属性依赖外边传入,每次变化重新触发Render。如果自己维护,则还需要提供一个回调方法供外部调用,返回最新的currentIndex值。
- 生命周期方法
// 初始化操作,具体实现再resetState里,对上面的这些状态属性初始化操作
@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系统回调该方法。当item数量变化时直接重新初始化;当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);
// 根据BottomNavigationBarType设背景色,shifting才会有
Color backgroundColor;
switch (widget.type) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
backgroundColor = _backgroundColor;
break;
}
return Semantics( // Semantics用来实现无障碍的
container: true,
explicitChildNodes: true,
child: Stack(
children: [
Positioned.fill(
child: Material( // Casts shadow.
elevation: 8.0,
color: backgroundColor,
),
),
ConstrainedBox(
constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding),
child: Stack(
children: [
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() {
...
// 构建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(
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: [
_buildIcon(),
label,
],
),
),
Semantics(
label: indexLabel,
}
2.2 Container
2.2.1. 简介
Container在Flutter中太常见了。官方给出的简介,是一个结合了绘制(painting)、定位(positioning)以及尺寸(sizing)widget的widget。
可以得出几个信息,它是一个组合的widget,内部有绘制widget、定位widget、尺寸widget。后续看到的不少widget,都是通过一些更基础的widget组合而成的。
2.2.2. 组成
- Container的组成如下:
最里层的是child元素;
child元素首先会被padding包着;
然后添加额外的constraints限制;
最后添加margin。
- Container的绘制的过程如下:
首先会绘制transform效果;
接着绘制decoration;
然后绘制child;
最后绘制foregroundDecoration。
- Container自身尺寸的调节分两种情况:
Container在没有子节点(children)的时候,会试图去变得足够大。除非constraints是unbounded限制,在这种情况下,Container会试图去变得足够小。
带子节点的Container,会根据子节点尺寸调节自身尺寸,但是Container构造器中如果包含了width、height以及constraints,则会按照构造器中的参数来进行尺寸的调节。
2.2.3. Container的属性
key:Container唯一标识符,用于查找更新。
alignment:控制child的对齐方式,如果container或者container父节点尺寸大于child的尺寸,这个属性设置会起作用,有很多种对齐方式。
padding:decoration内部的空白区域,如果有child的话,child位于padding内部。padding与margin的不同之处在于,padding是包含在content内,而margin则是外部边界,设置点击事件的话,padding区域会响应,而margin区域不会响应。
color:用来设置container背景色,如果foregroundDecoration设置的话,可能会遮盖color效果。
decoration:绘制在child后面的装饰,设置了decoration的话,就不能设置color属性,否则会报错,此时应该在decoration中进行颜色的设置。
foregroundDecoration:绘制在child前面的装饰。
width:container的宽度,设置为double.infinity可以强制在宽度上撑满,不设置,则根据child和父节点两者一起布局。
height:container的高度,设置为double.infinity可以强制在高度上撑满。
constraints:添加到child上额外的约束条件。
margin:围绕在decoration和child之外的空白区域,不属于内容区域。
transform:设置container的变换矩阵,类型为Matrix4。
child:container中的内容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算是目前项目中,最经常用到的一个widget。在实际使用过程中,笔者在以下情况会使用到Container,当然并不是绝对的,也可以通过其他widget来实现。
- 需要设置间隔(这种情况下,如果只是单纯的间隔,也可以通过Padding来实现);
- 需要设置背景色;
- 需要设置圆角或者边框的时候(ClipRRect也可以实现圆角效果);
- 需要对齐(Align也可以实现);
- 需要设置背景图片的时候(也可以使用Stack实现)。
2.2.5. Container源码分析
decoration = decoration ?? (color != null ? new BoxDecoration(color: color) : null),
可以看出,对于颜色的设置,最后都是转换为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函数不长,绘制也是一个线性的判断的过程,一层一层的包裹着widget,去实现不同的样式。
最里层的是child,如果为空或者其他约束条件,则最里层包含的为一个LimitedBox,然后依次是Align、Padding、DecoratedBox、前景DecoratedBox、ConstrainedBox、Padding(实现margin效果)、Transform。
Container的源码本身并不复杂,复杂的是它的各种布局表现。我们谨记住一点,如果内部不设置约束,则按照父节点尽可能的扩大,如果内部有约束,则按照内部来。
2.3 Scaffold
Scaffold 实现了基本的 Material 布局。只要是在 Material 中定义了的单个界面显示的布局控件元素,都可以使用 Scaffold 来绘制。
提供展示抽屉(drawers,比如:左边栏)、通知(snack bars) 以及 底部按钮(bottom sheets)。
我们可以将 Scaffold 理解为一个布局的容器。可以在这个容器中绘制我们的用户界面。
-
Scaffold源码分析
Scaffold 主要的属性说明
- appBar:显示在界面顶部的一个 AppBar
相关连接:https://flutterchina.club/catalog/samples/ - body:当前界面所显示的主要内容
- floatingActionButton: 在 Material 中定义的一个功能按钮。
- persistentFooterButtons:固定在下方显示的按钮。https://material.google.com/components/buttons.html#buttons-persistent-footer-buttons
- drawer:侧边栏控件
- bottomNavigationBar:显示在底部的导航栏按钮栏。可以查看文档:Flutter学习之制作底部菜单导航
- backgroundColor:背景颜色
- resizeToAvoidBottomPadding: 控制界面内容 body
是否重新布局来避免底部被覆盖了,比如当键盘显示的时候,重新布局避免被键盘盖住内容。默认值为 true。
- 代码示例
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.of 使用说明
关于 Scaffold.of 函数的说明:https://docs.flutter.io/flutter/material/Scaffold/of.html
显示 snackbar 或者 bottom sheet 的时候,需要使用当前的 BuildContext 参数调用 Scaffold.of 函数来获取 ScaffoldState 对象,然后使用 ScaffoldState.showSnackBar 和 ScaffoldState.showBottomSheet 函数来显示。
来自官方源码上面的例子。使用 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 实际上是在同一个构建函数中创建时,构建函数的 BuildContext 参数不能用于查找 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!'),
));
},
),
);
},
),
);
}
按照官方的说法,可以将我们的构建函数拆分到多个 Widgets中。分别引入新的 BuildContext 来获取 Scaffold.