【Flutter高級玩法- Flow 】個人位置我作主

零、前言

Flow佈局是一個超級強大的佈局,但應該不多有人用,由於入手的門檻仍是有的
Flow的屬性很簡單,只有FlowDelegate類型的delegate和組件列表children,
可能不少人看到delegate就揮揮手:臣妾作不到,今天就來掰扯一下這個FlowDelegate.編程

class Flow extends MultiChildRenderObjectWidget {
  Flow({
    Key key,
    @required this.delegate,
    List<Widget> children = const <Widget>[],
  }) : assert(delegate != null),
複製代碼
- -

第一幕、開場-演員入臺

1. 展現舞臺

咱們的第一個舞臺是一個200*200的灰色box,由FlowDemo組件出當主角bash

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,只有一條路:自定義app

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、paintChildless

---->[源碼: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 });
}
複製代碼

接下來用代碼測試一下這幾個屬性看看,不出所料
默認是繪製在父容器的左上角。ide

class _Delegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    print("父容器尺寸:${context.size}");
    print("孩子個數:${context.childCount}");
    for(int i=0;i<context.childCount;i++){
      print("第$i個孩子尺寸:${context.getChildSize(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<Widget> 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<Widget> 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<Widget> children;
  CircleFlow({@required this.children});

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

class _CircleFlowState extends State<CircleFlow>
    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<Widget> children;
  final Widget menu;

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

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

class _BurstFlowState extends State<BurstFlow>
    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的問題,期待與你的交流與切磋。

@張風捷特烈 2019.03.13 未允禁轉
個人公衆號:編程之王
聯繫我--郵箱:1981462002@qq.com --微信:zdl1994328
~ END ~

相關文章
相關標籤/搜索