Flutter Widget : Flow

Flow组件结合FlowDelegate在Flutter中用于自定义布局,通过在paint阶段使用变换矩阵调整子Widget的位置,实现高效重绘。文章通过一个FlowMenu示例展示了如何创建动画效果,当Animation变化时Flow自动重绘,避免了不必要的布局计算。此外,对比Stack,Flow提供了更灵活的布局方式,例如实现多个Widget的重叠效果。
摘要由CSDN通过智能技术生成

写在前面

\Flow组件需要搭配 \FlowDelegate使用,在 \FlowDelegate里编写逻辑,\Flow就可以通过它来调整其 children 的尺寸和位置。

使用 \Flow是用来优化 children 使用变换矩阵(transformation matrices)重新布局。类似于 Stack。

也就是说当我们想自定义一种对 children 布局的 Widget 的时候,可以使用 \Flow

内容

基本用法

从官方的例子来看一个基本的用法。在这个例子里,除了展示对 children 的布局,还演示了如何通过使用 Animation来让 children 使用的变换矩阵发生变化,从而达到位置的变更。

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

   @override
   _FlowMenuState createState() => _FlowMenuState();
 }

 class _FlowMenuState extends State<FlowMenu> with SingleTickerProviderStateMixin {
   late AnimationController menuAnimation;
   IconData lastTapped = Icons.notifications;
   final List<IconData> menuItems = <IconData>[
     Icons.home,
     Icons.new_releases,
     Icons.notifications,
     Icons.settings,
     Icons.menu,
   ];

   void _updateMenu(IconData icon) {
     if (icon != Icons.menu) {
       setState(() => lastTapped = icon);
     }
   }

   @override
   void initState() {
     super.initState();
     menuAnimation = AnimationController(
       duration: const Duration(milliseconds: 250),
       vsync: this,
     );
   }

   Widget flowMenuItem(IconData icon) {
     final double buttonDiameter = MediaQuery.of(context).size.width / menuItems.length;
     return Padding(
       padding: const EdgeInsets.symmetric(vertical: 8.0),
       child: RawMaterialButton(
         fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
         splashColor: Colors.amber[100],
         shape: const CircleBorder(),
         constraints: BoxConstraints.tight(Size(buttonDiameter, buttonDiameter)),
         onPressed: () {
           _updateMenu(icon);
           menuAnimation.status == AnimationStatus.completed
             ? menuAnimation.reverse()
             : menuAnimation.forward();
         },
         child: Icon(
           icon,
           color: Colors.white,
           size: 45.0,
         ),
       ),
     );
   }

   @override
   Widget build(BuildContext context) {
     return Flow(
       delegate: FlowMenuDelegate(menuAnimation: menuAnimation),
       children: menuItems.map<Widget>((IconData icon) => flowMenuItem(icon)).toList(),
     );
   }
 }

 class FlowMenuDelegate extends FlowDelegate {
   FlowMenuDelegate({required this.menuAnimation}) : super(repaint: menuAnimation);

   final Animation<double> menuAnimation;

   @override
   bool shouldRepaint(FlowMenuDelegate oldDelegate) {
     return menuAnimation != oldDelegate.menuAnimation;
   }

   @override
   void paintChildren(FlowPaintingContext context) {
     double dx = 0.0;
     for (int i = 0; i < context.childCount; ++i) {
       dx = context.getChildSize(i)!.width * i;
       context.paintChild(
         i,
         transform: Matrix4.translationValues(
           dx * menuAnimation.value,
           0,
           0,
         ),
       );
     }
   }
 }

相比在 layout 阶段布局 children,这里是在 paint 阶段,在 \FlowDelegate.paintChildren方法里,让 children 使用变换矩阵来确定位置。这样通过简单地对\Flow进行重绘,从而达到 children 的高效重新定位。就无需让 children 再重新布局。

最有效地促使 \Flow进行重绘地方法就是给 \FlowDelegate的构造方法里提供一个 Animation,这样 \Flow就会监听这个 Animation,然后当 Animation有回调时,\Flow就会进行重绘,避免了 build 和 layout。

构造方法

class Flow extends MultiChildRenderObjectWidget {
  Flow({
    Key? key,
    required this.delegate,
    List<Widget> children = const <Widget>[],
    this.clipBehavior = Clip.hardEdge,
  }) : assert(delegate != null),
       assert(clipBehavior != null),
       super(key: key, children: RepaintBoundary.wrapAll(children));

  Flow.unwrapped({
    Key? key,
    required this.delegate,
    List<Widget> children = const <Widget>[],
    this.clipBehavior = Clip.hardEdge,
  }) : assert(delegate != null),
       assert(clipBehavior != null),
       super(key: key, children: children);
  
  ...
}

\Flow有两个构造方法,区别在于默认情况下会用 RepaintBoundary进行包裹,它包裹住每个 children,避免当 \Flow进行重绘的时候,导致 children 也进行重绘。

一个例子

假如我们想实现一种多个 Widget 之间有重叠的效果:
在这里插入图片描述
我们当然可以使用 Stack,然后给每个 Widget 套上 Position,计算 x 来进行。但如果使用\Flow,我们可以做出一个封装性更好的 Widget 出来。

void main() {
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  Widget _circle(Color color) {
    return Container(
      height: 30,
      width: 30,
      decoration: BoxDecoration(color: color, shape: BoxShape.circle),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
            child: SizedBox(
          height: 30,
          child: MyHorizontalOverlayOffsetWidget(xOffset: 16, children: [
            _circle(Colors.red),
            _circle(Colors.amber),
            _circle(Colors.black),
            _circle(Colors.blue)
          ]),
        )),
      ),
    );
  }
}

class MyHorizontalOverlayOffsetWidget extends StatelessWidget {
  final List<Widget> children;

  /// children 之间水平方向的偏移量
  final double xOffset;

  /// 将 children 里的多个 Widget 进行一定的重叠偏移的 Widget
  const MyHorizontalOverlayOffsetWidget(
      {Key? key, required this.children, this.xOffset = 0.0})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Flow(
        delegate: OverlayOffsetDelegate(xOffset: xOffset), children: children);
  }
}

class OverlayOffsetDelegate extends FlowDelegate {
  final double xOffset;

  OverlayOffsetDelegate({this.xOffset = 0.0});

  @override
  void paintChildren(FlowPaintingContext context) {
    var currentXOffset = 0.0;
    for (var index = 0; index < context.childCount; index++) {
      context.paintChild(index,
          transform: Matrix4.translationValues(currentXOffset, 0, 0));
      currentXOffset += xOffset;
    }
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) {
    return oldDelegate is OverlayOffsetDelegate &&
        oldDelegate.xOffset != xOffset;
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值