CustomPaint用途
CustomPaint是flutter提供实现自定义view的类。作为SingleChildRenderObjectWidget的子类最多可以有一个子widget。
const CustomPaint({
Key? key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget? child,
}) : assert(size != null),
assert(isComplex != null),
assert(willChange != null),
assert(painter != null || foregroundPainter != null || (!isComplex && !willChange)),
super(key: key, child: child);
通过构造方法可以看到CustomPaint可以设置背景和前景的绘制,可以通过size设置大小,通过isComplex开启复杂界面缓存的模式。willChange是和isComplex配合使用的,当启用缓存时,该属性代表在下一帧中绘制是否会改变。
其中我们要关注的是 'painter' ,这里是实现view渲染部分的部分。
painter 的类型是CustomPainter ,是一个抽象类,提供了一些接口用于view的绘画渲染。我们要实现的也是这个类,通过实现CustomPainter后让底层的实现类替我们画出完整的view。
在画条形图中,我们需要关注和实现的只有两个方法
- void paint(Canvas canvas, Size size)
- bool shouldRepaint(CustomPainter oldDelegate)
paint 方法决定我们绘制什么 shouldRepaint的返回值决定这个view是否会被重新绘制。
绘制view的思路
view的拆分
把view拆成这几个部分有助于我们理解和构思整个view。
这个日期统计条形图的绘制总体分为这几个部分。
- 柱形指示条
- 白灰色相间背景
- 右侧组限的文言
- 上方的跳出的popup
- 底下组的文言和虚线
view的绘画
- 柱形指示条
主要要用canvas.drawRRect 来画出带有圆角的柱形
void drawRRect(RRect rrect, Paint paint)
-
白灰色相间背景
同上不过用的是canvas.drawRect画矩形白灰相间的背景 -
右侧组限的文言 和底下组文言用paragraphBuilder来绘制
由于原生绘制文言的方式较为麻烦。底下用extension实现了对 Canvas的拓展。
extension Draw on ui.Canvas {
void drawText(
Offset offset,
ui.TextStyle textStyle,
String value, {
TextAlign textAlign = TextAlign.start,
double fontSize = 13,
double maxWidth = 100.0,
FontWeight fontWeight = FontWeight.normal,
}) {
var paragraphBuilder = ui.ParagraphBuilder(
ui.ParagraphStyle(
textAlign: textAlign, fontSize: fontSize, fontWeight: fontWeight),
)..pushStyle(textStyle);
paragraphBuilder.addText(value);
var pc = ui.ParagraphConstraints(width: maxWidth);
var textParagraph = paragraphBuilder.build()..layout(pc);
drawParagraph(textParagraph, offset);
}
}
- 画柱形后面的虚线
由于虚线在flutter没有api可以实现,我们可以隔着画一定距离画实线的方式画出虚线
void _drawVerticalDashLine(ui.Canvas canvas, int key, String value) {
_linePaint.color = BeautyColors.colorChartDash;
double dashLineHeight;
if (value != null && value.isNotEmpty) {
dashLineHeight = topGraphHeight + outLineWidth;
} else {
dashLineHeight = topGraphHeight;
}
var dashWidth = 2;
var dashSpace = 1;
var startY = popTextHeight;
final space = (dashSpace + dashWidth);
while (startY < dashLineHeight) {
canvas.drawLine(
Offset(
pagePadding +
columnWidth * key -
columnWidth / 2 +
columnPadding * (key - 1),
startY),
Offset(
pagePadding +
columnWidth * key -
columnWidth / 2 +
columnPadding * (key - 1),
startY + dashWidth),
_linePaint);
startY += space;
}
}
- 画popup文言背景图
popup背景是图片的,可以用drawImageRect方法实现,为了节约代码同样用extension写一个拓展。
extension Draw on ui.Canvas {
void drawImageLTWH(
ui.Image image,
double left,
double top,
Paint paint, {
double width,
double height,
}) {
drawImageRect(
image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
Rect.fromLTWH(left, top, width ?? image.width.toDouble(),
height ?? image.height.toDouble()),
paint);
}
}
在画图之前我们要把图片加载到内存里,这里使用 rootBundle.load方法加载图片。在flutter中加载图片是比较慢的,通常使用异步的方法加载图片。为了提高性能也可以考虑做图片预加载。
Future<ui.Image> load(String asset) async {
var data = await rootBundle.load(asset);
var codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
var fi = await codec.getNextFrame();
return fi.image;
}
view的点击事件
view的点击事件不像android一样需要考虑点击事件的分发,只需要关注点击的位置就可以了。
我们用 GestureDetector套在widget的外部就可以获取到整个widget所有的点击事件。通过实现onTabDown方法可以获取到点击的状态。
TapDownDetails.localPosition 可以精准的获取到相对于这个widget的坐标。
view计算
最后也是最繁复的一步就是计算不同部分的view的位置。
按照上面做的拆分我们可以按照从上到下从左到右的方式把view画出来。
与android不同的是,我们不用像android那样过多的考虑父view测量问题。
整个画布的大小变动都会通过paint方法回调的Size参数获取到。
另外点击事件响应后,可以通过改变widget state更新view。
void paint(Canvas canvas, Size size)
整体代码
CustomPainter代码
import 'dart:ui' as ui;
import 'package:flitm/resource/colors.dart';
import 'package:flitm/view/screen/walk/widget/ImageLoader.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
enum ChartLayoutType {
day,
week,
month,
year,
}
class ColumnData {
final double mount;
final String date;
ColumnData({this.mount = 0, this.date = ''});
}
typedef OnTouchDown = void Function(TapDownDetails details);
class CustomChartPaint extends CustomPainter {
Paint _columnPaint;
Paint _linePaint;
Paint _backgroundPaint;
Paint _imagePaint;
static const List<String> graduationText = [
'0',
'500',
'1000',
'1500',
'2000',
'2500'
];
static const bottomTextTopMargin = 16.0;
static const bottomTextHeight = 20.0;
static const bottomTextAreaHeight = 36.0;
static const outLineWidth = 10;
static const outOfRangeTopRegionHeight = 22.0;
static const popTextHeight = 44.0;
static const Map<int, String> weekBottomText = {
1: '日',
2: '月',
3: '火',
4: '水',
5: '木',
6: '金',
7: '土'
};
static const Map<int, String> dayBottomText = {
1: '0:00',
4: '',
7: '6:00',
10: '',
13: '12:00',
16: '',
19: '18:00',
22: ''
};
static const Map<int, String> monthBottomText = {
7: '7日',
14: '14日',
21: '21日',
28: '28日'
};
static const Map<int, String> yearBottomText = {
1: '',
2: '',
3: '3月',
4: '',
5: '',
6: '6月',
7: '',
8: '',
9: '9月',
10: '',
11: '',
12: '12月'
};
double columnWidth = 22.0;
double columnPadding = 20.0;
double columnTopPadding = 20.0;
double pagePadding = 20.0;
double rightTextWidth;
double topGraphHeight;
BuildContext context;
ChartLayoutType currentChartLayoutType;
Rect columnRect;
var maxRange = 2500.0;
var maxColumnCount = 0;
ui.RRect columnRoundRect;
TapDownDetails tabDownloadDetails;
ui.Image imageCenter;
ui.Image imageLeft;
ui.Image imageRight;
ui.Image imageTriangle;
List<ColumnData> columnDataList;
Map<int, String> bottomTextList = {};
CustomChartPaint(this.context, this.columnDataList,
{this.currentChartLayoutType = ChartLayoutType.day,
this.tabDownloadDetails}) {
initPaint();
initData();
}
void initData() {
imageCenter = ImageLoader.getInstance().getImage(ImageLoader.markUpCenter);
imageLeft = ImageLoader.getInstance().getImage(ImageLoader.markUpLeft);
imageRight = ImageLoader.getInstance().getImage(ImageLoader.markUpRight);
imageTriangle =
ImageLoader.getInstance().getImage(ImageLoader.markUpTriangle);
switch (currentChartLayoutType) {
case ChartLayoutType.day:
columnWidth = 7.0;
pagePadding = 16;
bottomTextList = dayBottomText;
maxColumnCount = 24;
break;
case ChartLayoutType.week:
columnWidth = 15.0;
pagePadding = 28;
bottomTextList = weekBottomText;
maxColumnCount = 7;
break;
case ChartLayoutType.month:
columnWidth = 5.0;
pagePadding = 7;
bottomTextList = monthBottomText;
maxColumnCount = 31;
break;
case ChartLayoutType.year:
columnWidth = 13.0;
pagePadding = 20;
bottomTextList = yearBottomText;
maxColumnCount = 12;
break;
}
}
void initPaint() {
_columnPaint = Paint()..isAntiAlias = true;
_linePaint = Paint()..strokeWidth = 1;
_backgroundPaint = Paint()
..style = PaintingStyle.fill
..isAntiAlias = true;
_imagePaint = Paint()..isAntiAlias = true;
}
@override
void paint(Canvas canvas, Size size) {
//calculate data we need
_viewPreCalculate(size);
//draw black-white area
_drawBackgroundAndRightText(canvas, size);
bottomTextList.forEach((key, value) {
_drawBottomText(canvas, size, key, value);
_drawVerticalDashLine(canvas, key, value);
});
if (columnDataList.isNotEmpty) {
for (var i = 1; i <= columnDataList.length; i++) {
_calculateColumnRect(i, size);
var from = Offset(columnRect.left, columnRect.top);
var to = Offset(columnRect.right, columnRect.bottom);
_drawAllColumn(canvas, from, to);
// tab down action
if (tabDownloadDetails != null &&
tabDownloadDetails.localPosition.dx >
columnRect.left - columnPadding / 2 &&
tabDownloadDetails.localPosition.dx <
columnRect.right + columnPadding / 2) {
_drawSelectColumn(canvas, from, to, i);
_drawPopMessage(canvas, size, i);
}
}
}
// draw bottom line
_drawBottomLine(canvas, size);
}
void _viewPreCalculate(Size size) {
rightTextWidth = calculateTextSize(true);
topGraphHeight = size.height - bottomTextAreaHeight;
columnPadding = ((size.width - rightTextWidth - 4) -
2 * pagePadding -
(maxColumnCount - 1) * columnWidth) /
(maxColumnCount - 1);
}
void _calculateColumnRect(int i, Size size) {
var rectLeft =
pagePadding + columnPadding * (i - 1) + columnWidth * (i - 1);
var rectTop = outOfRangeTopRegionHeight +
popTextHeight +
(size.height - columnTopPadding) *
(maxRange - columnDataList[i - 1].mount) /
maxRange;
var rectRight = pagePadding + columnWidth * i + columnPadding * (i - 1);
columnRect = Rect.fromLTRB(rectLeft, rectTop, rectRight, topGraphHeight);
}
void _drawAllColumn(ui.Canvas canvas, Offset from, Offset to) {
_columnPaint.shader = ui.Gradient.linear(from, to, [
BeautyColors.colorColumnBackground1st,
BeautyColors.colorColumnBackgroundSec,
BeautyColors.colorColumnBackgroundTrd
], [
0,
0.5,
1
]);
_columnPaint.style = PaintingStyle.fill;
columnRoundRect = RRect.fromRectAndCorners(columnRect,
topLeft: ui.Radius.circular(10), topRight: Radius.circular(10));
canvas.drawRRect(columnRoundRect, _columnPaint);
_columnPaint.shader = ui.Gradient.linear(from, to, [
BeautyColors.colorColumn1st,
BeautyColors.colorColumnSec,
BeautyColors.colorColumnTrd
], [
0,
0.5,
1
]);
canvas.drawRRect(columnRoundRect, _columnPaint);
_columnPaint.shader = ui.Gradient.linear(
from, to, [BeautyColors.blue01, BeautyColors.blue01]);
_columnPaint.style = PaintingStyle.stroke;
_columnPaint.strokeWidth = 1.0;
canvas.drawRRect(columnRoundRect, _columnPaint);
}
void _drawSelectColumn(ui.Canvas canvas, Offset from, Offset to, int i) {
_columnPaint.shader = ui.Gradient.linear(
from, to, [BeautyColors.pink01, BeautyColors.pink01]);
_columnPaint.style = PaintingStyle.fill;
canvas.drawRRect(columnRoundRect, _columnPaint);
_columnPaint.style = PaintingStyle.stroke;
canvas.drawRRect(columnRoundRect, _columnPaint);
_linePaint.color = BeautyColors.pink01;
double lineEnd;
if (bottomTextList[i] != null && bottomTextList[i].isNotEmpty) {
lineEnd = columnRect.bottom + outLineWidth;
} else {
lineEnd = columnRect.bottom;
}
canvas.drawLine(
Offset(
pagePadding +
columnWidth * i -
columnWidth / 2 +
columnPadding * (i - 1),
popTextHeight),
Offset(
pagePadding +
columnWidth * i -
columnWidth / 2 +
columnPadding * (i - 1),
lineEnd),
_linePaint);
}
void _drawPopMessage(ui.Canvas canvas, Size size, int i) {
double messageWidth;
double messageLeft;
//todo not a specific text append method
var startText = '';
var stepText = '${columnDataList[i - 1].mount}歩';
var month = '10月';
switch (currentChartLayoutType) {
case ChartLayoutType.day:
startText = '$i:00 ~ ${i + 1}:00';
break;
case ChartLayoutType.week:
startText = columnDataList[i - 1].date;
break;
case ChartLayoutType.month:
startText = month + '$i日';
break;
case ChartLayoutType.year:
startText = '$i月';
break;
}
var startTextWidth =
calculateTextSize(true, fontSize: 14, value: startText);
var startTextHeight =
calculateTextSize(false, fontSize: 14, value: startText);
var stepTextWidth = calculateTextSize(true,
fontSize: 14, fontWeight: FontWeight.bold, value: stepText);
var stepTextHeight = calculateTextSize(false,
fontSize: 14, fontWeight: FontWeight.bold, value: stepText);
messageWidth = startTextWidth + stepTextWidth + 2 * 6 + 8;
if (pagePadding +
columnWidth * i -
columnWidth / 2 +
columnPadding * (i - 1) <=
(messageWidth + 12) / 2) {
messageLeft = 0;
} else if (pagePadding +
columnWidth * i -
columnWidth / 2 +
columnPadding * (i - 1) >=
size.width - (messageWidth + 12) / 2) {
messageLeft = size.width - (messageWidth + 12);
} else {
messageLeft = pagePadding +
columnWidth * i -
columnWidth / 2 +
columnPadding * (i - 1) -
messageWidth / 2 -
6;
}
if (imageCenter != null) {
canvas.drawImageRect(
imageCenter,
Rect.fromLTWH(0, 0, imageCenter.width.toDouble(),
imageCenter.height.toDouble()),
Rect.fromLTWH(messageLeft + 6, 0, messageWidth, 34),
_imagePaint);
}
if (imageLeft != null) {
canvas.drawImageLTWH(imageLeft, messageLeft, 0, _imagePaint,
width: 6, height: 34);
}
if (imageRight != null) {
canvas.drawImageLTWH(
imageRight, messageLeft + messageWidth + 6, 0, _imagePaint,
width: 6, height: 34);
}
if (imageTriangle != null) {
canvas.drawImageLTWH(
imageTriangle,
pagePadding +
columnWidth * i -
columnWidth / 2 +
columnPadding * (i - 1) -
5,
32,
_imagePaint,
width: 10,
height: 8);
}
var textXOffSet = messageLeft + 6 + 6;
var textYOffset = (34 - startTextHeight) / 2;
var leftTextOffset = Offset(textXOffSet, textYOffset);
canvas.drawText(
leftTextOffset,
ui.TextStyle(color: BeautyColors.gray02),
startText,
fontSize: 14,
maxWidth: startTextWidth,
);
var stepTextXOffSet = messageLeft + 6 + 6 + startTextWidth + 8;
var stepTextYOffset = (34 - stepTextHeight) / 2;
var stepTextOffset = Offset(stepTextXOffSet, stepTextYOffset);
canvas.drawText(
stepTextOffset,
ui.TextStyle(color: BeautyColors.gray02),
stepText,
fontSize: 14,
fontWeight: FontWeight.bold,
maxWidth: stepTextWidth,
);
}
double calculateTextSize(bool isWidth,
{double maxWidth = 200,
double fontSize = 13,
String value = '12345',
FontWeight fontWeight = FontWeight.normal,
int maxLines = 1}) {
var painter = TextPainter(
locale: Localizations.localeOf(context, nullOk: true),
maxLines: maxLines,
textDirection: TextDirection.ltr,
text: TextSpan(
text: value,
style: TextStyle(
fontWeight: fontWeight,
fontSize: fontSize,
)));
painter.layout(maxWidth: maxWidth);
if (isWidth) {
return painter.width;
} else {
return painter.height;
}
}
void _drawBackgroundAndRightText(ui.Canvas canvas, Size size) {
// draw area
//todo not accomplished much
var backgroundHeight =
(topGraphHeight - outOfRangeTopRegionHeight - popTextHeight) / 5;
_backgroundPaint.color = BeautyColors.gray04_50;
for (var n = 1; n <= 5; n++) {
n % 2 == 1
? _backgroundPaint.color = BeautyColors.gray04_50
: _backgroundPaint.color = BeautyColors.white01;
canvas.drawRect(
Rect.fromLTRB(
0,
topGraphHeight - backgroundHeight * n,
size.width - rightTextWidth - 4,
topGraphHeight - backgroundHeight * (n - 1)),
_backgroundPaint);
}
var wordsHeight = calculateTextSize(false);
for (var n = 1; n <= 6; n++) {
var textXOffSet = size.width - rightTextWidth;
var textYOffset = size.height -
bottomTextHeight -
bottomTextTopMargin -
backgroundHeight * (n - 1) -
wordsHeight / 2;
var rightTextOffset = Offset(textXOffSet, textYOffset);
canvas.drawText(rightTextOffset, ui.TextStyle(color: BeautyColors.gray02),
graduationText[n - 1],
maxWidth: rightTextWidth, fontWeight: FontWeight.normal);
}
}
void _drawBottomText(ui.Canvas canvas, Size size, int key, String value) {
var bottomTextWidth =
calculateTextSize(true, value: value, fontWeight: FontWeight.bold);
var textHalfWidth = (bottomTextWidth / 2);
var textXOffSet = pagePadding +
columnPadding * (key - 1) +
columnWidth * (key - 1) +
columnWidth / 2 -
textHalfWidth;
var bottomTextOffset = Offset(textXOffSet, size.height - bottomTextHeight);
canvas.drawText(
bottomTextOffset, ui.TextStyle(color: BeautyColors.blue01), value,
fontWeight: FontWeight.bold,
textAlign: TextAlign.center,
maxWidth: bottomTextWidth);
}
void _drawVerticalDashLine(ui.Canvas canvas, int key, String value) {
_linePaint.color = BeautyColors.colorChartDash;
double dashLineHeight;
if (value != null && value.isNotEmpty) {
dashLineHeight = topGraphHeight + outLineWidth;
} else {
dashLineHeight = topGraphHeight;
}
var dashWidth = 2;
var dashSpace = 1;
var startY = popTextHeight;
final space = (dashSpace + dashWidth);
while (startY < dashLineHeight) {
canvas.drawLine(
Offset(
pagePadding +
columnWidth * key -
columnWidth / 2 +
columnPadding * (key - 1),
startY),
Offset(
pagePadding +
columnWidth * key -
columnWidth / 2 +
columnPadding * (key - 1),
startY + dashWidth),
_linePaint);
startY += space;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
void _drawBottomLine(ui.Canvas canvas, ui.Size size) {
var start = ui.Offset(0, topGraphHeight);
var end = ui.Offset(
size.width - rightTextWidth - 4, size.height - bottomTextAreaHeight);
_linePaint.color = BeautyColors.blue01;
canvas.drawLine(start, end, _linePaint);
}
}
extension Draw on ui.Canvas {
void drawText(
Offset offset,
ui.TextStyle textStyle,
String value, {
TextAlign textAlign = TextAlign.start,
double fontSize = 13,
double maxWidth = 100.0,
FontWeight fontWeight = FontWeight.normal,
}) {
var paragraphBuilder = ui.ParagraphBuilder(
ui.ParagraphStyle(
textAlign: textAlign, fontSize: fontSize, fontWeight: fontWeight),
)..pushStyle(textStyle);
paragraphBuilder.addText(value);
var pc = ui.ParagraphConstraints(width: maxWidth);
var textParagraph = paragraphBuilder.build()..layout(pc);
drawParagraph(textParagraph, offset);
}
void drawImageLTWH(
ui.Image image,
double left,
double top,
Paint paint, {
double width,
double height,
}) {
drawImageRect(
image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
Rect.fromLTWH(left, top, width ?? image.width.toDouble(),
height ?? image.height.toDouble()),
paint);
}
}
ImageLoader代码
class ImageLoader {
ImageLoader._();
static final _instance = ImageLoader._();
factory ImageLoader.getInstance() => _instance;
Map<String, ui.Image> imagePool = {};
static const markUpCenter = 'mark_up_center';
static const markUpTriangle = 'mark_up_Triangle';
static const markUpLeft = 'mark_up_left';
static const markUpRight = 'mark_up_right';
void preCacheImage() {
load('assets/3x/ic_markup_1_center.png').then((value) {
imagePool.addAll({markUpCenter: value});
});
load('assets/3x/ic_markup_1.png').then((value) {
imagePool.addAll({markUpTriangle: value});
});
load('assets/3x/ic_markup_1_left.png').then((value) {
imagePool.addAll({markUpLeft: value});
});
load('assets/3x/ic_markup_1_right.png').then((value) {
imagePool.addAll({markUpRight: value});
});
}
void clearImageCache() {
imagePool = null;
}
ui.Image getImage(String name) {
return imagePool[name];
}
Future<ui.Image> load(String asset) async {
var data = await rootBundle.load(asset);
var codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
var fi = await codec.getNextFrame();
return fi.image;
}
}
在widget中使用代码
Widget _getContent() {
return Column(
children: [
Container(
decoration: BoxDecoration(
color:Colors.blue,
),
height: 190,
),
// padding: EdgeInsets.only(left: 16,right: 16),
Expanded(
flex: 1,
child: Container(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 30),
child: SizedBox(
width: double.infinity,
child: GestureDetector(
onTapDown: (detail) {
print('ontouchdown${detail.localPosition.dx}');
setState(() {
details = detail;
});
},
child: CustomPaint(
painter: CustomChartPaint(context, doubleList,
currentChartLayoutType: type,
tabDownloadDetails: details),
),
)),
)),
],
);
}
网友评论