Flutter 自定义widget

1,382 阅读2分钟

自定义widget

组合widget

组合的思想始终贯穿在框架设计之中,Flutter 更推荐使用组合的方式满足你的需求,这也是 Flutter 提供了如此丰富的控件库的原因之一;例如Container、Scaffold等都是通过组合不同功能的widget来实现的

自定义Widget

但对于一些不规则的视图,Flutter 提供的现有 Widget 组合可能无法实现,Flutter 也提供了自定义widget的方式

方式描述
CustomPaint自定义画布
CustomSingleChildLayout自定义单 child 布局
SingleChildRenderObjectWidget自定义单 child 布局
CustomMultiChildLayout自定义多 child 布局
MultiChildRenderObjectWidget自定义多child布局
Flow指定child位置

MultiChildRenderObjectWidget

通过MultiChildRenderObjectWidget实现自定义布局

效果图如下

通过继承 MultiChildRenderObjectWidgetRenderBox 这两个 abstract 类来实现, MultiChildRenderObjectElement 则负责关联起它们, 除了此之外,还有有几个关键的类 ContainerRenderObjectMixin 、RenderBoxContainerDefaultsMixin、ContainerBoxParentData

ContainerRenderObjectMixin 主要是维护提供了一个双链表的 children RenderObject

RenderBoxContainerDefaultsMixin 对children 提供常用的默认行为和管理

ContainerBoxParentData 是 BoxParentData 的子类,绘制时所需的位置类。

代码

import 'dart:async';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;

class CustomMultiWidgetPage extends StatefulWidget {
  @override
  _CustomMultiWidgetState createState() => _CustomMultiWidgetState();
}

class _CustomMultiWidgetState extends State<CustomMultiWidgetPage> {
  bool start = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("customMultiWidget"),
      ),
      body: Center(
        child: Container(
          child: AspectRatio(
            aspectRatio: 1,
            child: CustomMultiWidget(
              startAnimation: start,
              children: [
                ItemWidget(
                  color: Colors.red,
                ),
                ItemWidget(
                  color: Colors.blue,
                ),
                ItemWidget(
                  color: Colors.yellow,
                ),
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            start = !start;
          });
        },
        child: Icon(Icons.track_changes),
      ),
    );
  }
}

class ItemWidget extends StatelessWidget {
  final Color color;

  const ItemWidget({Key key, this.color}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: MediaQuery.of(context).size.width / 2,
      height: MediaQuery.of(context).size.width / 2,
      decoration: BoxDecoration(
          color: color.withOpacity(.4),
          borderRadius: BorderRadius.all(
              Radius.circular(MediaQuery.of(context).size.width / 4))),
    );
  }
}

class CustomMultiWidget extends MultiChildRenderObjectWidget {
  /// 是否有动画效果
  bool startAnimation;

  CustomMultiWidget({
    Key key,
    this.startAnimation = false,
    List<Widget> children = const <Widget>[],
  }) : super(key: key, children: children);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return MyRenderObject(startAnimation: startAnimation);
  }

  @override
  void updateRenderObject(BuildContext context, MyRenderObject renderObject) {
    renderObject.startAnimation = startAnimation;
  }
}

class MyRenderObject extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, MyParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, MyParentData> {
  /// 刷新频率
  double FPS = 1000 / 60;

  /// 刷新后角度增加
  double increase = 0;

  bool _startAnimation;

  ///创建一个新的重复计时器;
  Timer _timer;

  MyRenderObject({List<RenderBox> children, bool startAnimation})
      : _startAnimation = startAnimation {
    addAll(children);
  }

  set startAnimation(bool value) {
    if (_startAnimation != value) {
      _startAnimation = value;
      if (_startAnimation) {
        if (_timer == null || !_timer.isActive) {
          _timer = Timer.periodic(Duration(milliseconds: FPS.toInt()), (timer) {
            increase += 1;
            markNeedsLayout();
          });
        }
      } else {
        if (_timer != null && _timer.isActive) {
          _timer.cancel();
        }
      }
    }
  }

  @override
  void setupParentData(RenderObject child) {
    /// 设置parentData
    if (child.parentData is! MyParentData) child.parentData = MyParentData();
  }

  @override
  void performLayout() {
    /// 设置size,因为父widget是AspectRatio,宽高比为1,相等;
    size = constraints.biggest;

    /// 圆心绕这个半径的圆运动
    double radius = 48;

    /// 中心点位置
    final double centerX = size.width / 2;
    final double centerY = size.height / 2;

    /// 每个widget,间隔度数
    final double interval = 360 / childCount;
    var childrenAsList = getChildrenAsList();
    for (int i = 0; i < childrenAsList.length; i++) {
      RenderBox child = childrenAsList[i];
      child.layout(constraints.loosen(), parentUsesSize: true);
      final MyParentData childParentData = child.parentData as MyParentData;
      double hd;
      if (i == 0 || i == 1) {
        hd = (math.pi / 180) * (interval * i + 30 + increase);
      } else if (i == 2) {
        hd = (math.pi / 180) * (interval * i + 30 - increase * 1.5);
      }

      /// 根据弧度得到偏移offset
      double dx = centerX + math.cos(hd) * radius - child.size.width / 2;
      double dy = centerY - math.sin(hd) * radius - child.size.height / 2;
      childParentData.offset = Offset(dx, dy);
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

  @override
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

  @override
  bool hitTestChildren(HitTestResult result, {Offset position}) {
    return defaultHitTestChildren(result, position: position);
  }

  @override
  void detach() {
    super.detach();
    if (_timer != null) {
      _timer.cancel();
      _timer == null;
    }
  }
}

class MyParentData extends ContainerBoxParentData<RenderBox> {}