Flutter中自定义视图实现柱形图的绘制

本文详细介绍了如何使用Flutter自定义视图绘制带有具体数值和日期的柱形图,包括绘制矩形、直线和文字的方法,以及如何计算文字位置以实现居中显示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

主要通过这个博文学习通过Flutter自定义视图来绘制柱形图.

这年头没个图片都不好意思发博客了:

在柱形图上方显示具体数值,x轴下方显示具体日期,忽略上方的日月年的tabBar.

首先介绍几个主要的方法:

canvas.drawRect(Rect rect, Paint paint);    //绘制矩形
canvas.drawLine(Offset p1, Offset p2, Paint paint);    //绘制一条直线
canvas.drawParagraph(Paragraph paragraph, Offset offset);     //绘制文字

下面开始撸代码:

class CustomChartPaint extends CustomPainter {

  Paint _bgPaint;
  Paint _barPaint;
  Paint _linePaint;

  Color backgroundColor = Color(0xFFF6F6F6); //条形图背景颜色
  Color barColor = Color(0xFF5858D6); //条形图颜色
  static const Color textColor = Color(0xFF5858D6); //字体颜色
  Color lineColor = Color(0xFFD1D1D6); //X轴颜色
  double barWidth = 22.0; //条形图宽度
  double barMargin = 20.0; //两条形图间距
  double topMargin = 22.0;   //图表与其他试图上边距
  double textTopMargin = 8.0;   //X轴文字与X轴间距
  static const double xFontSize = 12.0;   //X轴文字的字体大小
  double bottomTextDescent = 20;
  double bottomTextHeight = 20;

  final List<double> dataList;
  final List<String> bottomList;
  final String xText;

  List<String> bottomTextList;


  List targetPercentList;
  List percentList;

  CustomChartPaint(this.dataList, this.bottomList, this.xText){

    //print("max" + (dataList.reduce(max)+dataList.reduce(min)).toString());
    setBottomTextList(bottomList);
    setDataList(dataList, dataList.reduce(max)+dataList.reduce(min));
    init();
  }

  init(){
    _bgPaint = new Paint()
      ..isAntiAlias = true    //是否启动抗锯齿
      ..color = backgroundColor;  //画笔颜色

    _barPaint = new Paint()
      ..color = barColor
      ..isAntiAlias = true
      ..style = PaintingStyle.fill; //绘画风格,默认为填充;

    _linePaint = new Paint()
      ..color = lineColor
      ..strokeWidth = 0.5;
  }



  setBottomTextList(List<String> bottomStringList) {

    this.bottomTextList = bottomStringList;

  }

  setDataList(List<double> list, double max) {
    targetPercentList = new List<double>();

    if (max == 0) max = 1;

    for (int i = 0; i < list.length; i++) {
      targetPercentList.add((1 - list[i] / max)); //当前数据占总长度的百分比
    }

    // TODO Make sure percentList.length() == targetPercentList.length()
    
  }

  Rect bgRect;
  Rect fgRect;

  @override
  void paint(Canvas canvas, Size size) {

    print("size: " + size.toString());  //画布大小

    int i = 1;
    if (targetPercentList.length != 0) {
      for (double f in targetPercentList) {
        bgRect = Rect.fromLTRB(barMargin * i + barWidth * (i - 1),
            topMargin,
            (barMargin + barWidth) * i,
            size.height - bottomTextHeight - textTopMargin);
        canvas.drawRect(bgRect, _bgPaint);      //绘制背景柱形

        double rectLeft = barMargin * i + barWidth * (i - 1);
        double rectRight = topMargin + (size.height - topMargin - bottomTextHeight - textTopMargin) * targetPercentList[i - 1];

        //print("rectX: " + rectLeft.toString() + ", rectY: " + rectRight.toString());

        fgRect = Rect.fromLTRB(rectLeft, rectRight,
            (barMargin + barWidth) * i,
            size.height - bottomTextHeight - textTopMargin);
        canvas.drawRect(fgRect, _barPaint);     //绘制前景柱形

        Offset textOffset = new ui.Offset(rectLeft - barWidth/2, rectRight - 18);     /// 18为具体数值与柱形顶部的间距
        _drawParagraph(canvas, textOffset, dataList[i-1].toString());   //在bar上描绘具体数值

        i++;
      }

      double mViewWidth = (bottomTextList.length + 1.5) * (barWidth + barMargin);   //整个视图的宽度

      Offset start = ui.Offset(0, size.height - bottomTextHeight - textTopMargin);
      Offset end = ui.Offset(mViewWidth, size.height - bottomTextHeight - textTopMargin);

      canvas.drawLine(start, end, _linePaint);    //画一条X轴

      if (bottomTextList != null && bottomTextList.isNotEmpty) {
        i = 1;
        for (String bottomText in bottomTextList) {

          Offset bottomTextOffset = new Offset(barMargin * i + barWidth * (i - 2) + barWidth/2, size.height - bottomTextDescent - textTopMargin);
          _drawParagraph(canvas, bottomTextOffset, bottomText);   //绘制X轴描述
          i++;
        }
      }

      Offset bottomTextOffset = new Offset(barMargin * i + barWidth * (i - 1), size.height - bottomTextDescent - textTopMargin);
      _drawParagraph(canvas, bottomTextOffset, xText);    //绘制X轴变量名

    }

  }

  _drawParagraph(Canvas canvas, Offset offset, String text, {double fontSize : xFontSize, Color textColor: textColor}){
    ParagraphBuilder paragraphBuilder = new ui.ParagraphBuilder(
      new ui.ParagraphStyle(
        textAlign: TextAlign.center,    //在ui.ParagraphConstraints(width: barWidth * 2);所设置的宽度中居中显示
        fontSize: fontSize,
      ),
    )
      ..pushStyle(ui.TextStyle(color: textColor));
    paragraphBuilder.addText(text);

    ParagraphConstraints pc = ui.ParagraphConstraints(width: barWidth * 2);    //字体可用宽度
    //这里需要先layout, 后面才能获取到文字高度
    Paragraph textParagraph = paragraphBuilder.build()..layout(pc);

    canvas.drawParagraph(textParagraph, offset);    //描绘offset所表示的位置上描绘文字text
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

在这简单介绍几个实用的方法:

List.reduce(max)   //求list列表中最大值
List.reduce(min)   //求list列表中的最小值

另外针对于:

double mViewWidth = (bottomTextList.length + 1.5) * (barWidth + barMargin);   //整个视图的宽度

其中这个1.5是为了预留给x轴中"天"的长度,避免跟屏幕挨得很近影响视觉效果.

 

其中重点说一下这个描绘文字的,其他构建矩形跟画直线相对简单,与Android原生自定义view很像,我们直接看方法然后看他的参数名就能理解.此处不详述了.

我这里先简单说一下Android原生绘制文字的方法吧:

fgPaint.setTextSize(textSize);
String text = targetPercentList.get(i-1).toString();
float sizeWidth = fgPaint.measureText(text);

canvas.drawText( text, barMargin* i + barWidth * (i - 1) + barWidth /2 - sizeWidth /2, topMargin + (int) ((getHeight()
        - topMargin
        - bottomTextHeight
        - textTopMargin) * targetPercentList.get(i - 1)) - 20, fgPaint);    //在bar上描绘具体数值

首先我们有drawText方法,而且measureText方法可以来测量文字的宽度,再通过文字的宽度来定位,让该文字显示在柱形图的上方,并且是居中的,这里的位置计算大家画个图理解一下.(这里的20是柱形图文字与柱形图的间距)

 但是在Flutter就根本没有drawText方法了,我们只能通过 canvas.drawParagraph(Paragraph paragraph, Offset offset);方法来实现,其中Offset是文字的位置,而且Flutter也没有measureText方法来计算文字宽度,这就一脸懵逼了,不能计算文字宽度,我们最多也就能让文字定位在柱形图上面,但是不能跟柱形对其并居中显示.

这就主要看另一个参数 Paragraph了.

这里拿柱形图上方文字来举例吧,因为文字太长,柱形图的宽度不够显示,所以可以通过下面的方法设置他的可用长度,

ParagraphConstraints pc = ui.ParagraphConstraints(width: barWidth * 2); 

设置了字体的可用长度为 2倍barWidth,不得不佩服我的机制,.

然后就是居中问题了,

ParagraphBuilder paragraphBuilder = new ui.ParagraphBuilder(
  new ui.ParagraphStyle(
    textAlign: TextAlign.center,    //在ui.ParagraphConstraints(width: barWidth * 2);所设置的宽度中居中显示
    fontSize: fontSize,
  ),
)
Offset textOffset = new ui.Offset(rectLeft - barWidth/2, rectRight - 18);     /// 18为具体数值与柱形顶部的间距

示意图如下,(兄弟们原谅我三更半夜没来及用画图工具画个好看的示意图了,只能手工来了),

 其中这个offset主要是关于我们之前设置的的 pc 的定位,至于文字已经在barWidth*2这个长度中居中了,那么只需要将这个长度的容器居中放在柱形图的上方就OK了.X轴下面的文字也是这个道理,相信就不用我再赘述了.

好了最后就是在我们的StatelessWidget中设置SingleChildScrollView,使我们的柱形图可以在横向左右滑动,即使柱形图太长一屏显示不完也没关系啦~~~~

return new SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: new Container(
    child: CustomPaint(
      size: new Size((bottomTextList.length + 1.5) * 42.0, 400.0),
      painter: CustomChartPaint(dataList, bottomTextList, xText),
    ),
  ),
);

另外补充一点就是这个TextStyle一定要这么写,如果去掉了ui则会报错

  ..pushStyle(ui.TextStyle(color: textColor));

 

The argument type 'TextStyle (where TextStyle is defined in D:\software\Android\Flutter\packages\flutter\lib\src\painting\text_style.dart)' can't be assigned to the parameter type 'TextStyle (where TextStyle is defined in D:\software\Android\Flutter\bin\cache\pkg\sky_engine\lib\ui\text.dart)'.

至于这个ui是啥:

import 'dart:ui' deferred as ui;
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值