阅读 610

你可能不知道的「Flutter」知识点,会持续更新...

这篇文章的前世今生

这篇文章是我在草稿箱里面捞起来,说起来有点历史了,先说说我和Flutter的一些“渊源”吧。

最早接触Flutter是在2019年初,当时公司的产品App使用uni-app这个框架开发的,当时uni-app的用户还比较少,社区不够完善,再加上本身是web的性质,导致App的长列表会出现一些性能问题,由于技术团队不够全面,没有做AndroidIOS开发的同学,后来我和Leader做过一些技术调研后,将目光转移了当时移动跨端框架黑马Flutter上面,后来我们毅然决然使用FlutterApp进行重新开发。当时第一次接触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控制),RenderRepaintBoundaryRepaintBoundary 包裹的部分取出来,然后通过 .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

正在更新...

文章分类
前端