阿里 Flutter-go 项目拆解笔记(七)
Flutter-go 项目地址是:https://github.com/alibaba/flutter-go
上文 我们分析了 第三个 Tab 页面,主要分析了 组件的收藏的实现,EventBus,sqflite 的使用
这篇文章主要拆解 第四个Tab页面(关于手册)。对应的welcome_page.dart
文件的路径如下:'package:flutter_go/views /welcome_page/welcome_page.dart';
下图是整理后的collection_page.dart
文件主要的内容:
页面切换实现
老实说,没用
Flutter
做过项目,直接来阅读源码还是有点吃力的,理解错了欢迎指出。
在fourth_page.dart
的布局中,我们可以看到children
是由Page、PageReveal、PageIndicator、PageDragger
几个Widget
组成的。那么我们就来分析这几个的Widget
的实现,了解他们的作用是什么。
Page 组件分析
作用是:承载每个页面。
在Page
组件中使用了Stack
组件,用于在右上角显示go GitHub
按钮。
Page
组件的children
有Container、Positioned
,Container
用于展示 每个页面 的内容Positioned
主要显示右上角go GitHub
按钮。
每个页面 的实现分析:
Transform
可以在其子Widget
绘制时对其应用一个矩阵变换(transformation)
。
这里的实现主要就是在children
集合中添加三个Transform
组件,一个用于 顶部图片 的动画,一个用于 中间标题文字 的动画,一个用于 描述文字 的动画。
/// 这里只贴出了顶部图片的变换代码,更多代码请查看源码
Transform(
// 参数1:x 轴的移动方向,参数2:y 轴的移动方向,参数3:z 轴的移动方向
transform: Matrix4.translationValues(
0.0, 50.0 * (1.0 - percentVisible), 0.0),
child: Padding(
padding: EdgeInsets.only(top: 20.0, bottom: 10.0),
/// 顶部图片
child: Image.asset(viewModel.heroAssetPath,
width: 160.0, height: 160.0),
),
),
/// 标题的实现也是类似
/// 描述文本的实现同上
go GitHub 按钮的实现分析:
使用了
RaisedButton.icon
组件,该组件的作用是:可生成一个带有icon
的按钮。而 半圆角的矩形边框 是使用RoundedRectangleBorder
实现的
/// 回到首页按钮,Github 按钮
Widget creatButton(
BuildContext context, String txt, IconData iconName, String type) {
return RaisedButton.icon(
onPressed: () async {
if (type == 'start') {
await SpUtil.getInstance()
..putBool(SharedPreferencesKeys.showWelcome, false);
/// 跳转首页
_goHomePage(context);
} else if (type == 'goGithub') {
/// 进入 Flutter-go GitHub 首页
Application.router.navigateTo(context,
'${Routes.webViewPage}?title=${Uri.encodeComponent(txt)} Doc&&url=${Uri.encodeComponent("https://github.com/alibaba/flutter-go")}');
}
},
elevation: 10.0,
color: Colors.black26,
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(20.0))),
//如果不手动设置icon和text颜色,则默认使用foregroundColor颜色
icon: Icon(iconName, color: Colors.white, size: 14.0),
label: Text(
txt,
maxLines: 1,
style: TextStyle(
color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700),
));
}
PageReveal 组件分析
PageReveal
作用是实现页面Page
的 裁剪 效果。在实现过程中继承了CustomClipper
,CustomClipper
重写的方法getClip
根据需要露出的百分比revealPercent
对child
进行裁剪,返回了Rect
,但是在CircleRevealClipper
的外层嵌套了ClipOval
,ClipOval
是使用椭圆来剪辑其子对象的Widget
。实现源码如下:
@override
Widget build(BuildContext context) {
return ClipOval(
clipper: new CircleRevealClipper(revealPercent),
// 这里的 child 是传入的 page
child: child,
);
}
}
class CircleRevealClipper extends CustomClipper<Rect>{
// 显示的百分比
final double revealPercent;
CircleRevealClipper(
this.revealPercent
);
@override
Rect getClip(Size size) {
final epicenter = new Offset(size.width / 2, size.height * 0.9);
double theta = atan(epicenter.dy / epicenter.dx);
final distanceToCorner = epicenter.dy / sin(theta);
final radius = distanceToCorner * revealPercent;
final diameter = 2 * radius;
return new Rect.fromLTWH(epicenter.dx - radius, epicenter.dy - radius, diameter, diameter);
}
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) {
return true;
}
}
PagerIndicator 组件分析
底部的指示器实现,在源码中可以看出指示器的实现也使用了
Matrix4.translationValues
动画,指示器的实现主要是由PageBubble Widget
集合组成。而PageBubble
的实现如下:
@override
Widget build(BuildContext context) {
return new Container(
width: 55.0,
height: 65.0,
child: new Center(
child: new Container(
// 宽度在(20.0,45.0)线性插值两个数字之间变换
width: lerpDouble(20.0,45.0,viewModel.activePercent),
// 高度在(20.0,45.0)线性插值两个数字之间变换
height: lerpDouble(20.0,45.0,viewModel.activePercent),
decoration: new BoxDecoration(
shape: BoxShape.circle,
// isHollow 是否显示圆圈
// i > viewModel.activeIndex 从右向左滑动 返回 true
// i == viewModel.activeIndex && viewModel.slideDirection == SlideDirection.leftToRight) 从左向右滑动 返回 true
// bool isHollow = i > viewModel.activeIndex || (i == viewModel.activeIndex && viewModel.slideDirection == SlideDirection.leftToRight);
// isHollow表示圆点对应的页码是否大于当前页码,如果大于的话显示空心,否则显示实心
color: viewModel.isHollow
? const Color(0x88FFFFFF).withAlpha(0x88 * viewModel.activePercent.round())
: const Color(0x88FFFFFF),
border: new Border.all(
color: viewModel.isHollow
? const Color(0x88FFFFFF).withAlpha((0x88 * (1.0 - viewModel.activePercent)).round())
: Colors.transparent,
width: 3.0,
),
),
// 指示器图片
child: new Opacity(
opacity: viewModel.activePercent,
child: Image.asset(
viewModel.iconAssetPath,
color: viewModel.color,
),
),
),
),
);
}
PageDragger 组件分析
PageDragger
主要用来接收触摸事件,然后根据触摸事件来进行对应的操作。首先是在FourthPage
的构造方法中创建了一个Stream
流的事件监听。关于Stream
介绍可以查看 官方文档 或者这篇博文 Flutter响应式编程 - Stream
FourthPage
的构造方法伪代码如下:
...
slideUpdateStream = new StreamController<SlideUpdate>();
// 开始监听
slideUpdateStream.stream.listen((SlideUpdate event) {
...
}
...
在创建了slideUpdateStream
之后将其传递个PageDragger
构造方法,如下:
new PageDragger(
canDragLeftToRight: activeIndex > 0,
canDragRightToLeft: activeIndex < pages.length - 1,
slideUpdateStream: this.slideUpdateStream,
)
在构造方法中控制了左右滑动的边界,而slideUpdateStream
就是用于监听触摸事件的。那么在这里是如何去监听触摸事件的呢?
在PageDragger
的build
实现中可以看出是通过监听了水平滑动来实现对应的操作
@override
Widget build(BuildContext context) {
// 水平触摸监听
return GestureDetector(
onHorizontalDragStart: onDragStart ,
onHorizontalDragUpdate: onDragUpdate ,
onHorizontalDragEnd: onDragEnd ,
);
}
我们可以看其中的一个方法onDragUpdate
的实现:
// 正在拖拽
onDragUpdate(DragUpdateDetails details) {
if (dragStart != null) {
final newPosition = details.globalPosition;
final dx = dragStart.dx - newPosition.dx;
// 滑动方向
if (dx > 0 && widget.canDragRightToLeft) {
slideDirection = SlideDirection.rightToLeft;
} else if (dx < 0 && widget.canDragLeftToRight) {
slideDirection = SlideDirection.leftToRight;
} else {
slideDirection = SlideDirection.none;
}
// 滑动的百分比
if (slideDirection != SlideDirection.none){
slidePercent = (dx / FULL_TRANSTITION_PX).abs().clamp(0.0, 1.0);
} else {
slidePercent = 0.0;
}
// 添加 stream 数据
widget.slideUpdateStream.add(
new SlideUpdate(
UpdateType.dragging,
slideDirection,
slidePercent
));
}
}
在上面的代码可以看到slideUpdateStream
通过add
方法添加了一个SlideUpdate
对象。
所以Stream
的使用方式可以分为如下步骤:
- 创建
slideUpdateStream = new StreamController<SlideUpdate>();
- 开启监听
slideUpdateStream.stream.listen((SlideUpdate event) {}
- 添加被监听的对象
slideUpdateStream.add(new SlideUpdate())
该效果实现总结
- 创建
page
装载每个页面 - 通过
PageReveal
去裁剪页面 - 根据
PageDragger
的滑动百分比来控制显示哪个页面
点击右上角 Github 跳转
属于跳转详情页面,在下一篇跳转详情中介绍。