美文网首页
用CustomPaint自定义ChartView

用CustomPaint自定义ChartView

作者: 大丸蛇 | 来源:发表于2020-11-03 15:31 被阅读0次

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),
                    ),
                  )),
            )),
      ],
    );
  }

相关文章

网友评论

      本文标题:用CustomPaint自定义ChartView

      本文链接:https://www.haomeiwen.com/subject/xizbvktx.html