引言
自定义Widget是Flutter开发中的高级技能,它允许开发者创建完全定制化的UI组件,满足特殊的设计需求和交互要求。Flutter的Widget系统基于组合模式设计,提供了强大的扩展能力。本文将深入探讨Flutter自定义Widget开发的各个层面,从基础的StatelessWidget和StatefulWidget,到复杂的RenderObject和CustomPainter,帮助开发者掌握创建高质量自定义组件的技能。
Flutter Widget系统架构
Widget树的层次结构
Flutter的UI系统由三个主要的树结构组成:
- Widget树:描述UI的配置信息
- Element树:Widget的实例化对象,管理Widget的生命周期
- RenderObject树:负责实际的布局、绘制和事件处理
// Widget系统基础架构示例
abstract class CustomWidgetBase extends Widget {
const CustomWidgetBase({Key? key}) : super(key: key);
@override
Element createElement();
// Widget的核心方法:描述如何构建UI
Widget build(BuildContext context);
}
// Element的基本结构
abstract class CustomElement extends Element {
CustomElement(Widget widget) : super(widget);
@override
Widget get widget => super.widget as CustomWidgetBase;
@override
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
// Element挂载时的初始化逻辑
}
@override
void update(Widget newWidget) {
super.update(newWidget);
// Widget更新时的处理逻辑
}
@override
void unmount() {
// Element卸载时的清理逻辑
super.unmount();
}
}
// RenderObject的基本结构
abstract class CustomRenderObject extends RenderBox {
@override
void performLayout() {
// 执行布局计算
}
@override
void paint(PaintingContext context, Offset offset) {
// 执行绘制操作
}
@override
bool hitTestSelf(Offset position) {
// 处理点击测试
return true;
}
}
Widget生命周期深入理解
// 完整的Widget生命周期演示
class LifecycleWidget extends StatefulWidget {
final String title;
final VoidCallback? onDispose;
const LifecycleWidget({
Key? key,
required this.title,
this.onDispose,
}) : super(key: key);
@override
_LifecycleWidgetState createState() {
print('LifecycleWidget: createState called');
return _LifecycleWidgetState();
}
}
class _LifecycleWidgetState extends State<LifecycleWidget>
with WidgetsBindingObserver, TickerProviderStateMixin {
late AnimationController _animationController;
int _counter = 0;
@override
void initState() {
super.initState();
print('LifecycleWidget: initState called');
// 注册应用生命周期观察者
WidgetsBinding.instance.addObserver(this);
// 初始化动画控制器
_animationController = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
// 延迟执行初始化任务
WidgetsBinding.instance.addPostFrameCallback((_) {
print('LifecycleWidget: First frame rendered');
_animationController.forward();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('LifecycleWidget: didChangeDependencies called');
// 依赖变化时的处理逻辑
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
print('Current theme brightness: ${theme.brightness}');
print('Screen size: ${mediaQuery.size}');
}
@override
void didUpdateWidget(LifecycleWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print('LifecycleWidget: didUpdateWidget called');
print('Old title: ${oldWidget.title}, New title: ${widget.title}');
// Widget更新时的处理逻辑
if (oldWidget.title != widget.title) {
// 标题变化时重新启动动画
_animationController.reset();
_animationController.forward();
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('LifecycleWidget: App lifecycle state changed to $state');
switch (state) {
case AppLifecycleState.resumed:
_animationController.forward();
break;
case AppLifecycleState.paused:
_animationController.stop();
break;
case AppLifecycleState.inactive:
_animationController.stop();
break;
case AppLifecycleState.detached:
break;
}
}
@override
Widget build(BuildContext context) {
print('LifecycleWidget: build called');
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Opacity(
opacity: _animationController.value,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 20),
Text(
'Counter: $_counter',
style: Theme.of(context).textTheme.bodyLarge,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: Text('Increment'),
),
],
),
);
},
);
}
@override
void deactivate() {
print('LifecycleWidget: deactivate called');
super.deactivate();
}
@override
void dispose() {
print('LifecycleWidget: dispose called');
// 清理资源
WidgetsBinding.instance.removeObserver(this);
_animationController.dispose();
// 调用回调
widget.onDispose?.call();
super.dispose();
}
}
基础自定义Widget开发
StatelessWidget自定义组件
// 自定义卡片组件
class CustomCard extends StatelessWidget {
final Widget child;
final Color? backgroundColor;
final double? elevation;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
final VoidCallback? onTap;
final bool isSelected;
final Color? selectedColor;
const CustomCard({
Key? key,
required this.child,
this.backgroundColor,
this.elevation,
this.padding,
this.borderRadius,
this.onTap,
this.isSelected = false,
this.selectedColor,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final effectiveBackgroundColor = backgroundColor ?? theme.cardColor;
final effectiveSelectedColor = selectedColor ?? theme.primaryColor.withOpacity(0.1);
final effectiveBorderRadius = borderRadius ?? BorderRadius.circular(8.0);
final effectivePadding = padding ?? EdgeInsets.all(16.0);
Widget cardContent = Container(
padding: effectivePadding,
decoration: BoxDecoration(
color: isSelected ? effectiveSelectedColor : effectiveBackgroundColor,
borderRadius: effectiveBorderRadius,
border: isSelected
? Border.all(color: theme.primaryColor, width: 2.0)
: null,
boxShadow: elevation != null
? [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: elevation!,
offset: Offset(0, elevation! / 2),
),
]
: null,
),
child: child,
);
if (onTap != null) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: effectiveBorderRadius,
child: cardContent,
),
);
}
return cardContent;
}
}
// 自定义按钮组件
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final Color? backgroundColor;
final Color? textColor;
final double? fontSize;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
final bool isLoading;
final Widget? icon;
final ButtonSize size;
const CustomButton({
Key? key,
required this.text,
this.onPressed,
this.backgroundColor,
this.textColor,
this.fontSize,
this.padding,
this.borderRadius,
this.isLoading = false,
this.icon,
this.size = ButtonSize.medium,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isEnabled = onPressed != null && !isLoading;
// 根据尺寸计算样式
final buttonStyle = _getButtonStyle(theme);
Widget buttonChild;
if (isLoading) {
buttonChild = SizedBox(
width: buttonStyle.iconSize,
height: buttonStyle.iconSize,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: AlwaysStoppedAnimation<Color>(
textColor ?? theme.primaryTextTheme.labelLarge!.color!,
),
),
);
} else if (icon != null) {
buttonChild = Row(
mainAxisSize: MainAxisSize.min,
children: [
icon!,
SizedBox(width: 8),
Text(
text,
style: TextStyle(
fontSize: buttonStyle.fontSize,
fontWeight: FontWeight.w600,
color: textColor ?? theme.primaryTextTheme.labelLarge!.color,
),
),
],
);
} else {
buttonChild = Text(
text,
style: TextStyle(
fontSize: buttonStyle.fontSize,
fontWeight: FontWeight.w600,
color: textColor ?? theme.primaryTextTheme.labelLarge!.color,
),
);
}
return Material(
color: Colors.transparent,
child: InkWell(
onTap: isEnabled ? onPressed : null,
borderRadius: borderRadius ?? BorderRadius.circular(buttonStyle.borderRadius),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
padding: padding ?? buttonStyle.padding,
decoration: BoxDecoration(
color: isEnabled
? (backgroundColor ?? theme.primaryColor)
: (backgroundColor ?? theme.primaryColor).withOpacity(0.5),
borderRadius: borderRadius ?? BorderRadius.circular(buttonStyle.borderRadius),
),
child: buttonChild,
),
),
);
}
_ButtonStyle _getButtonStyle(ThemeData theme) {
switch (size) {
case ButtonSize.small:
return _ButtonStyle(
fontSize: 12.0,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
borderRadius: 4.0,
iconSize: 16.0,
);
case ButtonSize.medium:
return _ButtonStyle(
fontSize: 14.0,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
borderRadius: 6.0,
iconSize: 18.0,
);
case ButtonSize.large:
return _ButtonStyle(
fontSize: 16.0,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 14),
borderRadius: 8.0,
iconSize: 20.0,
);
}
}
}
enum ButtonSize { small, medium, large }
class _ButtonStyle {
final double fontSize;
final EdgeInsetsGeometry padding;
final double borderRadius;
final double iconSize;
_ButtonStyle({
required this.fontSize,
required this.padding,
required this.borderRadius,
required this.iconSize,
});
}
StatefulWidget高级自定义组件
// 自定义滑动选择器
class CustomSlider extends StatefulWidget {
final double value;
final double min;
final double max;
final int? divisions;
final ValueChanged<double>? onChanged;
final ValueChanged<double>? onChangeStart;
final ValueChanged<double>? onChangeEnd;
final Color? activeColor;
final Color? inactiveColor;
final Color? thumbColor;
final double thumbRadius;
final double trackHeight;
final bool showLabel;
final String Function(double)? labelFormatter;
const CustomSlider({
Key? key,
required this.value,
this.min = 0.0,
this.max = 1.0,
this.divisions,
this.onChanged,
this.onChangeStart,
this.onChangeEnd,
this.activeColor,
this.inactiveColor,
this.thumbColor,
this.thumbRadius = 12.0,
this.trackHeight = 4.0,
this.showLabel = false,
this.labelFormatter,
}) : super(key: key);
@override
_CustomSliderState createState() => _CustomSliderState();
}
class _CustomSliderState extends State<CustomSlider>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
bool _isDragging = false;
double _currentValue = 0.0;
@override
void initState() {
super.initState();
_currentValue = widget.value;
_animationController = AnimationController(
duration: Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 1.2,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void didUpdateWidget(CustomSlider oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value && !_isDragging) {
_currentValue = widget.value;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final effectiveActiveColor = widget.activeColor ?? theme.primaryColor;
final effectiveInactiveColor = widget.inactiveColor ?? theme.disabledColor;
final effectiveThumbColor = widget.thumbColor ?? theme.primaryColor;
return GestureDetector(
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
child: Container(
height: widget.thumbRadius * 2 + 20,
child: CustomPaint(
painter: _SliderPainter(
value: _currentValue,
min: widget.min,
max: widget.max,
activeColor: effectiveActiveColor,
inactiveColor: effectiveInactiveColor,
thumbColor: effectiveThumbColor,
thumbRadius: widget.thumbRadius,
trackHeight: widget.trackHeight,
thumbScale: _scaleAnimation.value,
showLabel: widget.showLabel,
labelText: widget.labelFormatter?.call(_currentValue) ??
_currentValue.toStringAsFixed(1),
),
size: Size.infinite,
),
),
);
}
void _handlePanStart(DragStartDetails details) {
if (widget.onChanged == null) return;
_isDragging = true;
_animationController.forward();
widget.onChangeStart?.call(_currentValue);
}
void _handlePanUpdate(DragUpdateDetails details) {
if (widget.onChanged == null) return;
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final localPosition = renderBox.globalToLocal(details.globalPosition);
final trackWidth = renderBox.size.width - widget.thumbRadius * 2;
final trackLeft = widget.thumbRadius;
double progress = (localPosition.dx - trackLeft) / trackWidth;
progress = progress.clamp(0.0, 1.0);
double newValue = widget.min + progress * (widget.max - widget.min);
if (widget.divisions != null) {
final step = (widget.max - widget.min) / widget.divisions!;
newValue = (newValue / step).round() * step + widget.min;
}
newValue = newValue.clamp(widget.min, widget.max);
if (newValue != _currentValue) {
setState(() {
_currentValue = newValue;
});
widget.onChanged!(newValue);
}
}
void _handlePanEnd(DragEndDetails details) {
if (widget.onChanged == null) return;
_isDragging = false;
_animationController.reverse();
widget.onChangeEnd?.call(_currentValue);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
// 自定义滑动条绘制器
class _SliderPainter extends CustomPainter {
final double value;
final double min;
final double max;
final Color activeColor;
final Color inactiveColor;
final Color thumbColor;
final double thumbRadius;
final double trackHeight;
final double thumbScale;
final bool showLabel;
final String labelText;
_SliderPainter({
required this.value,
required this.min,
required this.max,
required this.activeColor,
required this.inactiveColor,
required this.thumbColor,
required this.thumbRadius,
required this.trackHeight,
required this.thumbScale,
required this.showLabel,
required this.labelText,
});
@override
void paint(Canvas canvas, Size size) {
final trackWidth = size.width - thumbRadius * 2;
final trackLeft = thumbRadius;
final trackTop = (size.height - trackHeight) / 2;
// 绘制轨道
final trackRect = RRect.fromRectAndRadius(
Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight),
Radius.circular(trackHeight / 2),
);
final trackPaint = Paint()
..color = inactiveColor
..style = PaintingStyle.fill;
canvas.drawRRect(trackRect, trackPaint);
// 绘制活动轨道
final progress = (value - min) / (max - min);
final activeWidth = trackWidth * progress;
if (activeWidth > 0) {
final activeRect = RRect.fromRectAndRadius(
Rect.fromLTWH(trackLeft, trackTop, activeWidth, trackHeight),
Radius.circular(trackHeight / 2),
);
final activePaint = Paint()
..color = activeColor
..style = PaintingStyle.fill;
canvas.drawRRect(activeRect, activePaint);
}
// 绘制滑块
final thumbCenter = Offset(
trackLeft + activeWidth,
size.height / 2,
);
final thumbPaint = Paint()
..color = thumbColor
..style = PaintingStyle.fill;
canvas.drawCircle(
thumbCenter,
thumbRadius * thumbScale,
thumbPaint,
);
// 绘制滑块边框
final thumbBorderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawCircle(
thumbCenter,
thumbRadius * thumbScale,
thumbBorderPaint,
);
// 绘制标签
if (showLabel && thumbScale > 1.0) {
_drawLabel(canvas, thumbCenter, labelText);
}
}
void _drawLabel(Canvas canvas, Offset thumbCenter, String text) {
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
final labelWidth = textPainter.width + 16;
final labelHeight = textPainter.height + 8;
final labelTop = thumbCenter.dy - thumbRadius * thumbScale - labelHeight - 8;
final labelLeft = thumbCenter.dx - labelWidth / 2;
// 绘制标签背景
final labelRect = RRect.fromRectAndRadius(
Rect.fromLTWH(labelLeft, labelTop, labelWidth, labelHeight),
Radius.circular(4),
);
final labelPaint = Paint()
..color = thumbColor
..style = PaintingStyle.fill;
canvas.drawRRect(labelRect, labelPaint);
// 绘制标签文本
textPainter.paint(
canvas,
Offset(
labelLeft + (labelWidth - textPainter.width) / 2,
labelTop + (labelHeight - textPainter.height) / 2,
),
);
}
@override
bool shouldRepaint(_SliderPainter oldDelegate) {
return oldDelegate.value != value ||
oldDelegate.thumbScale != thumbScale ||
oldDelegate.showLabel != showLabel ||
oldDelegate.labelText != labelText;
}
}
CustomPainter深度应用
复杂图形绘制
// 自定义图表组件
class CustomChart extends StatefulWidget {
final List<ChartData> data;
final Color? primaryColor;
final Color? secondaryColor;
final bool showGrid;
final bool showLabels;
final bool animated;
const CustomChart({
Key? key,
required this.data,
this.primaryColor,
this.secondaryColor,
this.showGrid = true,
this.showLabels = true,
this.animated = true,
}) : super(key: key);
@override
_CustomChartState createState() => _CustomChartState();
}
class _CustomChartState extends State<CustomChart>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOutCubic,
);
if (widget.animated) {
_animationController.forward();
}
}
@override
void didUpdateWidget(CustomChart oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data && widget.animated) {
_animationController.reset();
_animationController.forward();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: _ChartPainter(
data: widget.data,
primaryColor: widget.primaryColor ?? theme.primaryColor,
secondaryColor: widget.secondaryColor ?? theme.accentColor,
showGrid: widget.showGrid,
showLabels: widget.showLabels,
animationProgress: widget.animated ? _animation.value : 1.0,
textStyle: theme.textTheme.bodySmall!,
),
size: Size.infinite,
);
},
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
class _ChartPainter extends CustomPainter {
final List<ChartData> data;
final Color primaryColor;
final Color secondaryColor;
final bool showGrid;
final bool showLabels;
final double animationProgress;
final TextStyle textStyle;
_ChartPainter({
required this.data,
required this.primaryColor,
required this.secondaryColor,
required this.showGrid,
required this.showLabels,
required this.animationProgress,
required this.textStyle,
});
@override
void paint(Canvas canvas, Size size) {
if (data.isEmpty) return;
final padding = EdgeInsets.all(40);
final chartRect = Rect.fromLTRB(
padding.left,
padding.top,
size.width - padding.right,
size.height - padding.bottom,
);
// 计算数据范围
final maxValue = data.map((d) => d.value).reduce((a, b) => a > b ? a : b);
final minValue = data.map((d) => d.value).reduce((a, b) => a < b ? a : b);
final valueRange = maxValue - minValue;
// 绘制网格
if (showGrid) {
_drawGrid(canvas, chartRect, maxValue, minValue);
}
// 绘制数据线
_drawDataLine(canvas, chartRect, maxValue, minValue);
// 绘制数据点
_drawDataPoints(canvas, chartRect, maxValue, minValue);
// 绘制标签
if (showLabels) {
_drawLabels(canvas, chartRect, maxValue, minValue);
}
}
void _drawGrid(Canvas canvas, Rect chartRect, double maxValue, double minValue) {
final gridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0;
// 绘制水平网格线
for (int i = 0; i <= 5; i++) {
final y = chartRect.top + (chartRect.height / 5) * i;
canvas.drawLine(
Offset(chartRect.left, y),
Offset(chartRect.right, y),
gridPaint,
);
}
// 绘制垂直网格线
final stepX = chartRect.width / (data.length - 1);
for (int i = 0; i < data.length; i++) {
final x = chartRect.left + stepX * i;
canvas.drawLine(
Offset(x, chartRect.top),
Offset(x, chartRect.bottom),
gridPaint,
);
}
}
void _drawDataLine(Canvas canvas, Rect chartRect, double maxValue, double minValue) {
if (data.length < 2) return;
final linePaint = Paint()
..color = primaryColor
..strokeWidth = 3.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
final path = Path();
final stepX = chartRect.width / (data.length - 1);
for (int i = 0; i < data.length; i++) {
final x = chartRect.left + stepX * i;
final normalizedValue = (data[i].value - minValue) / (maxValue - minValue);
final y = chartRect.bottom - (chartRect.height * normalizedValue * animationProgress);
if (i == 0) {
path.moveTo(x, y);
} else {
// 使用贝塞尔曲线创建平滑的线条
final prevX = chartRect.left + stepX * (i - 1);
final prevNormalizedValue = (data[i - 1].value - minValue) / (maxValue - minValue);
final prevY = chartRect.bottom - (chartRect.height * prevNormalizedValue * animationProgress);
final controlPoint1X = prevX + (x - prevX) * 0.5;
final controlPoint1Y = prevY;
final controlPoint2X = prevX + (x - prevX) * 0.5;
final controlPoint2Y = y;
path.cubicTo(controlPoint1X, controlPoint1Y, controlPoint2X, controlPoint2Y, x, y);
}
}
canvas.drawPath(path, linePaint);
// 绘制渐变填充
final gradientPath = Path.from(path);
gradientPath.lineTo(chartRect.right, chartRect.bottom);
gradientPath.lineTo(chartRect.left, chartRect.bottom);
gradientPath.close();
final gradientPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
primaryColor.withOpacity(0.3),
primaryColor.withOpacity(0.0),
],
).createShader(chartRect);
canvas.drawPath(gradientPath, gradientPaint);
}
void _drawDataPoints(Canvas canvas, Rect chartRect, double maxValue, double minValue) {
final pointPaint = Paint()
..color = primaryColor
..style = PaintingStyle.fill;
final pointBorderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
final stepX = chartRect.width / (data.length - 1);
for (int i = 0; i < data.length; i++) {
final x = chartRect.left + stepX * i;
final normalizedValue = (data[i].value - minValue) / (maxValue - minValue);
final y = chartRect.bottom - (chartRect.height * normalizedValue * animationProgress);
final pointRadius = 6.0 * animationProgress;
canvas.drawCircle(Offset(x, y), pointRadius, pointPaint);
canvas.drawCircle(Offset(x, y), pointRadius, pointBorderPaint);
}
}
void _drawLabels(Canvas canvas, Rect chartRect, double maxValue, double minValue) {
final stepX = chartRect.width / (data.length - 1);
for (int i = 0; i < data.length; i++) {
final x = chartRect.left + stepX * i;
// 绘制X轴标签
final labelPainter = TextPainter(
text: TextSpan(
text: data[i].label,
style: textStyle,
),
textDirection: TextDirection.ltr,
);
labelPainter.layout();
labelPainter.paint(
canvas,
Offset(
x - labelPainter.width / 2,
chartRect.bottom + 10,
),
);
}
// 绘制Y轴标签
for (int i = 0; i <= 5; i++) {
final value = minValue + (maxValue - minValue) * (1 - i / 5);
final y = chartRect.top + (chartRect.height / 5) * i;
final valuePainter = TextPainter(
text: TextSpan(
text: value.toStringAsFixed(1),
style: textStyle,
),
textDirection: TextDirection.ltr,
);
valuePainter.layout();
valuePainter.paint(
canvas,
Offset(
chartRect.left - valuePainter.width - 10,
y - valuePainter.height / 2,
),
);
}
}
@override
bool shouldRepaint(_ChartPainter oldDelegate) {
return oldDelegate.data != data ||
oldDelegate.animationProgress != animationProgress ||
oldDelegate.primaryColor != primaryColor ||
oldDelegate.secondaryColor != secondaryColor ||
oldDelegate.showGrid != showGrid ||
oldDelegate.showLabels != showLabels;
}
}
class ChartData {
final String label;
final double value;
ChartData({
required this.label,
required this.value,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ChartData &&
other.label == label &&
other.value == value;
}
@override
int get hashCode => label.hashCode ^ value.hashCode;
}
RenderObject自定义渲染
自定义布局算法
// 自定义流式布局Widget
class CustomFlowLayout extends MultiChildRenderObjectWidget {
final double spacing;
final double runSpacing;
final WrapAlignment alignment;
final WrapCrossAlignment crossAxisAlignment;
final Axis direction;
CustomFlowLayout({
Key? key,
required List<Widget> children,
this.spacing = 0.0,
this.runSpacing = 0.0,
this.alignment = WrapAlignment.start,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.direction = Axis.horizontal,
}) : super(key: key, children: children);
@override
RenderCustomFlowLayout createRenderObject(BuildContext context) {
return RenderCustomFlowLayout(
spacing: spacing,
runSpacing: runSpacing,
alignment: alignment,
crossAxisAlignment: crossAxisAlignment,
direction: direction,
);
}
@override
void updateRenderObject(BuildContext context, RenderCustomFlowLayout renderObject) {
renderObject
..spacing = spacing
..runSpacing = runSpacing
..alignment = alignment
..crossAxisAlignment = crossAxisAlignment
..direction = direction;
}
}
class RenderCustomFlowLayout extends RenderBox
with ContainerRenderObjectMixin<RenderBox, FlowLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, FlowLayoutParentData> {
RenderCustomFlowLayout({
required double spacing,
required double runSpacing,
required WrapAlignment alignment,
required WrapCrossAlignment crossAxisAlignment,
required Axis direction,
}) : _spacing = spacing,
_runSpacing = runSpacing,
_alignment = alignment,
_crossAxisAlignment = crossAxisAlignment,
_direction = direction;
double _spacing;
double get spacing => _spacing;
set spacing(double value) {
if (_spacing != value) {
_spacing = value;
markNeedsLayout();
}
}
double _runSpacing;
double get runSpacing => _runSpacing;
set runSpacing(double value) {
if (_runSpacing != value) {
_runSpacing = value;
markNeedsLayout();
}
}
WrapAlignment _alignment;
WrapAlignment get alignment => _alignment;
set alignment(WrapAlignment value) {
if (_alignment != value) {
_alignment = value;
markNeedsLayout();
}
}
WrapCrossAlignment _crossAxisAlignment;
WrapCrossAlignment get crossAxisAlignment => _crossAxisAlignment;
set crossAxisAlignment(WrapCrossAlignment value) {
if (_crossAxisAlignment != value) {
_crossAxisAlignment = value;
markNeedsLayout();
}
}
Axis _direction;
Axis get direction => _direction;
set direction(Axis value) {
if (_direction != value) {
_direction = value;
markNeedsLayout();
}
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! FlowLayoutParentData) {
child.parentData = FlowLayoutParentData();
}
}
@override
void performLayout() {
if (childCount == 0) {
size = constraints.smallest;
return;
}
final isHorizontal = direction == Axis.horizontal;
final mainAxisLimit = isHorizontal ? constraints.maxWidth : constraints.maxHeight;
final crossAxisLimit = isHorizontal ? constraints.maxHeight : constraints.maxWidth;
final runs = <_RunMetrics>[];
double mainAxisExtent = 0.0;
double crossAxisExtent = 0.0;
// 第一遍布局:计算每个run的尺寸
RenderBox? child = firstChild;
double runMainAxisExtent = 0.0;
double runCrossAxisExtent = 0.0;
final runChildren = <RenderBox>[];
while (child != null) {
child.layout(BoxConstraints(
maxWidth: isHorizontal ? double.infinity : crossAxisLimit,
maxHeight: isHorizontal ? crossAxisLimit : double.infinity,
), parentUsesSize: true);
final childMainAxisExtent = isHorizontal ? child.size.width : child.size.height;
final childCrossAxisExtent = isHorizontal ? child.size.height : child.size.width;
// 检查是否需要换行
final wouldExceedLimit = runMainAxisExtent + childMainAxisExtent > mainAxisLimit;
if (wouldExceedLimit && runChildren.isNotEmpty) {
// 完成当前run
runs.add(_RunMetrics(
mainAxisExtent: runMainAxisExtent - spacing,
crossAxisExtent: runCrossAxisExtent,
children: List.from(runChildren),
));
mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent - spacing);
crossAxisExtent += runCrossAxisExtent + (runs.length > 1 ? runSpacing : 0);
// 开始新的run
runChildren.clear();
runMainAxisExtent = 0.0;
runCrossAxisExtent = 0.0;
}
runChildren.add(child);
runMainAxisExtent += childMainAxisExtent + spacing;
runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent);
child = childAfter(child);
}
// 完成最后一个run
if (runChildren.isNotEmpty) {
runs.add(_RunMetrics(
mainAxisExtent: runMainAxisExtent - spacing,
crossAxisExtent: runCrossAxisExtent,
children: runChildren,
));
mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent - spacing);
crossAxisExtent += runCrossAxisExtent;
}
// 设置容器尺寸
size = isHorizontal
? Size(mainAxisExtent, crossAxisExtent)
: Size(crossAxisExtent, mainAxisExtent);
// 第二遍布局:定位子元素
double runCrossAxisOffset = 0.0;
for (final run in runs) {
double runMainAxisOffset = _getRunMainAxisOffset(run.mainAxisExtent, mainAxisExtent);
for (final child in run.children) {
final childParentData = child.parentData as FlowLayoutParentData;
final childMainAxisExtent = isHorizontal ? child.size.width : child.size.height;
final childCrossAxisExtent = isHorizontal ? child.size.height : child.size.width;
final childCrossAxisOffset = runCrossAxisOffset +
_getChildCrossAxisOffset(childCrossAxisExtent, run.crossAxisExtent);
childParentData.offset = isHorizontal
? Offset(runMainAxisOffset, childCrossAxisOffset)
: Offset(childCrossAxisOffset, runMainAxisOffset);
runMainAxisOffset += childMainAxisExtent + spacing;
}
runCrossAxisOffset += run.crossAxisExtent + runSpacing;
}
}
double _getRunMainAxisOffset(double runExtent, double containerExtent) {
switch (alignment) {
case WrapAlignment.start:
return 0.0;
case WrapAlignment.center:
return (containerExtent - runExtent) / 2.0;
case WrapAlignment.end:
return containerExtent - runExtent;
case WrapAlignment.spaceBetween:
return 0.0; // 在实际实现中需要特殊处理
case WrapAlignment.spaceAround:
return 0.0; // 在实际实现中需要特殊处理
case WrapAlignment.spaceEvenly:
return 0.0; // 在实际实现中需要特殊处理
}
}
double _getChildCrossAxisOffset(double childExtent, double runExtent) {
switch (crossAxisAlignment) {
case WrapCrossAlignment.start:
return 0.0;
case WrapCrossAlignment.center:
return (runExtent - childExtent) / 2.0;
case WrapCrossAlignment.end:
return runExtent - childExtent;
}
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
class FlowLayoutParentData extends ContainerBoxParentData<RenderBox> {}
class _RunMetrics {
final double mainAxisExtent;
final double crossAxisExtent;
final List<RenderBox> children;
_RunMetrics({
required this.mainAxisExtent,
required this.crossAxisExtent,
required this.children,
});
}
总结
Flutter自定义Widget开发是一个深入且富有挑战性的领域,它要求开发者对Flutter的渲染机制有深入的理解。通过掌握从基础的StatelessWidget和StatefulWidget,到高级的CustomPainter和RenderObject,开发者可以创建出功能强大、性能优异的自定义组件。
自定义Widget开发的关键要点包括:
- 理解Widget生命周期:正确管理资源和状态
- 合理使用CustomPainter:实现复杂的绘制需求
- 深入RenderObject:创建自定义布局算法
- 性能优化:避免不必要的重建和重绘
- 可复用性设计:创建灵活且易于使用的API
随着Flutter生态系统的不断发展,自定义Widget的开发技术也在持续演进。开发者应该保持学习,关注最新的最佳实践,为Flutter应用创造更加丰富和独特的用户体验。
上一篇 下一篇