Flutter高级玩法Flow位置自定义

目录
  • 前言
  • 第一幕、开场-演员入台
    • 1. 展示舞台
    • 2. Flow出场
    • 3. FlowDelegate出场
    • 4. paintChildren方法和FlowPaintingContext对象
  • 第二幕、排兵布阵
    • 1. paintChild与Matrix4
    • 2. Flow布局的封装
    • 3. 圆形的Flow布局
  • 第三幕、当Flow遇到Animation
    • 1.圆形布局 + 旋转
    • 2.圆形布局 + 偏移

前言

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组件出当主角

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<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 Flow位置自定义的资料请关注我们其它相关文章!

(0)

相关推荐

  • Flutter学习之Navigator的高级用法分享

    目录 简介 named routes 给named route传参数 从Screen返回值 向Screen传值 总结 简介 上篇文章我们讲到了flutter中navigator的基本用法,我们可以使用它的push和pop方法来进行Router之间的跳转. 在flutter中一个Router就是一个widget,但是在Android中,一个Router就是Activity,在IOS中,一个Router是一个ViewController. Router除了之前讲过的push和pop方法之外,还有一些

  • flutter 微信聊天输入框功能实现

    目录 chat_bottom.dart chat_element_other.dart chat_element_self.dart chat_input_box.dart page_chat_person.dart provider_chat_content.dart 高仿微信聊天输入框,效果图如下(目前都是静态展示,服务还没开始开发): 大家如果观察仔细的话 应该会发现,他输入框下面的高度 刚好就是 软键盘的高度:所以在这里就需要监听软键盘的高度.还要配置 resizeToAvoidBott

  • Flutter Component动画的显和隐最佳实践

    目录 动画选择决策树 Implicit Animations——隐式动画 基本使用 使用场景 TweenAnimationBuilder Explicit Animations——显示动画 基本使用 AnimatedWidget AnimatedBuilder 动画选择决策树 Flutter中包含大量的动画组件和自定义动画方式,所以,在合适的场景下选择合适的动画实现方式就成了决定代码质量好坏的一个重要因素. Flutter中的动画从广义上来讲可以分为两类,一类是基于绘制的动画(Drawing-b

  • Flutter 自定义Drawer 滑出位置的大小实例代码详解

    Flutter开发过程中,Drawer控件的使用频率也是比较高的,其实有过移动端开发经验的人来说,Flutter中的Drawer控件就相当于ios开发或者Android开发中的"抽屉"效果,从侧边栏滑出导航菜单.对于Flutter中的Drawer控件的常规用法就不多介绍,网上大把的教程. 那么本篇博文分享一个网上教程不多的一个知识点,那就是自定义Drawer的滑出位置的大小,自定义Drawer滑出位置就需要修改一个double的widthPercent属性,widthPercent一般

  • Flutter使用Android原生播放器详解

    接上篇:播放器-IOS(Swift)篇 安卓端原生播放器的接入思路与ios基本一致,所以本篇就不废话了,直接上代码: 创建插件VideoViewPlugin实现FlutterPlugin: package io.flutter.plugins.videoplayer; import android.util.Log; import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlug

  • 使用Flutter定位包获取地理位置

    目录 Flutter 中获取地理位置 先决条件 使用 Flutter 定位包 设置 位置权限 获取当前位置 使用 Flutter 地理编码包 设置 获取地址 常见的陷阱 结论 Flutter 中获取地理位置 如今,发现用户位置是移动应用程序非常常见且功能强大的用例.如果您曾经尝试过在 Android 中实现位置,您就会知道样例代码会变得多么复杂和混乱. 但这与 Flutter 不同--它有很多令人惊叹的包,可以为您抽象出样板代码,并使实现地理定位成为梦想.另一个好的方面是您可以在 Android

  • Flutter高级玩法Flow位置自定义

    目录 前言 第一幕.开场-演员入台 1. 展示舞台 2. Flow出场 3. FlowDelegate出场 4. paintChildren方法和FlowPaintingContext对象 第二幕.排兵布阵 1. paintChild与Matrix4 2. Flow布局的封装 3. 圆形的Flow布局 第三幕.当Flow遇到Animation 1.圆形布局 + 旋转 2.圆形布局 + 偏移 前言 Flow布局是一个超级强大的布局,但应该很少有人用,因为入手的门槛还是有的 Flow的属性很简单,只

  • Redis高级玩法之利用SortedSet实现多维度排序的方法

    说明:本次实践基于Redis版本3.2.11. 关于SortedSet 首先,我们都知道Redis的SortedSet是可以根据score进行排序的,以手机应用商店的热门榜单排序为例,根据下载量倒序排列,其简单用法如下: 127.0.0.1:6379> zadd TopApp 12000000 wechat (integer) 1 127.0.0.1:6379> zadd TopApp 8000000 taobao 10000000 alipay (integer) 2 127.0.0.1:6

  • 详解PyQt5信号与槽的几种高级玩法

    信号(Signal)和槽(Slot)是Qt中的核心机制,也是在PyQt编程中对象之间进行通信的机制.本文介绍了几种PyQt 5信号与槽的几级玩法. 在Qt中,每一个QObject对象和PyQt中所有继承自QWidget的控件(这些都是QObject的子对象)都支持信号与槽机制.当信号发射时,连接的槽函数将会自动执行.在PyQt 5中信号与槽通过object.signal.connect()方法连接. PyQt的窗口控件类中有很多内置信号,开发者也可以添加自定义信号.信号与槽具有如下特点. 一个信

  • .Net集合排序的一种高级玩法实例教程

    前言 本文主要介绍了关于.Net集合排序的另一种高级玩法,文中通过示例代码介绍的非常详细,需要的朋友可以参考学习,下面话不多说了,来一起看看详细的介绍吧 背景: 学生有名称.学号, 班级有班级名称.班级序号 学校有学校名称.学校编号(序号) 需求  现在需要对学生进行排序 第一排序逻辑 按学校编号(序号)排列 再按班级序号排列 再按学生学号排列 当然,在我们录入数据库信息的时候,有的人可能比较懒,没有录入 学校的序号, 班级的序号,学生的学号 ,怎么办?  那么就Plan B  ! 第二排序逻辑

  • Python一些线程的玩法总结

    目录 一.线程基础以及守护进程 二.线程锁(互斥锁) 三.线程锁(递归锁) 四.死锁 五.队列 六.相关面试题 七.判断数据是否安全 八.进程池 & 线程池 一.线程基础以及守护进程 线程是CPU调度的最小单位 全局解释器锁 全局解释器锁GIL(global interpreter lock) 全局解释器锁的出现主要是为了完成垃圾回收机制的回收机制,对不同线程的引用计数的变化记录的更加精准. 全局解释器锁导致了同一个进程中的多个线程只能有一个线程真正被CPU执行. GIL锁每执行700条指令才会

  • C语言玩转指针之指针的高阶玩法

    目录 前言 一.字符指针 二.指针数组和数组指针 1.指针数组 2.数组指针 2.1.数组指针是什么? 2.2.&数组名和数组名的区别 2.3.数组指针的使用 三.数组参数与指针参数 1.一维数组参数 2.二维数组参数 3.一级指针传参 4.二级指针传参 四.函数指针 五.函数指针数组 六.指向函数指针数组的指针 七.回调函数 总结 前言 指针第一篇,万人浏览: [C语言]玩转指针--关于指针,你需要掌握的基础知识! 指针的主题,我们在初级阶段的<指针>章节已经接触过了,我们知道了指针

  • SpringBoot+kaptcha实现验证码花式玩法详解

    目录 1. 基本用法 2. 自定义验证码文本 在 vhr 项目中,松哥也跟大家讲了验证码的用法,不过那个里边的验证码是我们自己写的,其实功能也还算完整,够用.不过现在各个网站的验证码玩法花样越来越多,加上最近在搞的 TienChin 项目用的验证码是一个老牌开源库 kaptcha,所以松哥决定还是花点时间,跟大家聊聊 kaptcha 的用法,毕竟这个已经有 16 年历史的玩意还在有人用,说明它的功能还是相当强大的. 1. 基本用法 kaptcha 是一个非常老牌的验证码生成工具,多老呢?可以追溯

  • JS SVG获取验证码的玩法示例

    目录 介绍 演示 正文 绘制背景 拉杆绘制 生成条带 数字转动 介绍 之前在抖音上看的某个脑洞大开的产品设想的几种别具特色的后端看了抓狂前端看了想打人的阴间交互效果,其中一个脑洞是让用户拉一下拉杆如同抽奖的形式获取到验证码,本期就咱们就还原出这个交互效果看看它到底有多疯狂. 演示 效果就是这样喵~ Markup <div id="app"> <div class="code-dialog"> <h5>获取验证码</h5>

  • vue新玩法VueUse工具库具体用法@vueuse/core详解

    VueUse官方链接 一.什么是VueUse VueUse不是Vue.use,它是为Vue 2和3服务的一套Vue Composition API的常用工具集,是目前世界上Star最高的同类型库之一.它的初衷就是将一切原本并不支持响应式的JS API变得支持响应式,省去程序员自己写相关代码. VueUse 是一个基于 Composition API 的实用函数集合.通俗的来说,这就是一个工具函数包支持了更好的逻辑分离,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容.以及进

  • 一文详解C#中方法重载的底层玩法

    目录 一:为什么 C 不支持 二:C++ 符号表突破 三:C#如何实现突破 最近在看 C++ 的方法重载,我就在想 C# 中的重载底层是怎么玩的,很多朋友应该知道 C 是不支持重载的,比如下面的代码就会报错. #include <stdio.h> int say() { return 1; } int say(int i) { return i; } int main() { say(10); return 0; } 从错误信息看,它说 say 方法已经存在了,尴尬... 一:为什么 C 不支

随机推荐