So let’s look at the effect

The table can be swiped left or right, I wrote a simple, is a demonstration, the knowledge is as follows:

  1. The use of CustomPainter
  2. How do I draw a continuous line segment
  3. How do I add scroll events
  4. How do I use clipRect to intercept a drawing range without affecting other layers
  5. How to draw text

Let ‘s go!

1. Define the attributes of a custom table

Let’s start by defining some properties for our example

// The background Color of the line graph is bgColor; // the Color of the X-axis and Y-axis is xyColor; Bool showBaseline; // bool showBaseline; List<ChartData> dataList; Double columnSpace; double columnSpace; Int paddingLeft; paddingLeft; int paddingLeft; Int paddingTop; Int paddingBottom; // Paint linePaint for x, y, and marked text; Int markLineLength; int markLineLength; Int maxYValue; // how many rows on the y axis int yCount; // polygonalLineColor; // The offset of all the contents of the X-axis, used to change the position of the contents when sliding double xOffset; Int paddingRight = 30; Double realChartRectWidth; double realChartRectWidth; Function xOffsetSet = (double xOffset) {};Copy the code

Then there is the constructor, where we initialize some properties:

LineChartWidget({ @required this.dataList, @required this.maxYValue, @required this.yCount, @required this.xOffsetSet, this.bgColor = Colors.white, this.xyColor = Colors.black, this.showBaseline = false, this.columnSpace, this.paddingLeft, this.paddingTop, this.paddingBottom, this.markLineLength, this.polygonalLineColor = Colors.blue, this.xOffset, }) { linePaint = Paint().. color = xyColor; realChartRectWidth = (dataList.length - 1) * columnSpace; }Copy the code

RealChartRectWidth is the width of our internal rectangle, as shown in the red area below, which was created to facilitate the calculation of coordinates later

Let’s start drawing in paint(Canvas Canvas, Size Size) :

2. Paint the background color

canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint().. color = bgColor);

Where size is the width and height of the control, Paint is our brush, and color is the color of the brush

3. Create a rectangle for subsequent drawing

Rect innerRect = Rect.fromPoints(
   Offset(paddingLeft.toDouble(), paddingTop.toDouble()),
   Offset(size.width, size.height - paddingBottom),
 );
Copy the code

This rectangle is the red part of the figure above. PaddingLeft and paddingTop are the distances of the red rectangle from the left and top edges

4. The y axis

 canvas.drawLine(innerRect.topLeft, innerRect.bottomLeft.translate(0, markLineLength.toDouble()), linePaint);
Copy the code

InnerRect. BottomLeft. Translate this method just will you made A combined the incoming parameters, we can see the internal implementation, so that our y is A little bit longer than innerRect, is the picture Data such as A label on more that A small number of line segments.

5. Draw the x axis

canvas.drawLine(innerRect.bottomLeft, innerRect.bottomRight, linePaint);
Copy the code

This is the drawing of the bottom edge of the red rectangle above

6. Draw Y-axis markers

 double ySpace = innerRect.height / yCount;
 double startX = innerRect.topLeft.dx - markLineLength;
 double startY;
 for (int i = 0; i < yCount + 1; i++) {
   startY = innerRect.topLeft.dy + i * ySpace;
   if (showBaseline) {
     canvas.drawLine(
       Offset(innerRect.topLeft.dx - markLineLength, startY),
       Offset(innerRect.topLeft.dx + innerRect.width, startY),
       linePaint,
     );
   } else {
     canvas.drawLine(
       Offset(innerRect.topLeft.dx - markLineLength, startY),
       Offset(innerRect.topLeft.dx, startY),
       linePaint,
     );
   }
   drawYText(
     (i * maxYValue ~/ yCount).toString(),
     Offset(innerRect.topLeft.dx - markLineLength, innerRect.bottomLeft.dy - i * ySpace),
     canvas,
   );
 }
Copy the code

YSpace is the distance between each label on the y axis, startX is the x to the left of the red rectangle, and the left minus the length of that little line segment, that’s the starting coordinate of that little line segment, and then the logic is pretty simple, if you show the base line, which is the grid, you go from the x axis of that little line segment to the far right of the red rectangle, Otherwise I’ll just draw it to the left of the red rectangle. The drawYText method needs to be explained. Canvas can draw text, but I think it is easier to use TextPainter directly. The implementation of this method is as follows (drawXText) :

 List getTextPainterAndSize(String text) {
   TextPainter textPainter = TextPainter(
     textDirection: TextDirection.ltr,
     text: TextSpan(
       text: text,
       style: TextStyle(color: Colors.black),
     ),
   );
   textPainter.layout();
   Size size = textPainter.size;
   return [textPainter, size];
 }

 void drawYText(String text, Offset topLeftOffset, Canvas canvas) {
   List list = getTextPainterAndSize(text);
   list[0].paint(canvas, topLeftOffset.translate(-list[1].width, -list[1].height / 2));
 }

 void drawXText(String text, Offset topLeftOffset, Canvas canvas) {
   List list = getTextPainterAndSize(text);
   list[0].paint(canvas, topLeftOffset.translate(-list[1].width / 2, 0));
 }
Copy the code

7. We then store the value of each actual data in a set of x and Y coordinates on the screen, namely the set of points on the line chart

List<Pair<double, double>> pointList = [];

8. Draw the logo text below the X axis

Int xCount = dataList. Length; startY = innerRect.bottom + markLineLength; for (int i = 0; i < xCount; i++) { startX = innerRect.bottomLeft.dx + i * columnSpace + xOffset; If (innerrect.bottomleft. dx + xOffset < innerrect.left) {// mark 1 canvas.save(); canvas.clipRect( Rect.fromLTWH( innerRect.left, innerRect.top, innerRect.width, innerRect.height, ), ); If (I == 0 && startX > paddingLeft) {// mark 2 startX = innerrect.bottomleft.dx; // Set LineChart's xOffset to 0, otherwise LineChart's right slide to the first value will cause xOffset to add the value of the right drag. // Then the left drag will only change when xOffset is 0. It looks like I'm dragging to the left but the UI doesn't change // so I'm going to say xOffset = 0; xOffsetSet(0.toDouble()); } pointlist. add(Pair(startX, // the height of the inner rectangle minus the actual pixel size of the actual value of the data, Innerrect. height-datalist [I]. Value/maxYValue * innerRect.height + paddingTop,),); if (showBaseline) { canvas.drawLine( Offset(startX, innerRect.top), Offset(startX, startY), linePaint, ); } else { canvas.drawLine( Offset(startX, innerRect.bottom), Offset(startX, startY), linePaint, ); } if (innerrect.bottomleft. dx + xOffset < innerrect.left) {// mark 3 canvas.restore(); } drawXText( dataList[i].type, Offset(innerRect.bottomLeft.dx + i * columnSpace + xOffset, startY), canvas, ); }Copy the code

ColumnSpace is the spacing we set between the x axis markers when creating the line chart. The xOffset is the offset of the x label and the vertical line as we drag the line chart.

The code at mark 1 explains why this is so that when the line chart is dragged to the left, the vertical line of the line chart is only displayed in the red rectangle. This is done by using canvas.save(); And Canvas. clipRect. The save method can ensure that the next drawing of canvas does not affect the previous drawing, and the clipRect method specifies the next drawing range in the red rectangle.

If the first data has reached the leftmost position of the red rectangle when dragging the line graph to the right, it is invalid to let the user drag it to the right without changing the coordinate of the x axis and the startX value of the vertical line and the offset of xOffset. Here is a key operation is to just reset xOffset offset notice to upper nodes (code) is given for a while, tell the upper nodes change xOffset offset, because already xOffset values change, the need to tell the upper nodes in order to keep the unity of the two values, otherwise only change here for the offset and upper nodes have not changed, The next time you pass in the value of xOffset, it will be incorrect.

The pointlist. add method holds the actual x and y coordinates of the actual data.

Sava must correspond to the restore method one by one. Here we restore the previous drawing range, and then we continue to draw the text. If we do not restore the drawing range, our text will not be visible. Because the coordinates of the text are outside the red rectangle.

9. Painting line

canvas.save(); canvas.clipRect( Rect.fromLTWH( paddingLeft.toDouble(), paddingTop.toDouble(), innerRect.width, innerRect.height, ), ); Canvas. DrawPoints (///PointMode enumeration has three types: points, lines, polygon, Pointmode.polygon, pointList.map((pair) => Offset(pair.first, pair.last)).tolist (), Paint().. color = polygonalLineColor .. strokeWidth = 2, ); canvas.restore();Copy the code

Again, our polyline must be in the red rectangle, so we have a save and restore operation here, which is very simple to say.

10. Then we add a gesture to the CustomPainter, which basically changes the value of the xOffset as follows

class LineChart extends StatefulWidget { final double width; final double height; // The background Color of the bar graph is final Color bgColor; // final Color xyColor; // Final Color columnarColor; // Whether to display the baseline of x and y axes final bool showBaseline; // Final List<ChartData> dataList; Final double columnSpace; // Final double columnSpace; Final int paddingLeft; Final int paddingTop; Final int paddingBottom; Final int markLineLength; // final int maxYValue; // final int yCount; // Final Color polygonalLineColor; // Final double xOffset; LineChart( this.width, this.height, { @required this.dataList, @required this.maxYValue, @required this.yCount, this.bgColor = Colors.white, this.xyColor = Colors.black, this.columnarColor = Colors.blue, this.showBaseline = false, this.columnSpace = 60, this.paddingLeft = 40, this.paddingTop = 30, this.paddingBottom = 30, this.markLineLength = 10, this.polygonalLineColor = Colors.blue, this.xOffset = 0, }); @override _LineChartState createState() => _LineChartState(); } class _LineChartState extends State<LineChart> { double xOffset; @override void initState() { xOffset = widget.xOffset; super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( onHorizontalDragUpdate: (DragUpdateDetails details) { setState(() { xOffset += details.primaryDelta; }); }, onHorizontalDragDown: (DragDownDetails details){ print("onHorizontalDragDown"); }, onHorizontalDragCancel: (){ print("onHorizontalDragCancel"); }, onHorizontalDragEnd: (DragEndDetails details){ print("onHorizontalDragEnd"); }, onHorizontalDragStart: (DragStartDetails details){ print("onHorizontalDragStart"); }, child: CustomPaint( size: Size(widget.width, widget.height), painter: LineChartWidget( bgColor: widget.bgColor, xyColor: widget.xyColor, showBaseline: widget.showBaseline, dataList: widget.dataList, maxYValue: widget.maxYValue, yCount: widget.yCount, columnSpace: widget.columnSpace, paddingLeft: widget.paddingLeft, paddingTop: widget.paddingTop, paddingBottom: widget.paddingBottom, markLineLength: widget.markLineLength, polygonalLineColor: Color. blue, xOffset: xOffset, xOffsetSet: (double xOffset) {// mark 4 this. },),),); }}Copy the code

The key part of this code is to make the CustomPaint the GestureDetector child. Then in the onHorizontalDragUpdate method, we record the xOffset and refresh the UI. And that parameter is passed to LineChartWidget so that the xOffset in LineChartWidget is also changed so that the line graph in the red rectangle, the offset is used to calculate the x label and the x coordinate of the vertical line, And then mark 4 is the top line graph class and when you drag to the right if the first data is on the left edge of the red rectangle, you stop sliding, you pass in the xOffset value here and that changes LineChart’s value here.

11. The complete code is as follows:

 import 'dart:ui';
 
 import 'package:campsite_flutter/util/collection.dart';
 import 'package:flutter/material.dart';
 
 /// 自定义折线图
 /// 作者:liuhc
 class LineChartWidget extends CustomPainter {
   //折线图的背景颜色
   Color bgColor;
   //x轴与y轴的颜色
   Color xyColor;
   //是否显示x轴与y轴的基准线
   bool showBaseline;
   //实际的数据
   List<ChartData> dataList;
   //x轴之间的间隔
   double columnSpace;
   //表格距离左边的距离
   int paddingLeft;
   //表格距离顶部的距离
   int paddingTop;
   //表格距离底部的距离
   int paddingBottom;
   //绘制x轴、y轴、标记文字的画笔
   Paint linePaint;
   //标记线的长度
   int markLineLength;
   //y轴数据最大值
   int maxYValue;
   //y轴分多少行
   int yCount;
   //折线的颜色
   Color polygonalLineColor;
   //x轴所有内容的偏移量,用来在滑动的时候改变内容的位置
   double xOffset;
   //该值保证最后一条数据的底部文字能正常显示出来
   int paddingRight = 30;
   //内部折线图的实际宽度
   double realChartRectWidth;
   Function xOffsetSet = (double xOffset) {};
 
   LineChartWidget({
     @required this.dataList,
     @required this.maxYValue,
     @required this.yCount,
     @required this.xOffsetSet,
     this.bgColor = Colors.white,
     this.xyColor = Colors.black,
     this.showBaseline = false,
     this.columnSpace,
     this.paddingLeft,
     this.paddingTop,
     this.paddingBottom,
     this.markLineLength,
     this.polygonalLineColor = Colors.blue,
     this.xOffset,
   }) {
     linePaint = Paint()..color = xyColor;
     realChartRectWidth = (dataList.length - 1) * columnSpace;
   }
 
   @override
   void paint(Canvas canvas, Size size) {
     //画背景颜色
     canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = bgColor);
     //创建一个矩形,方便后续绘制
     Rect innerRect = Rect.fromPoints(
       Offset(paddingLeft.toDouble(), paddingTop.toDouble()),
       Offset(size.width, size.height - paddingBottom),
     );
     //画y轴
     canvas.drawLine(innerRect.topLeft, innerRect.bottomLeft.translate(0, markLineLength.toDouble()), linePaint);
     //画x轴
     canvas.drawLine(innerRect.bottomLeft, innerRect.bottomRight, linePaint);
     //画y轴标记
     double ySpace = innerRect.height / yCount;
     double startX = innerRect.topLeft.dx - markLineLength;
     double startY;
     for (int i = 0; i < yCount + 1; i++) {
       startY = innerRect.topLeft.dy + i * ySpace;
       if (showBaseline) {
         canvas.drawLine(
           Offset(innerRect.topLeft.dx - markLineLength, startY),
           Offset(innerRect.topLeft.dx + innerRect.width, startY),
           linePaint,
         );
       } else {
         canvas.drawLine(
           Offset(innerRect.topLeft.dx - markLineLength, startY),
           Offset(innerRect.topLeft.dx, startY),
           linePaint,
         );
       }
       drawYText(
         (i * maxYValue ~/ yCount).toString(),
         Offset(innerRect.topLeft.dx - markLineLength, innerRect.bottomLeft.dy - i * ySpace),
         canvas,
       );
     }
     //保存每个实际数据的值在屏幕中的x、y坐标值
     List<Pair<double, double>> pointList = [];
     //画x轴标记
     int xCount = dataList.length;
     startY = innerRect.bottom + markLineLength;
     for (int i = 0; i < xCount; i++) {
       startX = innerRect.bottomLeft.dx + i * columnSpace + xOffset;
       if (innerRect.bottomLeft.dx + xOffset < innerRect.left) {
         canvas.save();
         canvas.clipRect(
           Rect.fromLTWH(
             innerRect.left,
             innerRect.top,
             innerRect.width,
             innerRect.height,
           ),
         );
       }
       //保证向右拖动的时候第一个数据保持在起始位置
       if (i == 0 && startX > paddingLeft) {
         startX = innerRect.bottomLeft.dx;
         // 在这里将LineChart的xOffset置为0,否则LineChart向右滑到第一个值的时候继续向右滑动会导致xOffset累加向右拖动的值,
         // 然后会导致向左拖动的时候只能等xOffset等于0的时候UI才会变化,这样看起来就是向左拖动但是UI没有变化
         // 所以这里加此判断
         xOffset = 0;
         xOffsetSet(0.toDouble());
       }
       pointList.add(
         Pair(
           startX,
           //内矩形高度减去数据实际值的实际像素大小,再加上顶部空白的距离
           innerRect.height - dataList[i].value / maxYValue * innerRect.height + paddingTop,
         ),
       );
       if (showBaseline) {
         canvas.drawLine(
           Offset(startX, innerRect.top),
           Offset(startX, startY),
           linePaint,
         );
       } else {
         canvas.drawLine(
           Offset(startX, innerRect.bottom),
           Offset(startX, startY),
           linePaint,
         );
       }
       if (innerRect.bottomLeft.dx + xOffset < innerRect.left) {
         canvas.restore();
       }
       drawXText(
         dataList[i].type,
         Offset(innerRect.bottomLeft.dx + i * columnSpace + xOffset, startY),
         canvas,
       );
     }
     //画折线
     canvas.save();
     canvas.clipRect(
       Rect.fromLTWH(
         paddingLeft.toDouble(),
         paddingTop.toDouble(),
         innerRect.width,
         innerRect.height,
       ),
     );
     canvas.drawPoints(
       ///PointMode的枚举类型有三个,points(点),lines(线,隔点连接),polygon(线,相邻连接)
       PointMode.polygon,
       pointList.map((pair) => Offset(pair.first, pair.last)).toList(),
       Paint()
         ..color = polygonalLineColor
         ..strokeWidth = 2,
     );
     canvas.restore();
   }
 
   List getTextPainterAndSize(String text) {
     TextPainter textPainter = TextPainter(
       textDirection: TextDirection.ltr,
       text: TextSpan(
         text: text,
         style: TextStyle(color: Colors.black),
       ),
     );
     textPainter.layout();
     Size size = textPainter.size;
     return [textPainter, size];
   }
 
   void drawYText(String text, Offset topLeftOffset, Canvas canvas) {
     List list = getTextPainterAndSize(text);
     list[0].paint(canvas, topLeftOffset.translate(-list[1].width, -list[1].height / 2));
   }
 
   void drawXText(String text, Offset topLeftOffset, Canvas canvas) {
     List list = getTextPainterAndSize(text);
     list[0].paint(canvas, topLeftOffset.translate(-list[1].width / 2, 0));
   }
 
   @override
   bool shouldRepaint(CustomPainter oldDelegate) {
     return oldDelegate != this;
   }
 }
 
 class ChartData {
   String type;
   double value;
 
   ChartData(this.type, this.value);
 }
 
 class LineChart extends StatefulWidget {
   final double width;
   final double height;
   //柱状图的背景颜色
   final Color bgColor;
   //x轴与y轴的颜色
   final Color xyColor;
   //柱状图的颜色
   final Color columnarColor;
   //是否显示x轴与y轴的基准线
   final bool showBaseline;
   //实际的数据
   final List<ChartData> dataList;
   //每列之间的间隔
   final double columnSpace;
   //控件距离左边的距离
   final int paddingLeft;
   //控件距离顶部的距离
   final int paddingTop;
   //控件距离底部的距离
   final int paddingBottom;
   //标记线的长度
   final int markLineLength;
   //y轴最大值
   final int maxYValue;
   //y轴分多少行
   final int yCount;
   //折线的颜色
   final Color polygonalLineColor;
   //x轴所有内容的偏移量
   final double xOffset;
 
   LineChart(
     this.width,
     this.height, {
     @required this.dataList,
     @required this.maxYValue,
     @required this.yCount,
     this.bgColor = Colors.white,
     this.xyColor = Colors.black,
     this.columnarColor = Colors.blue,
     this.showBaseline = false,
     this.columnSpace = 60,
     this.paddingLeft = 40,
     this.paddingTop = 30,
     this.paddingBottom = 30,
     this.markLineLength = 10,
     this.polygonalLineColor = Colors.blue,
     this.xOffset = 0,
   });
 
   @override
   _LineChartState createState() => _LineChartState();
 }
 
 class _LineChartState extends State<LineChart> {
   double xOffset;
 
   @override
   void initState() {
     xOffset = widget.xOffset;
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
     return GestureDetector(
       onHorizontalDragUpdate: (DragUpdateDetails details) {
 //        print("DragUpdateDetails");
         setState(() {
           xOffset += details.primaryDelta;
         });
       },
       onHorizontalDragDown: (DragDownDetails details){
         print("onHorizontalDragDown");
       },
       onHorizontalDragCancel: (){
         print("onHorizontalDragCancel");
       },
       onHorizontalDragEnd: (DragEndDetails details){
         print("onHorizontalDragEnd");
       },
       onHorizontalDragStart: (DragStartDetails details){
         print("onHorizontalDragStart");
       },
       child: CustomPaint(
         size: Size(widget.width, widget.height),
         painter: LineChartWidget(
           bgColor: widget.bgColor,
           xyColor: widget.xyColor,
           showBaseline: widget.showBaseline,
           dataList: widget.dataList,
           maxYValue: widget.maxYValue,
           yCount: widget.yCount,
           columnSpace: widget.columnSpace,
           paddingLeft: widget.paddingLeft,
           paddingTop: widget.paddingTop,
           paddingBottom: widget.paddingBottom,
           markLineLength: widget.markLineLength,
           polygonalLineColor: Colors.blue,
           xOffset: xOffset,
           xOffsetSet: (double xOffset) {
             this.xOffset = xOffset;
           },
         ),
       ),
     );
   }
 }
 
 void main() {
   runApp(
     MaterialApp(
       home: Test(),
     ),
   );
 }
 
 class Test extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     Size size = MediaQuery.of(context).size;
     return Scaffold(
       appBar: AppBar(
         title: Text("自定义折线图"),
       ),
       body: Container(
         child: LineChart(
           size.width,
           300,
 //          bgColor: Colors.red,
           xOffset: 10,
           showBaseline: true,
           maxYValue: 600,
           yCount: 6,
           dataList: [
             ChartData("Data A", 100),
             ChartData("Data B", 300),
             ChartData("Data C", 200),
             ChartData("Data D", 500),
             ChartData("Data E", 450),
             ChartData("Data F", 230),
             ChartData("Data G", 270),
             ChartData("Data H", 170),
           ],
         ),
       ),
     );
   }
 }
Copy the code

Pair class:

 class Pair<E, F> {
   E first;
   F last;
 
   Pair(this.first, this.last);
 
   String toString() => '($first, $last)';
 
   bool operator ==(other) {
     if (other is! Pair) return false;
     return other.first == first && other.last == last;
   }
 
   int get hashCode => first.hashCode ^ last.hashCode;
 }
Copy the code

Welcome to the Flutter development group 457664582. Click join to learn and discuss Flutter