这篇文章的前世今生
这篇文章是我在草稿箱里面捞起来,说起来有点历史了,先说说我和Flutter的一些“渊源”吧。
最早接触Flutter
是在2019年初,当时公司的产品App使用uni-app
这个框架开发的,当时uni-app
的用户还比较少,社区不够完善,再加上本身是web的性质,导致App的长列表会出现一些性能问题,由于技术团队不够全面,没有做Android
和IOS
开发的同学,后来我和Leader做过一些技术调研后,将目光转移了当时移动跨端框架黑马Flutter
上面,后来我们毅然决然使用Flutter
对App进行重新开发。当时第一次接触Flutter
很不习惯,奈何临危受命,没有退路可言,从写第一行dart
代码开始到完成整个App的开发我和Leader用了将近2个星期。前面两天熟悉新的代码风格和开发模式,慢慢上手到一路过关斩将,时常开发到深夜,用两周的时间快速的学会一门新框架以及实战,这两周也是我那一年感觉最充实的时光,实践是检验真理的唯一途径,也是最有效的途径(致敬那些奋斗的岁月)。
重获新生的App上线后,性能和用户体验上都有很大的提升。
这篇文章来由的前因?
后来陆陆续续对App进行了方方面面的优化,同时用Flutter
开发了一些新的App,对Flutter
的理解日渐加深。直到后来换了新公司,基本上没怎么用到Flutter
,于是有点日渐荒废的感觉,有一天突然想起了它,老脸一红,说的有一种想起前女友的感觉,于是心血来潮写了一个Music App。期间有一些突然想记录下来的想法,就有了这篇文章,没错这是一篇思想杂籍,没有系统性,想到哪说到哪。因此之前也一直没有发出来,现在在想想知识就是用来分享的,万一它解开了正在困扰某位码友的难题呢,能让码友有一点收获可能就是这篇文章最好的归宿了,至少重见天日了。
那后果呢?
这篇文章可能有些地方描绘的会比较简单,作者会尽量去温故并持续更新优化这篇杂籍!!!
有什么错误的地方欢迎大佬指证,真的好久没写Flutter了!
正文开始!
关于一些动效方面的心得
animation的动画值为0-1
绝大多数的效果(防止打脸),过程都是从0-1,这个有点抽象,大多数武当弟子没明白太极生两仪,两仪生四象,四象生八卦(八卦怎么演变我也不知道了)…的真正含义,那张三丰等人就能悟出其中精髓,成就大师,指日可待。扯远了…
打个比方:
- 0 水平移动到 600 是 0-1? // 0*600 – 1*600 有问题嘛?
少侠?
get?
点到为止…
1.初始化函数
class Demo extends StatelessWidget{
Demo(){
doSomething()
}
}
class Demo extends StatefulWidget{
@override
_DemoState createState = > _DemoState();
}
class _DemoState extends extends State<Demo>{
_DemoState(){
print('_DemoState先输出');
doSomething();
}
@override
void initState(){
print('initState 先输入');
super.initState();
}
// 结果
// _DemoState先输出
// initState 先输入
}
复制代码
2.尽量使用Model传参
- 使用model传参能够在程序编译的时候就发现程序中关于参数传错的错误。
class DemoModel{
final Color color;
final String title;
DemoModel(this.color,this.title);
}
class Demo extends StatelessWidget{
final DemoModel model;
Demo({this.model})
}
复制代码
3.ClipOval切割组件做出不同层的图片同一层显示的效果,来看段代码
// 使用Stack将图片放入层级
Stack:[
Picture1(),
Picture2()
]
// 这样只能看到Picture2 ,我们将代码改造一下...
Stack:[
Picture1(),
ClipOval(
clipper:CircleRevealClipper(参数),
child:Picture2()
)
]
class CircleRevealClipper extends CustomClipper<Rect>{
final double param;
CircleRevealClipper(this.param);
@override
Rect getClip(Size size) {
// 这里绘制切割形状(路线)
}
@override
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) {
// TODO: implement shouldReclip
return true;
}
}
// 这样Picture1 和 Picture2就可以同时显示了出来了。
复制代码
4.通常固定值用全英文大写变量接受
// 阅读性强
final BUBBLE_WIDTH = 55.0;
复制代码
5.使用Transform做简单动画
var translation = 数值;
Transform(
transform: Matrix4.translationValues(translation, 0.0, 0.0),
child: Widget()
)
配合AnimationController
completionAnimationController = new AnimationController(
duration: duration,
vsync: vsync,
)
..addListener(() {
print(completionAnimationController.value);
// completionAnimationController.value值为 0-1
}
复制代码
6.关于枚举值的使用方法
enum SlideDirection { leftToRight, rightToLeft, none }
SlideDirection direction = SlideDirection.leftToright;
复制代码
7.从透明到显示的过程 可以使用Opacity组件,也是0-1过程。
8.最顶层手势事件的实现其中一种方式
Stack:[
page(),
GestureDetector(
onHorizontalDragStart: onDragStart, // 触碰屏幕
onHorizontalDragUpdate: onDragUpdate, // 触碰屏幕并滑动
onHorizontalDragEnd: onDragEnd, // 离开屏幕
);
]
复制代码
9.关于类基础使用
class Demo{
final double value;
Demo({
this.value
}){
// 初始化函数
print('执行');
}
fn1(){
// 方法体
}
fn2(){
//方法体
}
}
//使用
Demo a = new Demo();
a.fn1();
a.fn2();
复制代码
10.关于StreamController的使用
class Demo{
final double value;
Demo(this.value);
}
class Example extends StatefulWidget{
@override
_ExampleState createState => _ExampleState();
}
class _ExampleState extends State<Example>{
StreamController<Demo> slideUpdateStream;
_ExampleState(){
slideUpdateStream = new StreamController<Demo>();
// 设置监听
slideUpdateStream.stream.listen((Demo d){
// doSomething...
})
}
}
//触发
class Trigger extends StatelessWidget{
StreamController<Demo> demo;
Trigger(){
// 触发
demo.add(new Demo())
}
}
复制代码
11.关于SizeBox的其他小用法
元素之间的间距(部分情况下灵活试用)可以使用SizeBox实现而避免再次嵌套。
- 两个元素之间有20.0的间距
Row(
children:[
A(),
SizeBox(
width:20.0
),
B()
...
]
)
复制代码
12. 关于Position.fill()在Stack中的使用
Position.fill()会撑满Stack
Stack(
children:[
Text("小子,想挡住我?"),
Position.fill(
// 这里要注意,必须要有child,Position.fll才会生效,才会当爹!!!
child:Text('我小,但我爹遮天蔽日')
)
]
)
复制代码
13.PageView 一个非常好用的滚动视图列表(简单版的轮播)
class Example extends StatefulWidget {
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example>{
var currentPage = 1.0;
PageController controller = PageController(initialPage: 0);
@override
void initState() {
// TODO: implement initState
controller.addListener(() {
// 值得注意的是,这里controller.page的值是一个我称之为过程值,而不是结果值,比如 从0页-1页,是从0.000-0.99-1.0
// 而且当滑动没有滑动到下一页的时候,值会回弹(0.1-0.3-0.45-0.2-0.0)来利用这个特性 可以用来实现一个回弹的动画效果。
setState(() {
currentPage = controller.page;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 其他更多用法请自行百度
body:PageView.builder(
itemCount: images.length,
controller: controller,
reverse: true,
itemBuilder: (context, index) {
return Contain(
...
)
}
)
)
}
}
复制代码
14.FittedBox的使用
FittedBox会在自己的尺寸范围内缩放并且调整child位置,使得child适合其尺寸。FittedBox的fit属性有点像
写CSS的时候的background-size属性(感觉将子元素当做了图片进行布局)。
Container(
color: Colors.amberAccent,
width: 300.0,
height: 300.0,
child: new FittedBox(
fit: BoxFit.contain,
alignment: Alignment.topLeft,
child: new Container(
color: Colors.red,
child: new Text("FittedBox"),
),
),
),
复制代码
15.AspectRatio的使用
AspectRatio首先会在布局限制条件允许的范围内尽可能的扩展,widget的高度是由宽度和比率决定的,高宽值大致如一下代码计算
if (width有限制) {
height = width / _aspectRatio;
width = constraints.maxWidth;
} else if(height有限制) {
height = constraints.maxHeight;
width = height * _aspectRatio;
}else{
height = constraints.maxHeight;
width = constraints.maxWidth;
}
复制代码
注意:aspectRatio属性不能为空
一般AspectRatio使用在需要有高宽固定比例的视图上。
16.ConstrainedBox的使用
可以简单理解为一个限制到高宽的容器,比如设置了最大高度,最小高度,最大宽度,最小宽度。 看代码:
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 220.0,
minHeight: 100.0,
maxWidth: 250.0,
maxHeight: 150.0,
),
child: new Container(
width: 300.0,
height: 200.0,
color: Colors.red,
),
);
复制代码
最终显示的子元素高200,宽250,just so。
17.RepaintBoundary组件可以实现截图效果
用 RepaintBoundary 包裹想要截取的部分(通过key控制),RenderRepaintBoundary 将 RepaintBoundary 包裹的部分取出来,然后通过 .toImage() 方法转化为 ui.Image 对象,然后使用 .toByteData() 将 image 转化为 byteData 。最后通过 File(‘路径’).writeAsBytes(byteData) 存储为文件对象。看代码:
GlobalKey rootWidgetKey = GlobalKey();
List<Uint8List> images = List();
_interceptPng() async {
try {
RenderRepaintBoundary boundary = rootWidgetKey.currentContext.findRenderObject();
var image = await boundary.toImage(pixelRatio: 3.0);
ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
images = [pngBytes];
setState((){})
// 或者将图片存起来
// File('路径').writeAsBytes(pngBytes);
} catch (e) {
print(e);
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('RepaintBoundary Demo'),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.camera),
tooltip: '截图',
onPressed: () async {
await this._interceptPng();
},
),
body: Column(
children: <Widget>[
RepaintBoundary(
key: rootWidgetKey,
child: Container(
height:100.0,
width:30.0,
color:Colors.green
)
),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
return Image.memory(
images[index],
width: 100.0,
height: 200.0,
);
},
itemCount: images.length,
scrollDirection: Axis.vertical,
),
)
],
),
),
)
}
复制代码
18.CircleAvatar组件的使用
看英文单词的意思也能看出来用这个组件可以实现圆形头像的效果。但是要设置 backgroundImage 或者 backgroundColor 属性才能实现圆形效果, child 属性可以理解为在头像上层设置widgets(头像挂件)。看代码:
CircleAvatar(
radius: 40.0,
backgroundColor: Colors.red,
child: Text("test"),
)
复制代码
19.IndexedStack组件的使用
IndexedStack继承自Stack,作用是显示第index个child组件,其他child组件是不可见的,但所有的组件的状态都会被保持(还可以通过OffStage组件去做状态保持,但他们的缺点是开销比较大,在页面加载初始化的时候所有子组件都会被实例化)。
IndexedStack(
index: currentIndex, // 显示的index
children: widgetList, // 子组件列表
)
复制代码
20.AutomaticKeepAliveClientMixin类
这个类的作用在于保持页面状态,我们可以让页面继承这个类并重写wantKeepAlive为true即可,代码如下:
class TestPage extends StatefulWidget {
@override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> with AutomaticKeepAliveClientMixin {
// 重写属性一般可以通过错误修复提示插件直接生成重写属性,修改值对应值即可
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
...
}
复制代码
21.InheritedWidget
正在更新…