Flutter CustomBarChartWidget

Flutter-CustomBarChartWidget

1、Scene

We hold our mobile phone everywhere and almost everytime,naturally a application which can show our usage detail is born.Like this:

  • Daily
    daily
  • Weekly
    daily
  • Monthly
    daily
    -Yearly
    daily
    One of the most vital work is to accomplish the bar chart,and the charts_flutter library is taken into my consideration firstly,and this link seems meet my requirement,but the x axis’s value was limited to date,you can not self-design the x-axis value and y-axis value,so we need to deliver requirement in detail.

2、Requirement

The custom bar chart should be highly self-design,for instance,the x-axis value and y-axis value,the y-axis value’s position,which can be placed at left or right in bar chart,what’s more, the bar’s color and style,how many lines,lines’s color,each lines gap and so on ,which can be set by other widget.

3、Skeleton

Brief skeleton below:

Parent view is a stack,its children can be stacked from bottom to top,this children are bars,lines,y-units,x-units.
Each children can be placed by point exactly,and factors which related to points can be decided by other widgets,including gap between each lines,the x-units number,y-units number which must equal to lines’s.
With the help of CustomPaint class,we can accomplish this canvas by paint method.

4、CustomPainter

CustomPainter,the interface provides a paint method,that we can self-design what we want to draw.
Then we define a BarChartPaint extends CustomPainter class,and make repaint return true,let canvas redraw while new is coming.

5、Coding

Coding is working than words.

(1)Define the critical parameters

  • Draw type
enum DRAW_TYPE { TYPE_LINE, TYPE_X, TYPE_Y, TYPE_BAR }

Four items decide whole chart appearance.

(2) Label

Including x label and y label

class Label {
  String label;
  int color;

  Label(this.label, this.color);
}

It decide label’s color and value.

(3) LinePosition

class LinePosition {
  double x1;
  double y1;

  double x2;
  double y2;

  LinePosition(this.x1, this.y1, this.x2, this.y2);
}

From start to end.

(4) LabelPointPosition

class LabelPointPosition {
  final double x;
  final double y;
  final double value;

  LabelPointPosition(this.x, this.y, this.value);
}

Including label position and value.

(5) LabelPointPosition

enum LabelPosition { POSITION_LEFT, POSITION_RIGHT }

Y-label can be placed at left or right.

(6) BarStyle

class BarStyle {
  final double radius;
  final int color;
  final double barWidth;

  // ignore: invalid_required_param
  const BarStyle(
      @required this.radius, @required this.color, @required this.barWidth);
}

Bar contains radius for four angle and its width and color can be set.

(7)ChartData

class ChartData {
  double yData;
  double xData;

  ChartData(this.xData, this.yData);
}

chart data decides bar’s height and y label value.

(8)ChartData

class Margin {
  final double left;
  final double right;
  final double top;
  final double bottom;

  const Margin(this.left, this.right, this.top, this.bottom);
}

Charts’s margin from all sides is determined.

(9)Start Drawing

The parent widget is a stack,and each children is drew with given parameters.

return GestureDetector(
  child: Container(
    height: widget.lineMargin.top * (widget.lineNum - 1) +
        widget.margin.top +
        widget.margin.bottom +
        widget.heightOffset,
    width: actualWidth,
    color: Colors.white,
    margin: EdgeInsets.only(
        left: widget.margin.left,
        top: widget.margin.top,
        right: widget.margin.right,
        bottom: widget.margin.bottom),
    child: Stack(
      children: drawSkeleton(),
    ),
  ),
  onTap: () {
    widget.onTap("clicked");
  },
);

Container here do a convenience to margin,background,size.And GestureDetector wrap them to respond to tap.

  • drawLine
    line position parameters init below:
_linePosition.clear();
Margin lineMargin = widget.lineMargin;
for (int index = 0; index < widget.lineNum; index++) {
  LinePosition tempPosition = new LinePosition(
      lineMargin.left,
      lineMargin.top * index,
      width - lineMargin.right,
      lineMargin.bottom * index);
  _linePosition.add(tempPosition);
}

if (_linePosition == null ||
    _linePosition.length == 0 ||
    _linePosition.length != widget.yLabels.length) {
  return Text(
    "data init fail",
    style: TextStyle(color: Colors.red),
  );
}

From init method,we can find that line positions are determined by margin,left and right margin control gap from edge of screen,top and bottom margin control distance with each line from top to bottom.

then we can draw line:

for (LinePosition linePosition in _linePosition) {
  if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
    linePosition.x1 = linePosition.x1 + widget.margin.left;
    linePosition.x2 = linePosition.x2 - widget.margin.right;
  } else {
    linePosition.x1 = linePosition.x1 + widget.margin.left;
    linePosition.x2 = linePosition.x2 - widget.margin.right;
  }
  LinePosition curLinePosition = new LinePosition(
      linePosition.x1, linePosition.y1, linePosition.x2, linePosition.y2);
  barChartPaint = new BarChartPaint(
      widget, linePaint, curLinePosition, DRAW_TYPE.TYPE_LINE, null, 0);
  xWidgets.add(CustomPaint(
    painter: barChartPaint,
    willChange: true,
  ));
}

Because BarChartPaint extends CustomPainter,and when it commit a new object of BarChartPaint with painter parameter,it will start draw line from start point to end.

Draw method is easy:

if (DRAW_TYPE.TYPE_LINE == _drawType) {
//      print("x1 = ${_linePosition.x1}" + ",y1 = ${_linePosition.y1}");
  if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
    canvas.drawLine(Offset(_linePosition.x1, _linePosition.y1),
        Offset(_linePosition.x2, _linePosition.y2), _paint);
  } else {
    canvas.drawLine(Offset(_linePosition.x1, _linePosition.y1),
        Offset(_linePosition.x2, _linePosition.y2), _paint);
  }
}
  • draw y label
int positionSize = _linePosition.length;
int yLabelSize = widget.yLabels.length;
if (positionSize == yLabelSize) {
  for (int index = yLabelSize - 1; index >= 0; index--) {
    LinePosition curLinePosition = _linePosition[positionSize - index - 1];
    LabelPointPosition labelPointPosition;
    Label yLabel = widget.yLabels[index];
    if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
      curLinePosition.x1 = curLinePosition.x1 + widget.margin.left;
      labelPointPosition = new LabelPointPosition(curLinePosition.x1,
          curLinePosition.y1, double.parse(yLabel.label));
      curLinePosition.y1 = curLinePosition.y1 - 7.0;
    } else {
      curLinePosition.x2 = curLinePosition.x2 - widget.margin.right;
      labelPointPosition = new LabelPointPosition(curLinePosition.x2,
          curLinePosition.y2, double.parse(yLabel.label));
      curLinePosition.y2 = curLinePosition.y2 - 7.0;
    }
    widget.yLabelPositions.add(labelPointPosition);
    LinePosition tempLinePosition = new LinePosition(curLinePosition.x1,
        curLinePosition.y1, curLinePosition.x2, curLinePosition.y2);
    barChartPaint = new BarChartPaint(widget, linePaint, tempLinePosition,
        DRAW_TYPE.TYPE_Y, yLabel, yLabelSize);
    xWidgets.add(CustomPaint(
      painter: barChartPaint,
      willChange: true,
    ));
  }
}

Y label accompanies line,it locates left or right of line,and can keep a little distance from start or end of line.
Starting draw y label:

if (DRAW_TYPE.TYPE_Y == _drawType) {
  TextSpan span = new TextSpan(
      style: new TextStyle(color: Color(label.color)),
      text: label.label + widget.yLabelUnit);
  TextPainter tp = new TextPainter(
      text: span,
      textAlign: TextAlign.left,
      textDirection: TextDirection.ltr);
  tp.layout();

  if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
    tp.paint(canvas, new Offset(_linePosition.x1, _linePosition.y1));
  } else {
    tp.paint(canvas, new Offset(_linePosition.x2, _linePosition.y2));
  }
}
  • draw x label
int xLabelSize = widget.xLabels.length;
LinePosition lastLinePosition = _linePosition[positionSize - 1];
double xLabelGap = (actualWidth - 18) / xLabelSize;
double initPosition = widget.margin.left;
double yPosition = lastLinePosition.y1;
double xLabelValue = 0;
for (int index = 0; index < xLabelSize; index++) {
  Label xLabel = widget.xLabels[index];
  double xPosition =
      lastLinePosition.x1 + initPosition + index * (xLabelGap);
  LinePosition labelPosition = new LinePosition(xPosition, yPosition, 0, 0);
  barChartPaint = new BarChartPaint(widget, linePaint, labelPosition,
      DRAW_TYPE.TYPE_X, xLabel, xLabelSize);
  if (widget.isMultiData) {
    xLabelValue = double.parse(xLabel.label);
  } else {
    xLabelValue = (index + 1).toDouble();
  }
  LabelPointPosition labelPointPosition =
      new LabelPointPosition(xPosition, yPosition, xLabelValue);
  widget.xLabelPositions.add(labelPointPosition);
  var paint = CustomPaint(
    painter: barChartPaint,
    willChange: true,
  );
  xWidgets.add(paint);
}

X label’s x-axis start position is line’s,and y-axis position is the last y-axis position of line’s.The distance between each x-label is based on the screen width decrease margin and then divide x-label size.The critical parameter is isMultiData,which means whether the x-label is coordinated with value,if true,the bar can be draw between two x-labels,or one x-label by one bar.
Draw x label:

if (DRAW_TYPE.TYPE_X == _drawType) {
  TextSpan span = new TextSpan(
      style: new TextStyle(color: Color(label.color)),
      text: label.label + widget.xLabelUnit);
  TextPainter tp = new TextPainter(
      text: span,
      textAlign: TextAlign.left,
      textDirection: TextDirection.ltr);
  tp.layout();
  tp.paint(canvas, new Offset(_linePosition.x1, _linePosition.y1));
}
  • draw data
if (widget.data != null) {
  barChartPaint = new BarChartPaint(
      widget, barPaint, null, DRAW_TYPE.TYPE_BAR, null, null);
  barChartPaint.barData = widget.data;
  barChartPaint.initDrawData();
  xWidgets.add(CustomPaint(
    painter: barChartPaint,
    willChange: true,
  ));
}

Before drawing bar,we need to invoke initDrawData first,because of the bar position is uncertain.

void initDrawData() {
    xLabelPositions = widget.xLabelPositions;
    yLabelPositions = widget.yLabelPositions;
    if (xLabelPositions != null && xLabelPositions.length > 0) {
      xLabelSize = xLabelPositions.length;
      if (widget.isMultiData) {
        xLabelMaxValue = xLabelPositions[xLabelSize - 1].value;
        xAverageDistance =
            (xLabelPositions[xLabelSize - 1].x - xLabelPositions[0].x) /
                xLabelMaxValue;
        firstXLabelX = xLabelPositions[0].x;
      }
    }
    if (yLabelPositions != null &&
        yLabelPositions.length > 0 &&
        _curData != null &&
        _curData.length > 0) {
      dataSize = _curData.length;
      yLabelSize = yLabelPositions.length;
      yLabelMaxValue = yLabelPositions[0].value;
      yAverageDistance =
          (yLabelPositions[yLabelSize - 1].y - yLabelPositions[0].y) /
              yLabelMaxValue;
      lastYLabelY = yLabelPositions[yLabelSize - 1].y;
    }
}

Pixel Distance coordinate with actual data value is vital,which decide where the bar is. xAverageDistance and yAverageDistance are born before bars to be drew.

xAverageDistance = (x end position - x start position) / x label with max value

yAverageDistance = (y start position - y end position) / y label with max value

Beginning draw data:

if (DRAW_TYPE.TYPE_BAR == _drawType && _curData != null) {
  bool barStyleValid =
      widget.barStyle != null && widget.barStyle.length == dataSize;
  for (int index = 0; index < dataSize; index++) {
    ChartData chartData = _curData[index];
    double yData = chartData.yData;
    if (yData > yLabelMaxValue) {
      break;
    }
    double dataY = 0;
    for (int yIndex = 0; yIndex < yLabelSize; yIndex++) {
      double value = yLabelPositions[yLabelSize - yIndex - 1].value;
      double yLabelY = yLabelPositions[yLabelSize - yIndex - 1].y;
//            print("value = $value" + ",yData = $yData ,yLabelY = $yLabelY");
      if (yData == 0 && value == 0) {
        dataY = lastYLabelY;
        break;
      }
      if (yData <= value) {
        dataY = lastYLabelY - yData * yAverageDistance;
//            print("value = $value" + ",yData = $yData ,yLabelY = $yLabelY");
        break;
      }
    }
    double dataX = 0;
    if (widget.isMultiData) {
      dataX = getXPosition(chartData);
    } else {
      if (index >= xLabelSize) {
        break;
      }
      double xValue = 0;
      for (LabelPointPosition tempXLabelPosition in xLabelPositions) {
        if (tempXLabelPosition.value == chartData.xData) {
          dataX = tempXLabelPosition.x;
          xValue = tempXLabelPosition.value;
          break;
        }
      }
      String valueStr = xValue.toInt().toString();
      int valueLength = valueStr.length;
      double offset = valueLength % 2 == 0
          ? 2 * valueLength.toDouble()
          : valueLength * 3 / 2.0;
      dataX += offset;
    }
    if (dataX < 0) {
      break;
    }
    print("dataX = $dataX" +
        ",dataY = $dataY" +
        ",yData = $yData ,lastYLabelY = $lastYLabelY ,yAverageDistance = $yAverageDistance");
    if (barStyleValid) {
      RRect rRect = new RRect.fromLTRBR(
          dataX,
          dataY,
          dataX + widget.barStyle[index].barWidth,
          lastYLabelY,
          Radius.circular(widget.barStyle[index].radius));
      _paint.color = Color(widget.barStyle[index].color);
      canvas.drawRRect(rRect, _paint);
    } else {
      RRect rRect = new RRect.fromLTRBR(
          dataX,
          dataY,
          dataX + widget.barStyle[0].barWidth,
          lastYLabelY,
          Radius.circular(widget.barStyle[0].radius));
      _paint.color = Color(widget.barStyle[0].color);
      canvas.drawRRect(rRect, _paint);
    }
  }
}

Multi data mode seems complex,

double getXPosition(ChartData chartData) {
    double xData = chartData.xData;
    double dataX = -1;
    for (int xIndex = 0; xIndex < xLabelSize; xIndex++) {
      double value = xLabelPositions[xIndex].value;
      double xLabelX = xLabelPositions[xIndex].x;
    //            print("value = $value" + ",yData = $yData ,yLabelY = $yLabelY");
      if (xData <= value) {
        String valueStr = value.toInt().toString();
        int valueLength = valueStr.length;
        double offset = valueLength % 2 == 0
            ? 2 * valueLength.toDouble()
            : valueLength * 3 / 2.0;
        dataX = firstXLabelX + xData * xAverageDistance + offset;
    //        print("value = $value" + "yLabelY = $xLabelX");
        break;
      }
    }
    return dataX;
}

the codes are a little bit long,the main idea is to clarify how to get bar’s x and y position.If it is not multi mode,the bar’s x-point equal to x-label’s x point the y-point equal to y-average-distance-per-value multi value.And getXPosition method apply to multi mode.

6、Packge Interface

const CustomBarChart({
    Key key,
    this.lineColor = 0x7FA1A1A1,
    this.lineWidth = 1,
    this.lineNum = 3,
    @required this.xLabels,
    @required this.yLabels,
    @required this.data,
    this.onTap,
    this.xLabelUnit = "",
    this.yLabelUnit = "",
    this.yLabelPosition = LabelPosition.POSITION_RIGHT,
    this.barStyle = const [BarStyle(0, 0xFF35CABE, 6)],
    this.xLabelColor = 0xFFBCBCBC,
    this.yLabelColor = 0xFFBCBCBC,
    this.margin = const Margin(0, 0, 0, 0),
    this.isMultiData = false,
    this.lineMargin = const Margin(16, 32, 50, 50),
});

All parameters in this constructor can be customized,and other requirements such as chart data should be above bar,each bar can be touched with different color and so on.

7、How to use

Widget _getSportsDayBarChartView(
      List<SportsTypeDetail> sportsTypeDetailList) {
    var barStyles = [BarStyle(2, 0xFFFF7404, 4)];
    var chartList = _getChartData(sportsTypeDetailList);
    return CustomBarChart(
      xLabels: BarChartCommon.getXLabels(0),
      yLabels: BarChartCommon.getYLabel(chartList, ChartYLabelType.TYPE_STEP),
      data: chartList,
      lineNum: 3,
      yLabelUnit: "",
      isMultiData: true,
      yLabelPosition: LabelPosition.POSITION_RIGHT,
      barStyle: barStyles,
      lineMargin: Margin(16, 32, 64, 64),
      onTap: (value) {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => SingleDaySportsListView(
                sportsTypeDetailList: sportsTypeDetailList),
          ),
        );
      },
    );
  }

8、flutter codes

import 'package:flutter/material.dart';
import 'dart:ui';

///CustomBarChart
class CustomBarChart extends StatefulWidget {
  final int lineColor;
  final double lineWidth;
  final int lineNum;

  final List<Label> xLabels;

  ///y label number should equal to lineNum
  final List<Label> yLabels;
  final List<ChartData> data;
  final String xLabelUnit;
  final String yLabelUnit;
  final LabelPosition yLabelPosition;
  final List<BarStyle> barStyle;

  final int xLabelColor;
  final int yLabelColor;

  final Margin margin;

  ///line left and right margin should consider y unit position
  ///and line top and bottom margin related to gap between each line
  final Margin lineMargin;

  ///true: labels are not coordinate with data,bar can be draw between labels
  ///false:labels are related with data from one by one
  final bool isMultiData;

  static List<LabelPointPosition> _xLabelPositions = [];
  static List<LabelPointPosition> _yLabelPositions = [];

  get xLabelPositions => _xLabelPositions;

  get yLabelPositions => _yLabelPositions;

  final ValueChanged<String> onTap;
  final int heightOffset = 18;

  const CustomBarChart({
    Key key,
    this.lineColor = 0x7FA1A1A1,
    this.lineWidth = 1,
    this.lineNum = 3,
    @required this.xLabels,
    @required this.yLabels,
    @required this.data,
    this.onTap,
    this.xLabelUnit = "",
    this.yLabelUnit = "",
    this.yLabelPosition = LabelPosition.POSITION_RIGHT,
    this.barStyle = const [BarStyle(0, 0xFF35CABE, 6)],
    this.xLabelColor = 0xFFBCBCBC,
    this.yLabelColor = 0xFFBCBCBC,
    this.margin = const Margin(0, 0, 0, 0),
    this.isMultiData = false,
    this.lineMargin = const Margin(16, 32, 50, 50),
  });

  @override
  _CustomBarChartState createState() => _CustomBarChartState();

  void clearLabelPointPosition() {
    _xLabelPositions.clear();
    _yLabelPositions.clear();
  }
}

class _CustomBarChartState extends State<CustomBarChart> {
  Paint linePaint;
  Paint barPaint;
  double width;
  double actualWidth = 0;
  List<LinePosition> _linePosition = [];

  @override
  void initState() {
    super.initState();
    linePaint = new Paint();
    linePaint.color = Color(widget.lineColor);
    linePaint.strokeWidth = widget.lineWidth;

    barPaint = new Paint();
    if (widget.barStyle.length == 1) {
      barPaint.color = Color(widget.barStyle[0].color);
      barPaint.strokeWidth = widget.barStyle[0].barWidth;
    }
  }

  @override
  Widget build(BuildContext context) {
    width = MediaQuery.of(context).size.width;
    if (widget.lineNum == 0 ||
        widget.xLabels == null ||
        widget.xLabels.length == 0 ||
        widget.yLabels == null ||
        widget.yLabels.length == 0 ||
        widget.yLabels.length != widget.lineNum) {
      return Text(
        "data init fail",
        style: TextStyle(color: Colors.red),
      );
    }
    _linePosition.clear();
    Margin lineMargin = widget.lineMargin;
    for (int index = 0; index < widget.lineNum; index++) {
      LinePosition tempPosition = new LinePosition(
          lineMargin.left,
          lineMargin.top * index,
          width - lineMargin.right,
          lineMargin.bottom * index);
      _linePosition.add(tempPosition);
    }

    if (_linePosition == null ||
        _linePosition.length == 0 ||
        _linePosition.length != widget.yLabels.length) {
      return Text(
        "data init fail",
        style: TextStyle(color: Colors.red),
      );
    }
    actualWidth = width - widget.margin.left - widget.margin.right;
    return GestureDetector(
      child: Container(
        height: widget.lineMargin.top * (widget.lineNum - 1) +
            widget.margin.top +
            widget.margin.bottom +
            widget.heightOffset,
        width: actualWidth,
        color: Colors.white,
        margin: EdgeInsets.only(
            left: widget.margin.left,
            top: widget.margin.top,
            right: widget.margin.right,
            bottom: widget.margin.bottom),
        child: Stack(
          children: drawSkeleton(),
        ),
      ),
      onTap: () {
        widget.onTap("clicked");
      },
    );
  }

  List<Widget> drawSkeleton() {
    List<Widget> xWidgets = [];
    BarChartPaint barChartPaint;
    for (LinePosition linePosition in _linePosition) {
      if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
        linePosition.x1 = linePosition.x1 + widget.margin.left;
        linePosition.x2 = linePosition.x2 - widget.margin.right;
      } else {
        linePosition.x1 = linePosition.x1 + widget.margin.left;
        linePosition.x2 = linePosition.x2 - widget.margin.right;
      }
      LinePosition curLinePosition = new LinePosition(
          linePosition.x1, linePosition.y1, linePosition.x2, linePosition.y2);
      barChartPaint = new BarChartPaint(
          widget, linePaint, curLinePosition, DRAW_TYPE.TYPE_LINE, null, 0);
      xWidgets.add(CustomPaint(
        painter: barChartPaint,
        willChange: true,
      ));
    }
    widget.clearLabelPointPosition();
    int positionSize = _linePosition.length;
    int yLabelSize = widget.yLabels.length;
    if (positionSize == yLabelSize) {
      for (int index = yLabelSize - 1; index >= 0; index--) {
        LinePosition curLinePosition = _linePosition[positionSize - index - 1];
        LabelPointPosition labelPointPosition;
        Label yLabel = widget.yLabels[index];
        if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
          curLinePosition.x1 = curLinePosition.x1 + widget.margin.left;
          labelPointPosition = new LabelPointPosition(curLinePosition.x1,
              curLinePosition.y1, double.parse(yLabel.label));
          curLinePosition.y1 = curLinePosition.y1 - 7.0;
        } else {
          curLinePosition.x2 = curLinePosition.x2 - widget.margin.right;
          labelPointPosition = new LabelPointPosition(curLinePosition.x2,
              curLinePosition.y2, double.parse(yLabel.label));
          curLinePosition.y2 = curLinePosition.y2 - 7.0;
        }
        widget.yLabelPositions.add(labelPointPosition);
        LinePosition tempLinePosition = new LinePosition(curLinePosition.x1,
            curLinePosition.y1, curLinePosition.x2, curLinePosition.y2);
        barChartPaint = new BarChartPaint(widget, linePaint, tempLinePosition,
            DRAW_TYPE.TYPE_Y, yLabel, yLabelSize);
        xWidgets.add(CustomPaint(
          painter: barChartPaint,
          willChange: true,
        ));
      }
    }
    int xLabelSize = widget.xLabels.length;
    LinePosition lastLinePosition = _linePosition[positionSize - 1];
    double xLabelGap = (actualWidth - 18) / xLabelSize;
    double initPosition = widget.margin.left;
    double yPosition = lastLinePosition.y1;
    double xLabelValue = 0;
    for (int index = 0; index < xLabelSize; index++) {
      Label xLabel = widget.xLabels[index];
      double xPosition =
          lastLinePosition.x1 + initPosition + index * (xLabelGap);
      LinePosition labelPosition = new LinePosition(xPosition, yPosition, 0, 0);
      barChartPaint = new BarChartPaint(widget, linePaint, labelPosition,
          DRAW_TYPE.TYPE_X, xLabel, xLabelSize);
      if (widget.isMultiData) {
        xLabelValue = double.parse(xLabel.label);
      } else {
        xLabelValue = (index + 1).toDouble();
      }
      LabelPointPosition labelPointPosition =
          new LabelPointPosition(xPosition, yPosition, xLabelValue);
      widget.xLabelPositions.add(labelPointPosition);
      var paint = CustomPaint(
        painter: barChartPaint,
        willChange: true,
      );
      xWidgets.add(paint);
    }

    if (widget.data != null) {
      barChartPaint = new BarChartPaint(
          widget, barPaint, null, DRAW_TYPE.TYPE_BAR, null, null);
      barChartPaint.barData = widget.data;
      barChartPaint.initDrawData();
      xWidgets.add(CustomPaint(
        painter: barChartPaint,
        willChange: true,
      ));
    }

    return xWidgets;
  }
}

class BarChartPaint extends CustomPainter {
  final Paint _paint;
  final LinePosition _linePosition;
  final DRAW_TYPE _drawType;
  final Label label;
  final int labelSize;
  final CustomBarChart widget;

  BarChartPaint(this.widget, this._paint, this._linePosition, this._drawType,
      this.label, this.labelSize);

  List<ChartData> _curData;
  int xLabelSize = 0;
  double xLabelMaxValue = 0;
  double xAverageDistance = 0;
  double firstXLabelX = 0;
  List<LabelPointPosition> xLabelPositions;
  List<LabelPointPosition> yLabelPositions;

  int dataSize = 0;
  int yLabelSize = 0;
  double yLabelMaxValue = 0;
  double yAverageDistance = 0;
  double lastYLabelY = 0;

  set barData(List<ChartData> data) {
    _curData = data;
  }

  void initDrawData() {
    xLabelPositions = widget.xLabelPositions;
    yLabelPositions = widget.yLabelPositions;
    if (xLabelPositions != null && xLabelPositions.length > 0) {
      xLabelSize = xLabelPositions.length;
      if (widget.isMultiData) {
        xLabelMaxValue = xLabelPositions[xLabelSize - 1].value;
        xAverageDistance =
            (xLabelPositions[xLabelSize - 1].x - xLabelPositions[0].x) /
                xLabelMaxValue;
        firstXLabelX = xLabelPositions[0].x;
      }
    }
    if (yLabelPositions != null &&
        yLabelPositions.length > 0 &&
        _curData != null &&
        _curData.length > 0) {
      dataSize = _curData.length;
      yLabelSize = yLabelPositions.length;
      yLabelMaxValue = yLabelPositions[0].value;
      yAverageDistance =
          (yLabelPositions[yLabelSize - 1].y - yLabelPositions[0].y) /
              yLabelMaxValue;
      lastYLabelY = yLabelPositions[yLabelSize - 1].y;
    }
  }

  double getXPosition(ChartData chartData) {
    double xData = chartData.xData;
    double dataX = -1;
    for (int xIndex = 0; xIndex < xLabelSize; xIndex++) {
      double value = xLabelPositions[xIndex].value;
      double xLabelX = xLabelPositions[xIndex].x;
//            print("value = $value" + ",yData = $yData ,yLabelY = $yLabelY");
      if (xData <= value) {
        String valueStr = value.toInt().toString();
        int valueLength = valueStr.length;
        double offset = valueLength % 2 == 0
            ? 2 * valueLength.toDouble()
            : valueLength * 3 / 2.0;
        dataX = firstXLabelX + xData * xAverageDistance + offset;
//        print("value = $value" + "yLabelY = $xLabelX");
        break;
      }
    }
    return dataX;
  }

  @override
  void paint(Canvas canvas, Size size) {
    if (DRAW_TYPE.TYPE_LINE == _drawType) {
//      print("x1 = ${_linePosition.x1}" + ",y1 = ${_linePosition.y1}");
      if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
        canvas.drawLine(Offset(_linePosition.x1, _linePosition.y1),
            Offset(_linePosition.x2, _linePosition.y2), _paint);
      } else {
        canvas.drawLine(Offset(_linePosition.x1, _linePosition.y1),
            Offset(_linePosition.x2, _linePosition.y2), _paint);
      }
    }
    if (DRAW_TYPE.TYPE_Y == _drawType) {
      TextSpan span = new TextSpan(
          style: new TextStyle(color: Color(label.color)),
          text: label.label + widget.yLabelUnit);
      TextPainter tp = new TextPainter(
          text: span,
          textAlign: TextAlign.left,
          textDirection: TextDirection.ltr);
      tp.layout();

      if (widget.yLabelPosition == LabelPosition.POSITION_LEFT) {
        tp.paint(canvas, new Offset(_linePosition.x1, _linePosition.y1));
      } else {
        tp.paint(canvas, new Offset(_linePosition.x2, _linePosition.y2));
      }
    }
    if (DRAW_TYPE.TYPE_X == _drawType) {
      TextSpan span = new TextSpan(
          style: new TextStyle(color: Color(label.color)),
          text: label.label + widget.xLabelUnit);
      TextPainter tp = new TextPainter(
          text: span,
          textAlign: TextAlign.left,
          textDirection: TextDirection.ltr);
      tp.layout();
      tp.paint(canvas, new Offset(_linePosition.x1, _linePosition.y1));
    }
    if (DRAW_TYPE.TYPE_BAR == _drawType && _curData != null) {
      bool barStyleValid =
          widget.barStyle != null && widget.barStyle.length == dataSize;
      for (int index = 0; index < dataSize; index++) {
        ChartData chartData = _curData[index];
        double yData = chartData.yData;
        if (yData > yLabelMaxValue) {
          break;
        }
        double dataY = 0;
        for (int yIndex = 0; yIndex < yLabelSize; yIndex++) {
          double value = yLabelPositions[yLabelSize - yIndex - 1].value;
          double yLabelY = yLabelPositions[yLabelSize - yIndex - 1].y;
//            print("value = $value" + ",yData = $yData ,yLabelY = $yLabelY");
          if (yData == 0 && value == 0) {
            dataY = lastYLabelY;
            break;
          }
          if (yData <= value) {
            dataY = lastYLabelY - yData * yAverageDistance;
//            print("value = $value" + ",yData = $yData ,yLabelY = $yLabelY");
            break;
          }
        }
        double dataX = 0;
        if (widget.isMultiData) {
          dataX = getXPosition(chartData);
        } else {
          if (index >= xLabelSize) {
            break;
          }
          double xValue = 0;
          for (LabelPointPosition tempXLabelPosition in xLabelPositions) {
            if (tempXLabelPosition.value == chartData.xData) {
              dataX = tempXLabelPosition.x;
              xValue = tempXLabelPosition.value;
              break;
            }
          }
          String valueStr = xValue.toInt().toString();
          int valueLength = valueStr.length;
          double offset = valueLength % 2 == 0
              ? 2 * valueLength.toDouble()
              : valueLength * 3 / 2.0;
          dataX += offset;
        }
        if (dataX < 0) {
          break;
        }
        print("dataX = $dataX" +
            ",dataY = $dataY" +
            ",yData = $yData ,lastYLabelY = $lastYLabelY ,yAverageDistance = $yAverageDistance");
        if (barStyleValid) {
          RRect rRect = new RRect.fromLTRBR(
              dataX,
              dataY,
              dataX + widget.barStyle[index].barWidth,
              lastYLabelY,
              Radius.circular(widget.barStyle[index].radius));
          _paint.color = Color(widget.barStyle[index].color);
          canvas.drawRRect(rRect, _paint);
        } else {
          RRect rRect = new RRect.fromLTRBR(
              dataX,
              dataY,
              dataX + widget.barStyle[0].barWidth,
              lastYLabelY,
              Radius.circular(widget.barStyle[0].radius));
          _paint.color = Color(widget.barStyle[0].color);
          canvas.drawRRect(rRect, _paint);
        }
      }
    }
  }

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

enum DRAW_TYPE { TYPE_LINE, TYPE_X, TYPE_Y, TYPE_BAR }

class Label {
  String label;
  int color;

  Label(this.label, this.color);
}

class LinePosition {
  double x1;
  double y1;

  double x2;
  double y2;

  LinePosition(this.x1, this.y1, this.x2, this.y2);
}

class LabelPointPosition {
  final double x;
  final double y;
  final double value;

  LabelPointPosition(this.x, this.y, this.value);
}

enum LabelPosition { POSITION_LEFT, POSITION_RIGHT }

class BarStyle {
  final double radius;
  final int color;
  final double barWidth;

  // ignore: invalid_required_param
  const BarStyle(
      @required this.radius, @required this.color, @required this.barWidth);
}

class ChartData {
  double yData;
  double xData;

  ChartData(this.xData, this.yData);
}

class Margin {
  final double left;
  final double right;
  final double top;
  final double bottom;

  const Margin(this.left, this.right, this.top, this.bottom);
}

9、java codes

package com.example.view;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;

import com.qiku.healthguard.R;
import com.qiku.healthguard.bean.ChartDataBean;
import com.qiku.healthguard.sport.common.util.SDKUtils;
import com.qiku.healthguard.util.SystemDataUtil;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by chuck chan-iri on 2017/5/19.
 */

public class CustomBarChartView extends View {
    public final static int TYPE_TIME = 0;
    public final static int TYPE_WEEK = 1;
    public final static int TYPE_MONTH = 2;
    public final static int TYPE_YEAR = 3;

    private static DisplayMetrics mMetrics;
    float yMaxData, yDataForAveragePosition;
    private String TAG = "HG-CustomBarChartView";
    private float offsetLeft = 18;
    private float offsetRight = 18;
    private float offsetBottom = 20;
    private float leftOffsetInPixel = 0;
    private float bottomOffsetInPixel = 0;
    private int width = 0;
    private int height = 0;
    private int positionLeft = 0;
    private int positionRight = 0;
    private int positionTop = 0;
    private int positionBottom = 0;
    private float xGap, yGap, xStartPosition, xEndPosition, yStartPosition,
            yEndPosition, yBaseStartPosition, yBaseEndPosition, rectGap;
    private Paint xPaint = null;
    private Paint yPaint = null;
    private Paint linePaint = null;
    private Paint rectPaint = null;
    private List<String> xItem;
    private int xType = -1;
    private List<String> yItem;
    private int lineColor = R.color.color_A1;
    private int labelColor = R.color.color_BC;
    private List<ChartDataBean> dataList;
    private RectF drawRectF;
    private float rectWidth = 4f;
    private int rectColor = R.color.color_35;
    private Map<String, Point> pointMap = new HashMap<>();
    private boolean isMultiData = false;
    private String[] WEEK_X_ARRAY = null;
    private String yLabelUnit = "h";
    //draw x label only for empty data set
    private boolean isDrawXLabelOnly = false;

    public CustomBarChartView(Context context) {
        this(context, null);
    }

    public CustomBarChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Resources res = context.getResources();
        mMetrics = res.getDisplayMetrics();
        xPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        float xLabelSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                10, mMetrics);
        xPaint.setTextSize(xLabelSize);
        xPaint.setColor(getResources().getColor(labelColor));
        yPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        float yLabelSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                10, mMetrics);
        yPaint.setTextSize(yLabelSize);
        yPaint.setColor(getResources().getColor(labelColor));
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setColor(getResources().getColor(lineColor));
        rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        rectPaint.setColor(getResources().getColor(rectColor));
        leftOffsetInPixel = dp2Pixel(offsetLeft);
        bottomOffsetInPixel = dp2Pixel(offsetBottom);
        rectWidth = dp2Pixel(rectWidth);

        drawRectF = new RectF();
        setWeekArray(res);
    }

    private void setWeekArray(Resources resource) {
        WEEK_X_ARRAY = new String[]{
                resource.getString(R.string.week_1), resource.getString(R.string.week_2),
                resource.getString(R.string.week_3), resource.getString(R.string.week_4),
                resource.getString(R.string.week_5), resource.getString(R.string.week_6),
                resource.getString(R.string.week_7)};
    }

    public List<ChartDataBean> getData() {
        return dataList;
    }

    public void setData(List<ChartDataBean> list) {
        dataList = list;
    }

    private float dp2Pixel(float dp) {
        return dp * mMetrics.density;
    }

    public void setXLabelData(List<String> xItem) {
        this.xItem = xItem;
    }

    public void setXLabelDataType(int type) {
        xType = type;
    }

    public void setYLabelData(List<String> yItem) {
        this.yItem = yItem;
    }

    public void setRectWidth(int rectWidth) {
        this.rectWidth = dp2Pixel(rectWidth);
    }

    public void setRectColor(int rectColor) {
        this.rectColor = rectColor;
        rectPaint.setColor(rectColor);
    }

    public void setIsMultiData(boolean isMultiData) {
        this.isMultiData = isMultiData;
    }

    public void setyLabelUnit(String yLabelUnit) {
        this.yLabelUnit = yLabelUnit;
    }

    private String getXLabel(String label, int index) {
        if (index > xItem.size()) {
            return "";
        }
        if (xType == TYPE_TIME) {
            if (label.equals("24")) {
                label = "0";
            }
            return label;
        } else if (xType == TYPE_WEEK) {
            return WEEK_X_ARRAY[index];
        } else {
            return xItem.get(index);
        }
    }

    public void setDrawXLabelOnly(boolean drawXLabelOnly) {
        isDrawXLabelOnly = drawXLabelOnly;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        positionLeft = 0;
        positionTop = 0;
        positionRight = positionLeft + width;
        positionBottom = positionTop + height;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (xItem == null || yItem == null) {
            return;
        }
        pointMap.clear();
        xStartPosition = positionLeft + leftOffsetInPixel;
        xEndPosition = positionRight - leftOffsetInPixel;
        xGap = (xEndPosition - xStartPosition) / (xItem.size());

        yStartPosition = positionBottom - positionTop;
        yEndPosition = 0;
        yBaseStartPosition = yStartPosition - getResources().getDimensionPixelOffset(R.dimen.view_margin_40dp);
        yBaseEndPosition = yEndPosition;

        if (isDrawXLabelOnly) {
            drawX(canvas);
            return;
        }
        if (dataList == null || dataList.size() == 0) {
            return;
        }

        yMaxData = Float.parseFloat(yItem.get(yItem.size() - 1));
        yDataForAveragePosition = (yBaseStartPosition - yBaseEndPosition) / yMaxData;
        yGap = (yBaseStartPosition - yBaseEndPosition) / (yItem.size() - 1);

        float yBaseStartPositionTmp = yBaseStartPosition;
        try {
            for (ChartDataBean mChartDataBean : dataList) {
                if (mChartDataBean.getDataY() <= 0 || mChartDataBean.getResIds() == null || mChartDataBean.getResIds().size() <= 0)
                    continue;
                if (mChartDataBean.getResIds() != null && mChartDataBean.getResIds().size() >= 1) {
                    for (Integer resId : mChartDataBean.getResIds()) {
                        Bitmap bitmap = SystemDataUtil.getBitmap(mContext, resId);
                        if (bitmap == null) {
                            continue;
                        }
                        yBaseStartPositionTmp -= bitmap.getHeight() + SDKUtils.dip2px(mContext, 4);
                    }
                }
                float tmp = (yBaseStartPositionTmp - yBaseEndPosition) / mChartDataBean.getDataY();
                if (yDataForAveragePosition > tmp) yDataForAveragePosition = tmp;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        drawX(canvas);
        drawY(canvas);

        drawData(canvas);
    }

    private void drawX(Canvas canvas) {
        float xBasePosition = 0f;
        int xItemSize = xItem.size();
        if (xType == TYPE_YEAR) {
            xBasePosition = xStartPosition + getResources().getDimensionPixelOffset(R.dimen.view_margin_4dp);
        } else if (xType == TYPE_MONTH || xType == TYPE_WEEK) {
            xBasePosition = xStartPosition + getResources().getDimensionPixelOffset(R.dimen.view_margin_12dp);
        } else {
            xBasePosition = xStartPosition + getResources().getDimensionPixelOffset(R.dimen.view_margin_24dp);
        }
        float yBasePosition = yStartPosition - bottomOffsetInPixel;
        for (int i = 0; i < xItemSize; ++i) {
            String tempXItem = xItem.get(i);
            float x = xBasePosition + xGap * i;
            float y = yBasePosition;
//            Log.d(TAG, "x = " + x + ",y = " + y);
            pointMap.put(tempXItem, new Point((int) x, (int) y));
            tempXItem = getXLabel(tempXItem, i);
            canvas.drawText(tempXItem + "", x, y, xPaint);
        }
    }

    private void drawData(Canvas canvas) {
        int nextKeyValue = -1;
        int previousValue = -1;
        for (int j = 0; j < dataList.size(); ++j) {
            ChartDataBean data = dataList.get(j);
            int dataOne = data.getDataX();
            float dataTwo = data.getDataY();
//            MyLog.d(TAG, "drawData dataOne = " + dataOne + ",dataTwo = " + dataTwo);

            float left = 0f;
            float top = 0f;
            float right = 0f;
            float bottom = 0f;
            boolean canDraw = false;
            int[] dataGap = getProperValueGap(dataOne);
            previousValue = dataGap[0];
            nextKeyValue = dataGap[1];

            Point tempPoint = pointMap.get("" + previousValue);
            if (tempPoint == null) {
                continue;
            }
            int x = tempPoint.x;
            int y = tempPoint.y;
            if (isMultiData) {
                if (dataOne >= previousValue && dataOne <= nextKeyValue) {
                    canDraw = true;
                }
                rectGap = (xGap - (rectWidth * (nextKeyValue - previousValue))) / (nextKeyValue - previousValue);
                left = x + (dataOne - previousValue) * (rectWidth + rectGap);
                top = yBaseStartPosition - yDataForAveragePosition * dataTwo;
                right = left + rectWidth;
                bottom = yBaseStartPosition;
            } else {
                canDraw = true;
                left = x;
                top = yBaseStartPosition - yDataForAveragePosition * dataTwo;
                right = left + rectWidth;
                bottom = yBaseStartPosition;
            }
            if (canDraw && dataTwo >= 0f) {
                drawRectF.set(left, top, right, bottom);
                canvas.drawRoundRect(drawRectF, 12f, 12f, rectPaint);
            }
            try {
                if (data.getResIds() != null && data.getResIds().size() >= 1) {
                    for (Integer resId : data.getResIds()) {
                        Bitmap bitmap = SystemDataUtil.getBitmap(mContext, resId);
                        if (bitmap == null) {
                            continue;
                        }
                        float mBpWidth = bitmap.getWidth() / 2f;
                        top = top - bitmap.getHeight() - SDKUtils.dip2px(mContext, 4);
                        canvas.drawBitmap(bitmap, left + (rectWidth / 2) - mBpWidth, top, null);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private int[] getProperValueGap(int currentData) {
        int nextKeyValue = -1;
        int previousValue = -1;
        int size = xItem.size();
        if (!isMultiData) {
            return new int[]{currentData, currentData};
        }
        for (int i = 0; i < xItem.size(); ++i) {
            String tempLabel = xItem.get(i);
            int nextKey = i + 1;
            int curHour = Integer.parseInt(tempLabel);
            if (nextKey < size) {
                previousValue = curHour;
                nextKeyValue = Integer.parseInt(xItem.get(nextKey));
            } else {
                nextKeyValue = curHour;
                if (currentData == curHour && i == size - 1) {
                    previousValue = Integer.parseInt(xItem.get(i - 1));
                } else {
                    previousValue = Integer.parseInt(xItem.get(nextKey - 1));
                }
            }
            if (currentData >= previousValue && currentData < nextKeyValue) {
                break;
            }
        }
//        MyLog.d(TAG, "getProperValueGap currentData = " + currentData + ",previousValue = " + previousValue + ",nextKeyValue = " + nextKeyValue);
        return new int[]{previousValue, nextKeyValue};
    }

    private void drawY(Canvas canvas) {
        int offset = SDKUtils.dip2px(getContext(), 2);
        for (int i = 0; i < yItem.size(); ++i) {
            String tempYItem = yItem.get(i);
            float yLinePosition = yBaseStartPosition - yGap * i;

            Rect rect = new Rect();
            yPaint.getTextBounds((tempYItem + yLabelUnit), 0, (tempYItem + yLabelUnit).length(), rect);
            int w = rect.width();
            int h = rect.height();

            canvas.drawText(tempYItem + yLabelUnit, xEndPosition - w + offset, yLinePosition + offset + h, yPaint);
//            Log.d(TAG, "drawY x = " + xEndPosition + ",yLinePosition = " + yLinePosition);
            canvas.drawLine(xStartPosition, yLinePosition, xEndPosition, yLinePosition, linePaint);
        }
    }
}

Article will be synced to wechat blog:Android部落格

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值