【Flutter高级玩法- Flow 】我的位置我做主

时间:2022-07-27
本文章向大家介绍【Flutter高级玩法- Flow 】我的位置我做主,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

零、前言

Flow布局是一个超级强大的布局,但应该很少有人用,因为入手的门槛还是有的 Flow的属性很简单,只有FlowDelegate类型的delegate和组件列表children, 可能很多人看到delegate就挥挥手:臣妾做不到,今天就来掰扯一下这个FlowDelegate.

class Flow extends MultiChildRenderObjectWidget {
  Flow({
    Key key,
    @required this.delegate,
    List children = const [],
  }) : assert(delegate != null),

1

2

1
2

第一幕、开场-演员入台

1. 展示舞台

我们的第一个舞台是一个200*200的灰色box,由FlowDemo组件出当主角

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Scaffold(
          appBar: AppBar(),
          body: Center(child: HomePage()),
        ));
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200,
      height: 200,
      color: Colors.grey.withAlpha(66),
      alignment: Alignment.center,
      child: FlowDemo(),
    );
  }
}

2. Flow出场

FlowDemo中使用Flow组件,包含四个box 四个box变成依次是60.0(红), 50.0(黄), 40.0(蓝), 30.0(绿)

class FlowDemo extends StatelessWidget {
  final sides = [60.0, 50.0, 40.0, 30.0];
  final colors = [Colors.red,Colors.yellow,Colors.blue,Colors.green];
  
  @override
  Widget build(BuildContext context) {
    return Flow(
      delegate: _Delegate(),
      children: sides.map((e) => _buildItem(e)).toList(),
    );
  }

  Widget _buildItem(double e) {
    return Container(
      width: e,
      alignment: Alignment.center,
      height: e,
      color: colors[sides.indexOf(e)],  
      child: Text('$e'),
    );
  }
}

3. FlowDelegate出场

Flow布局需要一个FlowDelegate类型的delegate对象 但是Flutter中并没有其实现类,所以想玩Flow,只有一条路:自定义

class _Delegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {

  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return true;
  }
}


4. paintChildren方法和FlowPaintingContext对象

paintChildren顾名思义是用来画孩子的 FlowPaintingContext也就是绘制的上下文,即绘制的信息 那就轻轻的瞄一眼FlowPaintingContext里面有啥吧: 一共有四个东西: size、childCount、getChildSize、paintChild

---->[源码:flutter/lib/src/rendering/flow.dart:23]----
abstract class FlowPaintingContext {
  Size get size;//父亲尺寸
  int get childCount;//孩子个数
  Size getChildSize(int i);//第i个孩子尺寸
  //绘制孩子
  void paintChild(int i, { Matrix4 transform, double opacity = 1.0 });
}

接下来用代码测试一下这几个属性看看,不出所料 默认是绘制在父容器的左上角。

class _Delegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    print("父容器尺寸:${context.size}");
    print("孩子个数:${context.childCount}");
    for(int i=0;i复制代码

第二幕、排兵布阵

前面只是将组件排在了左上角,那如何对进行其他排布呢?

1. paintChild与Matrix4

paintChild时可以传入transform的Matrix4对象进行变换 在这里基本上只用了Matrix4的平移translationValues功能, 至于Matrix4的具体用法,那又是一个故事了 这里让黄色的box移到右上角,即X方向平移(父宽-己宽):

  @override
  void paintChildren(FlowPaintingContext context) {
    var size = context.size;
    for (int i = 0; i < context.childCount; i++) {
      if (i == 1) {
        var tr = context.getChildSize(i);
        context.paintChild(i,
            transform:
                Matrix4.translationValues(size.width - tr.width, 0, 0.0));
      } else {
        context.paintChild(i);
      }
    }
  }

现在让四个组件排布在父亲的四角,如下:

class _AngleDelegate extends FlowDelegate {
  Matrix4 m4;

  @override
  void paintChildren(FlowPaintingContext context) {
    var size = context.size;
    for (int i = 0; i < context.childCount; i++) {
      var cSize = context.getChildSize(i);
      if (i == 1) {
        m4 = Matrix4.translationValues(size.width - cSize.width, 0, 0.0);
      } else if (i == 2) {
        m4 = Matrix4.translationValues(0, size.height - cSize.height, 0.0);
      } else if (i == 3) {
        m4 = Matrix4.translationValues(size.width - cSize.width, size.height - cSize.height, 0.0);
      }
      context.paintChild(i, transform: m4);
    }
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return true;
  }
}

2. Flow布局的封装

如果需要一个排布四角的组件,可以基于上面的Delegate做一个组件 虽然用处很有限,但原来了解一下Flow还是挺好的。

class AngleFlow extends StatelessWidget {
  final List children;

  AngleFlow({@required this.children}) : assert(children.length == 4);

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

class _AngleDelegate extends FlowDelegate {
  Matrix4 m4;

  @override
  void paintChildren(FlowPaintingContext context) {
    var size = context.size;
    for (int i = 0; i < context.childCount; i++) {
      var cSize = context.getChildSize(i);
      if (i == 1) {
        m4 = Matrix4.translationValues(size.width - cSize.width, 0, 0.0);
      } else if (i == 2) {
        m4 = Matrix4.translationValues(0, size.height - cSize.height, 0.0);
      } else if (i == 3) {
        m4 = Matrix4.translationValues(
            size.width - cSize.width, size.height - cSize.height, 0.0);
      }
      context.paintChild(i, transform: m4);
    }
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return true;
  }
}

3. 圆形的Flow布局

其实可以看出,Flow的核心就是根据信息来计算位置 所以,所有的布局都可以通过Flow进行实现。 除此之外对应一些特定情况的布局,使用Flow会非常简单,比如:

class CircleFlow extends StatelessWidget {
  final List children;

  CircleFlow({@required this.children});

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

class _CircleFlowDelegate extends FlowDelegate {
  @override //绘制孩子的方法
  void paintChildren(FlowPaintingContext context) {
    double radius = context.size.shortestSide / 2;
    var count = context.childCount;
    var perRad = 2 * pi / count;
    for (int i = 0; i < count; i++) {
      print(i);
      var cSizeX = context.getChildSize(i).width / 2;
      var cSizeY = context.getChildSize(i).height / 2;

      var offsetX = (radius - cSizeX) * cos(i * perRad) + radius;
      var offsetY = (radius - cSizeY) * sin(i * perRad) + radius;
      context.paintChild(i,
          transform: Matrix4.translationValues(
              offsetX - cSizeX, offsetY - cSizeY, 0.0));
    }
  }
  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return true;
  }
}

第三幕、当Flow遇到Animation

全面说Flow最重要的就是进行定位,而动画的本质是若干个变动的数字 那么两者自然是郎才女貌,情投意合

1.圆形布局 + 旋转

前面圆形布局靠的是计算某个组件偏转的角度 那么想要实现旋转是非常简单的,由于有角度的状态,所以StatefulWidget

class CircleFlow extends StatefulWidget {
  final List children;
  CircleFlow({@required this.children});

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

class _CircleFlowState extends State
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  double rad = 0.0;

  @override
  void initState() {
    _controller =
        AnimationController(duration: Duration(milliseconds: 3000), vsync: this)
          ..addListener(() => setState(() =>
              rad = _controller.value*pi*2));
    _controller.forward();
    super.initState();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return  Flow(
        delegate: _CircleFlowDelegate(rad),
        children: widget.children,
    );
  }
}

在构造_CircleFlowDelegate时传入角度,在offsetX、offsetY 时加上角度就行了

class _CircleFlowDelegate extends FlowDelegate {
  final double rad;
  _CircleFlowDelegate(this.rad);

  @override //绘制孩子的方法
  void paintChildren(FlowPaintingContext context) {
    double radius = context.size.shortestSide / 2;
    var count = context.childCount;
    var perRad = 2 * pi / count ;
    for (int i = 0; i < count; i++) {
      print(i);
      var cSizeX = context.getChildSize(i).width / 2;
      var cSizeY = context.getChildSize(i).height / 2;
      var offsetX = (radius - cSizeX) * cos(i * perRad+ rad) + radius;
      var offsetY = (radius - cSizeY) * sin(i * perRad+ rad) + radius;
      context.paintChild(i,
          transform: Matrix4.translationValues(
              offsetX - cSizeX, offsetY - cSizeY, 0.0));
    }
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return true;
  }
}

2.圆形布局 + 偏移

能实现出来我还是蛮激动的。定义了menu为中间的组件 children为周围的组件,点击中间组件,执行动画, 在进行定位时,让offsetX和offsetY乘以分率后加半径,这样就会向中心靠拢, 反之扩散,我取名为BurstFlow,意为绽放

class BurstFlow extends StatefulWidget {
  final List children;
  final Widget menu;

  BurstFlow({@required this.children, @required this.menu});

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

class _BurstFlowState extends State
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  double _rad = 0.0;
  bool _closed = true;

  @override
  void initState() {
    _controller =
        AnimationController(duration: Duration(milliseconds: 1000), vsync: this)
          ..addListener(() => setState(() =>    _rad = (_closed ? (_controller.value) :1- _controller.value)))
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              _closed = !_closed;
            }
          });

    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Flow(
      delegate: _CircleFlowDelegate(_rad),
      children: [
        ...widget.children,
        InkWell(
            onTap: () {
              _controller.reset();
              _controller.forward();
            },
            child: widget.menu)
      ],
    );
  }
}

class _CircleFlowDelegate extends FlowDelegate {
  final double rad;
  _CircleFlowDelegate(this.rad);

  @override //绘制孩子的方法
  void paintChildren(FlowPaintingContext context) {
    double radius = context.size.shortestSide / 2;
    var count = context.childCount - 1;
    var perRad = 2 * pi / count;
    for (int i = 0; i < count; i++) {
      print(i);
      var cSizeX = context.getChildSize(i).width / 2;
      var cSizeY = context.getChildSize(i).height / 2;
      var offsetX = rad * (radius - cSizeX) * cos(i * perRad) + radius;
      var offsetY = rad * (radius - cSizeY) * sin(i * perRad) + radius;
      context.paintChild(i,
          transform: Matrix4.translationValues(
              offsetX - cSizeX, offsetY - cSizeY, 0.0));
    }
    context.paintChild(context.childCount - 1,
        transform: Matrix4.translationValues(
            radius - context.getChildSize(context.childCount - 1).width / 2,
            radius - context.getChildSize(context.childCount - 1).height / 2,
            0.0));
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return true;
  }
}

另外可以对周围的组件排布进行设计,可以是半圆弧收方放、 四分之一圆弧收方、甚至是指定角度弧排列 周围的组件也可以进行透明度的渐变,这些都是可以优化的点 这里就不再说了,跟你们一些空间,各位可以自行优化。 布局重在定位,而Flow是定位之王,我的位置我做主。好了,这篇就到这里吧。


尾声

另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,期待与你的交流与切磋。