自定义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实现自定义布局
效果图如下
通过继承 MultiChildRenderObjectWidget 和 RenderBox 这两个 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> {}