Flutter 基础进阶

629 阅读11分钟

该文章是王叔不秃flutter系列视频的学习笔记

网络测试接口 github: https://api.github.com/events get

snh48: h5.48.cn/resource/js… get

照片:www.snh48.com/images/memb…

快捷键

  • 1、快速创建widget:在dart文件中输入stf或stl出现提示后按回车即可
  • 2、快速修复:option +回车
  • 3、自动生成构造函数:选中 final参数,快捷键:option +回车
  • 4、添加父组件、变为子组件、删除子组件:option+回车
  • 5、万能的搜索:双击shift
  • 6、查看最近打开的文件:command + E
  • 7、查看当前类结构:command + fn + f12
  • 8、查看源码:将光标放到要查看源码的类名或方法名上,长按command然后的点击
  • 9、查看类的子类:选中要查看的类,然后: command +B 或 option + command + B
  • 10、将代码更新到模拟器上:选中模拟器然后 command +R
  • 11、导入类的快捷键:将光标放在要导入类的上面,然后按 option + enter
  • 12、前进后退:当跟踪代码的时候,经常跳转到其他类,后退快捷键:option+command+方向左键,前进快捷键:option+command+方向右键
  • 13、全局搜索:command + shift + F
  • 14、全局替换:command + shift + R
  • 15、查找引用:option + shift + F7
  • 16、重命名:fn+shift+f6

注:以上快捷键是在Android Studio 的macOS的keymap下,如果是Windows系统,将command 换成Ctrl,option换成Alt即可。

注意:该部分 copy于# Android Studio开发flutter快捷键

空安全 Null-safety

为什么要做空安全

运行时的错误尽量提前到编译时的错误。

  • 优点: 速度快、效率高、更安全(不是绝对的安全)

空安全的类型

  • 空安全类型汇总 截屏2022-05-11 上午10.22.31.png
  • Null !== 0

如何处理空安全

int addOne(int x) {
  return x + 1;
}
  • 只有值不为空时才调用
_nullSafety0() {
  int? i;
  //只有值不为空时才调用
  if (i != null) {
    addOne(i);
  }
}
  • 调用时传入一个预备值
_nullSafety1() {
  int? i;
  //调用时传入一个预备值
  addOne(i ?? 0);
}
  • 用感叹号强制解包,保证值不为空。这种方法尽量避免
_nullSafety2() {
  int? i;
  // 用感叹号强制解包,保证值不为空
  addOne(i!);
}

as 关键字

  • as 可以将某个值向下解包成某种类型,但是如果这个值如果不能被转换成目标类型时,就会崩溃
main {
   int? i;
   addOne(i as int);
}

late 关键字

  • 声明一个非空的变量,但是没有马上给它赋值。需要在之后某个时机才会赋值,再赋值前使用就会发生错误
late int id;
late String name;
late Color color;
  • 延后执行:虽然在声明的时候已经赋值,但是其实并没有,只有真正使用到时才会赋值.类似于swift中lazy关键字
late int id = 100;
  • dart语言中,所有的Class以外的全局变量都会默认加上 late关键字
//这个name会默认被dart编译器加上late关键字
String name = 'lee'; // ==> late String name = 'lee';
class Demo {

}

健全的空安全

健全的空安全:每一个模块都是空安全 --> 那就是健全的空安全

布局

布局流

  • 向下传递约束
  • 向上传递尺寸 截屏2022-05-09 下午3.33.17.png

获取和设置布局约束

获取约束 -LayoutBuilder

body: Container(
    width: 300,
    height: 300,
    //使用 LayoutBuilder,就可以获得上级对下级的 constraints
    child: Center(
      child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
        print('constraints: $constraints');
        return FlutterLogo(size: 200,);
      },),
    )
)

打印结果 flutter: constraints: BoxConstraints(0.0<=w<=300.0, 0.0<=h<=300.0) 说明FlutterLogo 大小只要在满足0.0<=w<=300.0, 0.0<=h<=300.0,都是可以的正常显示的

松约束 - 紧约束
  • loosen-constraints:松约束,最小约束 == 0,eg:(0.0<=w<=120.0, 0.0<=h<=120.0), BoxConstraints源码=>
BoxConstraints.loose(Size size)
  : minWidth = 0.0,
    maxWidth = size.width,
    minHeight = 0.0,
    maxHeight = size.height;
  • tightly-constraints:紧约束,最小约束和最大约束相等,eg:(60.0<=w<=60.0, 120.0<=h<=120.0),BoxConstraints源码=>
BoxConstraints.tight(Size size)
  : minWidth = size.width,
    maxWidth = size.width,
    minHeight = size.height,
    maxHeight = size.height;
  • 将尺寸约束变为一个松约束 --> loosen()
child: ConstrainedBox(
  constraints: const BoxConstraints(
    minHeight: 60,
    maxHeight: 120,
    minWidth: 60,
    maxWidth: 120
  ).loosen(),//loosen(),
  child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
    print('constraints: $constraints');
    return FlutterLogo(size: 200,);
  },),
),
  • 不加loosen() 约束:constraints: BoxConstraints(60.0<=w<=120.0, 60.0<=h<=120.0)
  • 加loosen() 约束:BoxConstraints(0.0<=w<=120.0, 0.0<=h<=120.0) 可以看到变为了minHeight和minWidth是0。--> 在BoxConstraints术语中如果minHeight == minWidth == 0时,就是松约束,其实就是约束变为了类似 Width == Height == 120的 Center()
  • 松约束 和 紧约束不是对立的。eg: (0.0<=w<=0.0, 0.0<=h<=0.0),
Padding 会 欺上瞒下

padding会修改子部件的约束

body: Container(
    width: 300,
    height: 300,
    //使用 LayoutBuilder,就可以获得上级对下级的 constraints
    child: Padding(
      padding: const EdgeInsets.all(10.0),
      child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
        print('constraints: $constraints');
        return FlutterLogo(size: 200,);
      },),
    )
)

==> flutter: constraints: BoxConstraints(w=280.0, h=280.0),而并不是(300,300)

Flex

  • 内部子组件分为弹性的和非弹性的
  • 计算高度时先计算非弹性的,再计算弹性的

Stack

  • 内部子组件分为有位置的和没有位置的
  • stack布局先计算有没有位置的,再计算有位置的
  • stack的大小 == 所有没有位置组件中最大组件的大小
  • 超出stack大小的控件,超出的部分不能响应点击事件
  • Positioned组件只能在Stack中用,不属于 StatefulWidget、StatelessWidget、RenderObjectWidget任何一种,属于ParentDataWidget这类的组件只能给子组件提供数据

Container

  • 在Container 中有子部件的时候,会紧紧包裹子部件
  • 在Container中没有子部件的时候,Container 大小 == 所给约束的最大
  • Container是个什么 ===> 集合了很多个组件,以属性对外提供组件能力。避免组件嵌套太多层。eg:图中 padding ==> Padding、alignment ==> Align... 截屏2022-05-09 下午5.50.08.png Container源码。可以看到其实当设置padding 就是 会自动加上Padding组件
Widget build(BuildContext context) {
  Widget? current = child;

  if (child == null && (constraints == null || !constraints!.isTight)) {
    current = LimitedBox(
      maxWidth: 0.0,
      maxHeight: 0.0,
      child: ConstrainedBox(constraints: const BoxConstraints.expand()),
    );
  } else if (alignment != null) {
    current = Align(alignment: alignment!, child: current);
  }

  final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
  if (effectivePadding != null)//可以看到其实当设置padding 就是 会自动加上Padding组件
    current = Padding(padding: effectivePadding, child: current);

  if (color != null)
    current = ColoredBox(color: color!, child: current);

  if (clipBehavior != Clip.none) {
    assert(decoration != null);
    current = ClipPath(
      clipper: _DecorationClipper(
        textDirection: Directionality.maybeOf(context),
        decoration: decoration!,
      ),
      clipBehavior: clipBehavior,
      child: current,
    );
  }

Container大小

  • 没有child就越大越好,除非约束无边界
  • 有child就匹配尺寸,除非Container要对齐

CustomMultiChildLayout

  • 可以自定义内部子部件的尺寸
  • 需要遵循代理 截屏2022-05-09 下午6.20.09.png 截屏2022-05-09 下午6.20.32.png

SingleChildRenderObjectWidget

自定义绘制组件 手动实现一个 RenderObject --> Flutter 教程 Layout-7 自己动手写个RenderObject

滚动

ListView

重用、缓冲数量

  • 使用ListView.build()去构建,其缓冲区的大小大概是上下各多屏幕大小的1/3;eg: 当屏幕能显示9个cell时,上下未显示的cell各有3个; 而当屏幕能显示90个cell时,上下未显示的cell各有30个;
  • cacheExtent: 控制缓冲数量
  • itemExtent: 强制每个cell的高度,用于大幅加载
  • padding: 主轴方向一直生效,而交叉轴方向只有滑到最顶部和最底部才生效,和Padding控件略有不同
  • physics: 滚动状态,iOSor安卓
ListView.builder(
    itemCount: 100,
    cacheExtent: 0, //缓冲数量
    itemExtent: 60, //紧约束,控制每个item的高度,这里设置了,itemBuilder设置的高度就不会生效
    padding: const EdgeInsets.all(20.0),
    itemBuilder: (BuildContext context, int index) {
      print('$index'); //加个打印就能判断出来
      return Text('dddd');
}),

分割线

使用 ListView.separated -> separatorBuilder

  body: ListView.separated(
      itemCount: 10,
      cacheExtent: 0, //缓冲数量
      separatorBuilder: (BuildContext context, int index) {
        print('Divider $index');
        return Divider(
          thickness: 1,
          color: Colors.red,
        );
      },
      itemBuilder: (BuildContext context, int index) {
        print('Text $index');
        return Text('dddd');
      }),
);

滚动条 - ScrollBar

  • 外面加上ScrollBar就可以了,如果要使用iOS风格的就需要用到;
  • 如果要使用controller,那就要和ListView使用同一个controller
Scrollbar(
  controller: _scrollController,
  child: ListView.builder(
      controller: _scrollController,
      itemBuilder: (BuildContext context, int index) {
        print('Text $index');
        return Container(
          height: 40,
          color: Colors.red,
          child: Text('dddd'),
        );
      }),
),
  • 原理:(滑动)事件向上传递,通过NotificationListener监听 截屏2022-05-10 下午2.30.41.png

下拉刷新 - RefreshIndicator

RefreshIndicator(
  onRefresh: () {
    return Future.delayed(Duration(seconds: 2));
  },
  child: ListView.builder(
      itemCount: 50,
      itemBuilder: (BuildContext context, int index) {
        print('Text $index');
        return Container(
          height: 40,
          color: Colors.red,
          child: Text('dddd'),
        );
      }),
),

滑动删除 - Dismissible

ListView.builder(
    itemCount: 50,
    itemBuilder: (BuildContext context, int index) {
      print('Text $index');
      return Dismissible(
        key: UniqueKey(),
        //左滑
        background: Container( 
          alignment: Alignment.centerLeft,
          color: Colors.green,
          child: Icon(Icons.phone),
        ),
        //右滑
        secondaryBackground: Container(
          alignment: Alignment.centerRight,
          color: Colors.blue,
          child: Icon(Icons.message),
        ),
        //滑动 是否删除
        confirmDismiss: (direction) async {
          if (direction == DismissDirection.endToStart) {
            await  Future.delayed(Duration(seconds: 1));
            return true;
          } else {
            return false;
          }
        },
        onDismissed: (direction) {
          print('$direction');
          if (direction == DismissDirection.endToStart) {
            // 删除源数据操作...
            listDatas.removeWhere((e) => e.id == '9527');
          }
        },
        child: Container(
          height: 40,
          color: Colors.red[index % 9 * 100]
        ),
      );
    }),

GridView - 二维网格

  • gridDelegate: 如何显示网格
  body: GridView.builder(
      itemCount: 50,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 4,//主轴方向有几个
        childAspectRatio: 16/9,//每个item宽高比例,默认11
      ),
      itemBuilder: (BuildContext context, int index) {
        print('Text $index');
        return Container(
          height: 40,
          color: Colors.red[index % 8 * 100]
        );
      }),
);
  • GridView.count
body: GridView.count(
  crossAxisCount: 5,
  children: const [
    Icon(Icons.add),
    Icon(Icons.add),
    Icon(Icons.add),
    Icon(Icons.add),
  ],
),

大小如何计算:

  • 1.width: 先计算主轴的宽度/个数
  • 2.height: 根据childAspectRatio(默认1:1) 由width计算height simulator_screenshot_EBC3E801-2ED8-423E-87DD-F3E06FBEE684.png
  • 横竖屏也会按比列来

ListWheelScrollView - 转轮类似于iOS中的UIPickView

ListWheelScrollView(
  //高度
  itemExtent: 50,
  //偏离中心显示透明度
  overAndUnderCenterOpacity: 0.5,
  // offAxisFraction: 1.5,
  useMagnifier: true,
  //滚动到某个item固定效果
  physics: FixedExtentScrollPhysics(),
  onSelectedItemChanged: (index) {
    print('$index');
    },
  children: List.generate(10, (index) => Container(
    alignment: Alignment.center,
    color: Colors.red[200],
    child: Text('${index}'),
  ),),
)

simulator_screenshot_28D825F2-ECCA-4413-9207-69D037B9F093.png

  • 横向滚动 很遗憾ListWheelScrollView不支持横向滚动,但是我们可以使用RotatedBox处理横向问题 截屏2022-05-10 下午3.38.55.png

截屏2022-05-10 下午3.40.19.png

PageView

PageView(
  //滚动事件
  onPageChanged: (index) => print('$index'),
  //滑动方向
  scrollDirection: Axis.vertical,
  children: [
    Container(color: Colors.red,),
    Container(color: Colors.orangeAccent,),
  ],
)

ReorderableListView 可拖动的换位置的listView

ReorderableListView(
  header: Container(
    alignment: Alignment.center,
    height: 40,
    color: Colors.green,
    key: UniqueKey(),//注意需要唯一的Key
    child: const Text('这个是Header 不支持拖动'),
  ),
  children: List.generate(10, (index) => Container(
    alignment: Alignment.center,
    height: 60,
    color: Colors.red[index % 9 *100],
    key: UniqueKey(),//注意需要唯一的Key
    child: Text('$index'),)
  ),
  onReorder: (int newIndex, int oldIndex) {
    print('newIndex:$newIndex ,oldIndex:$oldIndex');
  },
)

simulator_screenshot_CBE2AF0C-DB2C-4B6F-8B7D-7573F0D2FE23.png

SingleChildScrollView

  • 处理单个视图的滚动,eg: Column本身不支持滚动,但是内容太过多就会溢出,可以考虑使用SingleChildScrollView
  • 当内容不溢出的时候,是不滚动的
  • 但是不推荐这么使用,因为SingleChildScrollView 会导致layout两遍,效率很低
SingleChildScrollView(
  child: Column(
    children: [
      FlutterLogo(size:500),
      FlutterLogo(size:400),
    ],
  ),
)

simulator_screenshot_AB71BDDE-5797-4416-88C6-1FE63F42F6AE.png

异步

简单使用

以下两种都是异步操作

//.then
void _incrementCounter() {
  Future.delayed(Duration(seconds: 2)).then((value) {
    setState(() {
      _counter++;
    });
  });
}

//async await
void _incrementCounter2() async {
   await Future.delayed(Duration(seconds: 2));
   setState(() {
     _counter++;
   });
}

事件循环Event Loop

dart的异步操作不是多线程

事件队列

  • EventQueue:事件队列
  • MicrotaskQueue: 微任务事件,优先级大于EventQueue 截屏2022-05-10 下午4.19.43.png

事件执行类型

截屏2022-05-10 下午4.21.49.png

  • 直接运行
void futureTest() {
  print('1');
  Future.sync(() => print('Future.sync'));
  Future.value(_getName());
  print('2');
}

打印

flutter: 1
flutter: Future.sync
flutter: Closure: <Y0>([FutureOr<Y0>?]) => Future<Y0> from Function 'Future.value': static.
flutter: 2
  • Microtask
void futureTest() {
  print('1');
  scheduleMicrotask(() => print('Microtask 1'));
  Future.microtask(() => print('Microtask 2'));
  Future.value(123).then((value) => print('Microtask 3'));
  print('2');
}

打印

flutter: 1
flutter: 2
flutter: Microtask 1
flutter: Microtask 2
flutter: Microtask 3
  • event
void futureTest() {
  print('1');
  Future.delayed(Duration(seconds: 1)).then((value) => print('event 1'));
  print('2');
}

打印

flutter: 1
flutter: 2
flutter: event 1

Future

Future的三种状态

http.get('www.baidu.com')//未完成态
    .then((value) => null)//完成态
    .catchError(onError)//错误态
    .whenComplete(() => null);//完成态

.then 可以多个使用

Future<int> futureTest1() {
  return Future.value(100);
}

void futureTest() {
  futureTest1()
      .then((value)  {
    print(value);
    return value * 2;
  }).then((value) => print(value));

打印

flutter: 100
flutter: 200

FutureBuilder

child: FutureBuilder(
  // future: Future.delayed(Duration(seconds: 2)).then((value) => 555),
  future: Future.delayed(Duration(seconds: 2), () => 555),
  // future: Future.delayed(Duration(seconds: 2)).then((value) => throw('错误')),
  initialData: 72,//没有完成前
  builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
    //方式一
    // if (snapshot.connectionState == ConnectionState.waiting) {
    //   return CircularProgressIndicator();
    // }
    // if (snapshot.connectionState == ConnectionState.done) {
    //   print(snapshot.error);
    //   print(snapshot.data);
    //   if (snapshot.hasError) {
    //     return Icon(Icons.error);
    //   }
    //   if (snapshot.hasData) {
    //     return Text('${snapshot.data}',style: TextStyle(fontSize: 72),);
    //   }
    // }
    // throw "should not happen";
 
 
    //方式二
    if (snapshot.hasData) {
      return Text('${snapshot.data}',style: TextStyle(fontSize: 72),);
    }

    if (snapshot.hasError) {
      return Icon(Icons.error);
    }

    return CircularProgressIndicator();
  },
),

Stream & StreamBuilder

如果说Future是一次等待处理,那么Stream 就是多次等待处理

创建

final _testStream = Stream.periodic(Duration(seconds: 1), (_) => 44);
final _streamController = StreamController();//一个stream监听, 事件会被缓存
final _streamController_broadcast = StreamController.broadcast();//多个监听,事件不会被缓存

事件 sink.add、listen

  • 添加事件:_streamController.sink.add(77);
  • 监听事件:_streamController.stream.listen((event) { });
  • 组合事件:
_streamController.stream
    .map((event) => event * 2)//map可以处理event
    .where((event) => event is int)//where可以过滤
.distinct();//可以去重
  • 需要关闭,一旦关闭在去add就会奔溃
@override
void dispose() {
  //需要关闭
  _streamController.close();
  super.dispose();
}
  • Steam流方法
Stream<DateTime> _getTime() async* {
  while (true) {
    await Future.delayed(Duration(seconds: 1));
    yield DateTime.now();
  }
}

使用列子

final _testStream = Stream.periodic(Duration(seconds: 1), (_) => 44);
final _streamController = StreamController();//一个stream监听, 事件会被缓存
final _streamController_broadcast = StreamController.broadcast();//多个监听,事件不会被缓存

_dd() {
  _streamController.sink.add(77);
  _streamController.stream.listen((event) { });

  _streamController.stream
      .map((event) => event * 2)//map可以处理event
      .where((event) => event is int)//where可以过滤
  .distinct();//可以去重
}

Stream<DateTime> _getTime() async* {
  while (true) {
    await Future.delayed(Duration(seconds: 1));
    yield DateTime.now();
  }
}

@override
void dispose() {
  // TODO: implement dispose
  //需要关闭
  _streamController.close();
  super.dispose();
}


Column(
  children: [
    RaisedButton(onPressed: () => _streamController.sink.add(1),child: Text('add1'),),
    RaisedButton(onPressed: () => _streamController.sink.add(99),child: Text('add99'),),
    //close() 之后就不能再接受数据了,会崩溃
    RaisedButton(onPressed: () => _streamController.sink.close(),child: Text('Close'),),
    StreamBuilder(
        //多种情况的stream
        // stream: null,
        // stream: _testStream,
        // stream: _streamController.stream,
        stream: _getTime(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.none:
              return Text("none: 没有数据");
            break;
            case ConnectionState.waiting:
              return Text("waiting: 等待");
              break;
            case ConnectionState.active:
              if (snapshot.hasError) {
                return Text("active: 错误:${snapshot.error}");
              } else {
                return Text("active: 正常:${snapshot.data}");
              }
              break;
            case ConnectionState.done:
              return Text("Done");
              break;
          }
          return Container();
        }),
  ],
),

Sliver - 片段

Sliver 是滚动视图中的片段,需要通过视窗(一般是滚动的容器:CustomScrollView)才能使用Sliver片段

  • 使用Sliver后,内部的子组件都应该是Sliver开头的子组件。eg:SliverGrid,SliverList、、、

使用

  • SliverToBoxAdapter: 用于转换具体的组件,一对一
  • SliverList: 类似ListView,内部可以放好多
  • SliverGrid: 类似GridView,内部可以放好多
body: CustomScrollView(
  slivers: [
    SliverToBoxAdapter(child: FlutterLogo(size: 100,)),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return Container(
            height: 50,
            color: Colors.primaries[Random().nextInt(18)],
          );
        },
        childCount: 4,//定义个数
      ),
    ),
    SliverGrid(
      delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
          return Container(
            height: 50,
            color: Colors.primaries[Random().nextInt(18)],
          );
        },
        childCount: 20,//定义个数
      ),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 4,
      ),
    ),
  ],
)

simulator_screenshot_9524DF4F-AC3F-4533-A5CD-B0589CEDBF5B.png

各种各样的Sliver

  • SliverPrototypeExtentList: 通过prototypeItem 可以做到不同设备间的适配
SliverPrototypeExtentList(
  prototypeItem:  FlutterLogo(size: 30,),
  delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
      return Container(
        height: 50,
        color: Colors.primaries[Random().nextInt(18)],
      );
    },
    childCount: 4,//定义个数
  ),
),
  • SliverFillViewport:每个子部件都是占满屏幕的,类似pageView
SliverFillViewport(delegate: SliverChildBuilderDelegate(
      (BuildContext context, int index) {
    return Container(
      color: Colors.primaries[Random().nextInt(18)],
    );
  },
  childCount: 4,//定义个数
),)

SliverAppBar 可以做到动态的AppBar

  • 类似上滑隐藏AppBar
SliverAppBar(title: Text('SliverAppBar'),
  floating: true,
  // pinned: true,
  snap: true,
  expandedHeight: 300,
  flexibleSpace: FlexibleSpaceBar(
    background: FlutterLogo(),
    title: Text('FlexibleSpaceBar'),
    collapseMode: CollapseMode.parallax,//上滑效果
    stretchModes: [ //下拉效果
      StretchMode.blurBackground,
      StretchMode.zoomBackground,
      StretchMode.fadeTitle,
    ],
  ),
),

simulator_screenshot_45FD7827-F76A-4455-8D41-0DFD70BF0451.png

SliverOpacity

注意内部不是child 是 sliver

SliverOpacity(
    opacity: 0.5,
    sliver: SliverToBoxAdapter(
        child: FlutterLogo(
      size: 100,
    ))),

SliverAnimatedOpacity

SliverAnimatedOpacity(
  opacity: 0.5,
  duration: Duration(seconds: 3),
  sliver: SliverToBoxAdapter(
      child: FlutterLogo(
    size: 100,
  )),
),

SliverFillRemaining

  • 填充剩余空间
  • 一般用于Sliver 最底部
SliverFillRemaining(child: Placeholder(),),

Simulator Screen Shot - iPhone SE (2nd generation) - 2022-05-11 at 19.30.29.png

SliverFillRemaining(child: Center(child: CircularProgressIndicator(),),),

simulator_screenshot_CF8B2FDC-DD12-4702-BB88-0F259C01B7D6.png

SliverLayoutBuilder 用于查看约束,便于理解sliver原理

SliverLayoutBuilder(builder: (BuildContext context, SliverConstraints constraints) {
  print('$constraints');
  return SliverToBoxAdapter();
}),

打印

flutter: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.forward, scrollOffset: 781.6, remainingPaintExtent: 667.0, crossAxisExtent: 375.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 667.0, remainingCacheExtent: 1167.0, cacheOrigin: -250.0)

SliverPersistentHeader

固定头效果

SliverPersistentHeader(delegate: delegate)

截屏2022-05-11 下午8.00.12.png

BuildContext

BuildContext到底是什么:看源码

/// [BuildContext] objects are actually [Element] objects. The [BuildContext]
/// interface is used to discourage direct manipulation of [Element] objects.
abstract class BuildContext {
...
}

==> 第一句: BuildContext是element

==> 第二句: 不鼓励直接操作[Element] 对象

setState

  • 使用setState 会触发重绘
int _counter = 0;
void _incrementCounter() {
  //单独_counter++ 是不生效的
  _counter++;

  //setState 触发重绘 才会生效
  setState(() {
    _counter++;
  });
}
  • 原理: 看源码 => 可以看到setState将当前element 标记需要重新build,所以后面会调用build方法
@protected
void setState(VoidCallback fn) {
 //...
 //...
 _element!.markNeedsBuild();
}
  • 所以我们也可以手动强制使用markNeedsBuild(),也可以触发页面刷新; eg:
int _counter = 0;
void _incrementCounter() {
  _counter++;
  (context as Element).markNeedsBuild();
}

但是直接操作Element是不被推荐的,因为有性能上的浪费

XXX.of(context)

Scaffold.of(context).showSnackBar(snackbar);
Theme.of(context)

平常我们接触很多类似上面这种context的用法。那这个context是什么呢

==> XXX.of(context)方法通过传入的context找到当前Widget树上面存在的第一个对应的state 然后做操作。eg:

Scaffold.of(context).showSnackBar(snackbar);

static ScaffoldState of(BuildContext context) {
  assert(context != null);
  final ScaffoldState? result = context.findAncestorStateOfType<ScaffoldState>();
  ...
  ...
}  

Scaffold.of内部是,通过context.findAncestorStateOfType方法找到ScaffoldState类型的state,再返回。这个查找的顺序是从当前context往上找;

组件拆分会增加层次,能避免找不到对应的组件

下面的Drawer在点击FloatingActionButton是会崩溃的,显示不了Drawer,因为当前FloatingActionButtonScaffold 在同一层级找不到Scaffold

class _TestPageState extends State<TestPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sliver'),
      ),
      body: Container(
        height: 50,
        color: Colors.red,
      ),
      drawer: Drawer(),
      floatingActionButton:FloatingActionButton(onPressed: () {
        Scaffold.of(context).openDrawer();
      }),
    );
  }
}

应该拆分出来,这样Foo就是TestPage的下级了,而TestPage内部有Scaffold,就能找到,所以能打开Drawer()

class _TestPageState extends State<TestPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sliver'),
      ),
      body: Container(
        height: 50,
        color: Colors.red,
      ),
      drawer: Drawer(),
      floatingActionButton: Foo(),
    );
  }
}

class Foo extends StatelessWidget {
  const Foo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(onPressed: () {
      Scaffold.of(context).openDrawer();
    });
  }
}

build是什么

上面的情况还可以使用LayoutBuilder,因为LayoutBuilder的不属于当前的context 不属于当前的context

  Widget build(BuildContext context) {//1 context
    return Scaffold(
      appBar: AppBar(
        title: Text('Sliver'),
      ),
      body: Container(
        height: 50,
        color: Colors.red,
      ),
      drawer: Drawer(),
      floatingActionButton: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {// 2 context
        return FloatingActionButton(onPressed: () {
          Scaffold.of(context).openDrawer();//3 context
        });
      }),
    );
  }
}

可以看到当光标选择context的时候,2和3 是同一个context,而1不是

截屏2022-05-12 下午12.30.45.png

到这里我们可以知道builder内部其实是创建一个匿名的组件,会有一个新的context

key

key: 保证widget的唯一性。

key的原理

widget和state是分开的,下图中color是在widget中的,_count是在state中的 截屏2022-05-16 下午7.26.54.png

  • 先判断类型,再判断key
  • 同级寻找对应关系 截屏2022-05-16 下午7.29.43.png
  • StatefulWidget 才需要key,StatelessWidget不需要key

key的类型

  • local key: 局部key,只校验同级,性能好.下面中还有两个valueKey(1),但是它们不是在同一级,所以是ok的 截屏2022-05-16 下午7.37.56.png

  • global key: 全局key,校验全局,性能差

LocalKey

LocalKey有三种: valueKey, ObjectKey, UniqueKey

  • valueKey: 对比的是value
TextField(key:ValueKey('name));
TextField(key:ValueKey('password));
  • ObjectKey: 对比的是Object 下面两个得到的key是不相等的。
TextField(key:ObjectKey(new Student()));
TextField(key:ObjectKey(new Student()));
  • UniqueKey: 唯一key,每次都会变,但是不相等 一般用于强行需要widget更新
TextField(key:UniqueKey());
TextField(key:UniqueKey());

GlobalKey

  • 全局唯一 截屏2022-05-16 下午7.51.01.png
  • 快速获取某个widget的(CurrentState,CurrentWidget,CurrentContext), 注意需要通过as转换成对应类型
  1. CurrentState 截屏2022-05-16 下午7.54.53.png
  2. CurrentWidget 截屏2022-05-16 下午7.59.00.png
  3. CurrentContext 截屏2022-05-16 下午7.58.38.png

动画

如何选择动画

截屏2022-09-14 上午9.48.05.png

隐式动画(自动控制)

两行代码就能动起来

  • 使用AnimatedContainer
class _AnimationPageState extends State<AnimationPage> {
  double  pHeight = 150;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Animation'),
        ),
        body: Center(
          
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 1000),
            width: 300,
            height: pHeight,
            color: Colors.red,
          ),
        ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            pHeight += 100.0;
          });
        },
      ),
    );
  }
}

ps: 不影响child内部的效果

  • 蓄水效果 截屏2022-09-14 下午1.35.06.png

在不同控件之间切换的过渡动画

  • 使用AnimatedSwitcher可以做到在不同控件间有过度动画
AnimatedSwitcher(
  duration: const Duration(milliseconds: 1000),
  child: isOk ? Text('1111111') : Image.network(imageUrl) ,
),

child 需要是不同类型的控件,如果是相同是没有动画效果的;但是如果相同控件拥有不同的key,也是会有动画效果的,因为AnimatedSwitcher会先判断类型,再判断key,如果可以不同也会有动画效果

AnimatedSwitcher(
  duration: const Duration(milliseconds: 1000),
  child: isOk ? Text('1111111') : Text('2222222',key: GlobalKey(),)  ,
),
  • AnimatedSwitcher的transitionBuilder属性:控制动画的效果
AnimatedSwitcher(
  transitionBuilder: (child,animation) {
    //还可以组合
    return RotationTransition(
      turns: animation,
      child: ScaleTransition(
        scale: animation,
        child: child,)
    );
    // 旋转
    return RotationTransition(
      turns: animation,
      child: child,
    );
    //AnimatedSwitcher 默认的Transition
    return FadeTransition(
      opacity: animation,
      child: child,
    );
  },
  duration: const Duration(milliseconds: 1000),
  child: isOk ? Text('1111111',style: TextStyle(fontSize: 72),) : Text('2222222',key: GlobalKey(),)  ,
),

更多动画控件及曲线(Curves)

  • 任何Animated控件存在curve属性,用于指定动画变化线性关系
AnimatedPadding(
  duration: Duration(seconds: 1),
  curve: Curves.linear,//线性变化
  //curve: Curves.bounceInOut,//弹跳变化
  padding: EdgeInsets.only(top: 0),
  child: Container(
    width: 300,
    height: 300,
    color: Colors.blue,
    child: null,
  ),

内置的还不够用? ==》万能补间动画

  • TweenAnimationBuilder
  • tween:代表between,设置初始和结尾的动画效果值
  • builder:每次tween中的值变化时,这个builder就会被调用
TweenAnimationBuilder(
  tween: Tween(begin: 0.0,end: 1.0) ,//between 0-1
  builder: (BuildContext context, value, Widget? child) {
    //这里的value就是tween 中的0.0 - 1.0在1秒中的变化值
    return Opacity(
      opacity: value as double,
      child: Container(
        height: 300,
        width: 300,
        color: Colors.blue,
      ),
    );
  }, duration: Duration(seconds: 1),
),
  • value是个泛型,可以是任何值,下面就是代表字体大小
TweenAnimationBuilder(
  duration: Duration(seconds: 1),
  tween: Tween<double>(begin: 20, end: 100), //between 0-1
  builder: (BuildContext context, value, Widget? child) {
    return Container(
      height: 300,
      width: 300,
      color: Colors.blue,
      child: Text(
        'Hi',
        style: TextStyle(fontSize: value as double),
      ),
    );
  },
),

实例:翻滚吧!计数器 ==》 封装成一枚动画控件

截屏2022-09-14 下午2.34.18.png

显示动画(手动控制)

可以无尽旋转的显示动画

  • SingleTickerProviderStateMixin:获取硬件设备屏幕刷新参数,每次屏幕刷新就会产生一次Ticker

SingleTickerProviderStateMixin 只能提供一个Ticker,如果想要多个可以使用TickerProviderStateMixin

  • 要在initState中初始化AnimationController
  • dispose()中要移除
  • _animationController.forward();//转一次
  • _animationController.reset();//重置
  • _animationController.repeat();//循环
class _AnimationPageState extends State<AnimationPage>  with SingleTickerProviderStateMixin{
  late AnimationController _animationController;
  bool _loading = true;
  
  @override
  void initState() {
    _animationController = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this// 垂直同步
    );
    super.initState();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animation'),
      ),
      body: Center(
        child: RotationTransition(
          turns: _animationController,
          child: Container(
            height: 300,
            width: 300,
            color: Colors.blue,
            child: Text(
              'Hi',
              style: TextStyle(fontSize: 100),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // _animationController.stop();//转一次
          // _animationController.forward();//转一次
          if (_loading) {
            _animationController.reset();//重置
          } else {
            _animationController.repeat();//循环
            //_animationController.repeat(reverse: true);//循环变大变小
          }
          _loading = !_loading;
        },
      ),
    );
  }
}

动画控制器到底是个什么东西

class AnimationController extends Animation<double> {}

由源码可知AnimationController 是Animation一系列的double,动画开始的时候它会从lowerBound设置的值到upperBound设置的值中不断在duration这个时间段中借助vsync垂直同步(屏幕硬件FPS)变化而变化

_animationController = AnimationController(
  duration: Duration(seconds: 1),
  lowerBound: 3.0,
  upperBound: 4.0,
  vsync: this// 垂直同步
);
_animationController.addListener(() {
  print('${_animationController.value}');
});

RotationTransition(
  turns: _animationController,//[3.0..4.0]之间的一个值
  child: Container(
    height: 300,
    width: 300,
    color: Colors.blue,
    child: Text(
      'Hi',
      style: TextStyle(fontSize: 100),
    ),
  ),
),

//打印结果

flutter: 3.0
flutter: 3.016666
flutter: 3.033333
...
flutter: 3.983334
flutter: 4.0

RotationTransitionturns就会从变化中_animationController中取值

如何使用控制器串联补间(Tween)和曲线

-小技巧: 自动创建一个带有AnimationController的快捷 ==> sta

class  extends StatefulWidget {
  const ({Key? key}) : super(key: key);

  @override
  State<> createState() => _State();
}

class _State extends State<> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  • 使用_animationController.drive(Tween(begin: 0.0,end:1.0)), 可以替换 _animationControllerlowerBound、upperBound
ScaleTransition(
  scale: _animationController.drive(Tween(begin: 0.0,end:1.0)),
  child: Container(
    height: 300,
    width: 300,
    color: Colors.blue,
    child: Text(
      'Hi',
      style: TextStyle(fontSize: 100),
    ),
  ),
),
  • 其他写法
SlideTransition(
  position: _animationController.drive(Tween(begin: Offset(0, 0),end:Offset(0.5, 0))),
  position: 
      (Tween(begin: Offset(0, 0), end: Offset(0.5, 0)))  
    .chain(CurveTween(curve: Curves.easeInOut))
    .chain(CurveTween(curve: Interval(0.8,1.0)))
      .animate(_animationController),
  child: Container(
    height: 300,
    width: 300,
    color: Colors.blue,
  ),
),

交错动画

截屏2022-09-15 上午10.40.27.png

内置不够用?==》 万能自定义动画

  • AnimatedBuilder:自定义各种动画,需要配合AnimationController 截屏2022-09-15 上午10.55.28.png
  • 使用child,减少不必要的渲染
  • 使用Tween.evaluate 更加灵活 :Tween(begin: 200.0,end:300.0).evaluate(_animationController)
_animationController =
    AnimationController(duration: Duration(seconds: 1), vsync: this // 垂直同步
        )
      ..repeat();

AnimatedBuilder(
    animation: _animationController,
    builder: (BuildContext context, Widget? child) {
      return Opacity(
          opacity: _animationController.value,
          child: Container(
            height: Tween<double>(begin: 200.0,end:300.0).evaluate(_animationController),
            width: 300,
            color: Colors.blue,
            child: child,
          ));
    },
    child: const Center(
      child: Text(
        'Hi',
        style: TextStyle(fontSize: 100),
      ),
    ),
  ),
),
  • 还可以直接使用Animation 截屏2022-09-15 上午11.11.43.png

其他

flutter动画背后的机制和原理

  • 隐式动画: 以AnimatedContainer为例 看源码
class AnimatedContainer extends ImplicitlyAnimatedWidget {}

abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin<T> {
  /// The animation controller driving this widget's implicit animations.
  @protected
  AnimationController get controller => _controller;
  late final AnimationController _controller = AnimationController(
    duration: widget.duration,
    debugLabel: kDebugMode ? widget.toStringShort() : null,
    vsync: this,
  );

可以看到AnimatedContainer 继承ImplicitlyAnimatedWidget,而ImplicitlyAnimatedWidget内部也是使用AnimationController 做动画处理的

  • 显示动画: 以AnimatedBuilder为例
//继承于AnimatedWidget
class AnimatedBuilder extends AnimatedWidget {
  const AnimatedBuilder({
    Key? key,
    required Listenable animation,//内部监听一个animation
    required this.builder,
    this.child,
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

  final TransitionBuilder builder;practice.
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
}

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    Key? key,
    required this.listenable,
  }) : assert(listenable != null),
       super(key: key);
       
  @protected
  Widget build(BuildContext context);
  @override
  State<AnimatedWidget> createState() => _AnimatedState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
  }
}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    //添加对这个listenable的监听回调事件
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }
  
  //回调事件会每次调用setState
  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }
  
  //也就会每次重新widget.build(context)
  @override
  Widget build(BuildContext context) => widget.build(context);
}

总结: 监听动画控制器,当动画的数值发生变化时,就直接调用setState(),重新渲染整个控件

  • 什么是Ticker: 硬件设备屏幕刷新就会有个回调
Ticker _ticker = Ticker((duration) => setState(() {}));

hero动画(主动画)

截屏2022-09-15 下午2.48.03.png

直接操作底层的CustomPainter