Flutter繪製barchart

river發表於2021-03-04

Flutter繪製linechart

專案地址

HiCoordinate

import 'package:flutter/material.dart';
import 'package:hi_chart/src/bean/chart_bean.dart';
import 'package:hi_chart/src/constant.dart' as constant;
import 'dart:math' as math;

import 'package:hi_chart/src/util/axis_util.dart';

typedef HiCoordinateBuilder = Widget Function(BuildContext context, int cormax, int cormin, double translateX);

class HiCoordinate extends StatefulWidget {
  final HiCoordinateBuilder builder;
  final double width;
  final double height;
  final List<ChartBean> data;
  final Color axisColor;
  final TextStyle labelStyle;
  final double axisWidth;
  final VoidCallback onHorizontalDrag;

  const HiCoordinate({
    @required this.data,
    this.builder,
    this.width,
    this.height,
    this.axisColor = constant.axisColor,
    this.labelStyle,
    this.axisWidth = 1,
    this.onHorizontalDrag,
    Key key,
  }) : super(key: key);

  @override
  _HiCoordinateState createState() => _HiCoordinateState();
}

class _HiCoordinateState extends State<HiCoordinate> {
  num maxValue; //最大值
  num minValue; //最小值
  int cormax, cormin; //優化後最大值、最小值
  int step; //優化後步數大小
  int cornumber; //優化後Y軸分割數

  double translateX = 0; //偏移量
  double maxTranslateX; //最大偏移量
  double startX; //手勢臨時變數
  double tempTranslateX; //手勢臨時變數

  @override
  void initState() {
    super.initState();
    initValue();
    initAxisData();
  }

  initValue() {
    minValue = widget.data[0].value;
    maxValue = widget.data[0].value;
    for (final item in widget.data) {
      maxValue = math.max(item.value, maxValue);
      minValue = math.min(item.value, minValue);
    }
  }

  initAxisData() {
    final yAxisData = AxisUtils.mathYAxis(maxValue, minValue, constant.cornumber);
    cormax = yAxisData[0];
    cormin = yAxisData[1];
    cornumber = yAxisData[2];
    step = yAxisData[3];
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width ?? double.infinity,
      height: widget.height,
      child: Stack(
        children: [
          Container(
            width: double.infinity,
            height: double.infinity,
            child: CustomPaint(
              painter: _HiCoordinatePainter(
                data: widget.data,
                cornumber: cornumber,
                step: step,
                axisColor: widget.axisColor,
                axisWidth: widget.axisWidth,
                labelStyle: widget.labelStyle,
                translateX: translateX,
              ),
            ),
          ),
          if (widget.builder != null)
            Positioned.fill(
              child: LayoutBuilder(
                builder: (context, viewport) {
                  final out = (widget.data.length + 1) * constant.pointSpace - viewport.maxWidth;
                  maxTranslateX = math.max(0, out);

                  return GestureDetector(
                    onHorizontalDragStart: _onHorizontalDragStart,
                    onHorizontalDragUpdate: _onHorizontalDragUpdate,
                    onHorizontalDragEnd: _onHorizontalDragEnd,
                    child: widget.builder(context, cormax, cormin, translateX),
                  );
                },
              ),
              bottom: constant.xLabelHeight,
            ),
        ],
      ),
    );
  }

  /*
    更新偏移量
   */
  void _onHorizontalDragStart(DragStartDetails details) {
    startX = details.globalPosition.dx;
    tempTranslateX = translateX;
  }

  void _onHorizontalDragUpdate(DragUpdateDetails details) {
    final distanceX = details.globalPosition.dx - startX;
    final tempX = tempTranslateX + distanceX;
    if (tempX > 0 || tempX.abs() > maxTranslateX) return;

    setState(() {
      translateX = tempX;
    });

    if (widget.onHorizontalDrag != null) widget.onHorizontalDrag();
  }

  void _onHorizontalDragEnd(DragEndDetails details) {
    startX = null;
    tempTranslateX = null;
  }

/*
    更新偏移量
   */
}

class _HiCoordinatePainter extends CustomPainter {
  final List<ChartBean> data;
  final int cornumber;
  final Color axisColor;
  final double axisWidth;
  final TextStyle labelStyle;
  final double translateX;
  final int step;

  Paint axisPaint; //座標軸畫筆

  _HiCoordinatePainter({
    @required this.data,
    @required this.cornumber,
    @required this.step,
    this.axisColor,
    this.axisWidth,
    this.labelStyle,
    this.translateX = 0,
  }) {
    init();
  }

  void init() {
    axisPaint = Paint()
      ..color = axisColor
      ..strokeWidth = axisWidth
      ..style = PaintingStyle.stroke;
  }

  @override
  void paint(Canvas canvas, Size size) {
    //調整畫布為正常笛卡爾座標系
    canvas.save();
    canvas.scale(1, -1);
    canvas.translate(0, -size.height + constant.xLabelHeight);

    final axisSize = Size(size.width, size.height - constant.xLabelHeight);

    drawAxis(canvas, axisSize);
    drawAxisLabel(canvas, axisSize);

    canvas.restore();
  }

  //繪製座標軸
  void drawAxis(Canvas canvas, Size size) {
    //繪製X軸
    final xAxisPath = Path();
    xAxisPath.lineTo(size.width, 0);

    canvas.drawPath(xAxisPath, axisPaint);

    //繪製Y軸
    final yAxisPath = Path();
    yAxisPath.lineTo(0, size.height);

    canvas.drawPath(yAxisPath, axisPaint);
  }

  //繪製座標軸標籤
  void drawAxisLabel(Canvas canvas, Size size) {
    canvas.save();
    canvas.scale(1, -1);
    canvas.clipRect(Rect.fromLTWH(constant.pointSpace / 2, -constant.xLabelHeight, size.width - constant.pointSpace / 2, size.height));

    final labelTextStyle = labelStyle ?? TextStyle(fontSize: 14, color: axisColor);

    for (final item in data) {
      final index = data.indexOf(item) + 1;
      final offset = Offset(index * constant.pointSpace + translateX, 2);

      TextPainter(
        text: TextSpan(
          text: item.title,
          style: labelTextStyle,
        ),
        textDirection: TextDirection.ltr,
        textAlign: TextAlign.center,
      )
        ..layout(minWidth: constant.pointSpace, maxWidth: constant.pointSpace)
        ..paint(canvas, offset);
    }
    canvas.restore();

    canvas.save();
    canvas.scale(1, -1);

    final ySpace = size.height / cornumber;

    for (int i = 0; i <= cornumber; i++) {
      final labelNumber = i * step;
      final offset = Offset(5, -ySpace * i);

      TextPainter(
        text: TextSpan(text: '$labelNumber', style: labelTextStyle),
        textDirection: TextDirection.ltr,
      )
        ..layout(minWidth: constant.yLabelWidth, maxWidth: constant.yLabelWidth)
        ..paint(canvas, offset);
    }

    canvas.restore();
  }

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

複製程式碼

該widget負責繪製座標系及作為內容的承載

HiBarChart

import 'dart:async';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:hi_chart/src/bean/chart_bean.dart';
import 'package:hi_chart/src/constant.dart' as constant;
import 'package:hi_chart/src/util/util.dart';
import 'package:hi_chart/src/widget/hi_coordinate.dart';

class HiBarChart extends StatefulWidget {
  final List<ChartBean> data; //資料集
  final double width; //寬度
  final double height; //高度
  final Color lineColor; //線、點顏色
  final Color axisColor; //座標軸顏色
  final TextStyle axisLabelStyle; //座標軸標籤字型樣式
  final double axisWidth; // 座標軸寬度
  final double lineWidth; //線寬度

  const HiBarChart({
    @required this.data,
    this.width = double.infinity,
    this.height,
    this.axisColor = const Color(0xffe6e6e6),
    this.lineColor,
    this.axisLabelStyle,
    this.axisWidth = 1,
    this.lineWidth = 1,
    Key key,
  }) : super(key: key);

  @override
  _HiBarChart createState() => _HiBarChart();
}

class _HiBarChart extends State<HiBarChart> with TickerProviderStateMixin {
  List<Rect> rects = []; //點集合

  String tipTitle; //提示標題
  String tipValue; //提示資料值
  double tipTop; //提示頂部距離
  double tipLeft; //提示底部距離
  bool isShowTip = false; //是否顯示提示
  Timer tipTimer; //自動關閉提示
  GlobalKey tipKey = GlobalKey(); //提示key

  GlobalKey contentKey = GlobalKey(); //內容區域

  AnimationController initAnimationController; //初始化動畫

  AnimationController changeAnimationController; //資料變更動畫

  List<DiffValue> changeList; //資料變更資料集

  @override
  void initState() {
    super.initState();

    initAnimationController = AnimationController(duration: Duration(seconds: 1), vsync: this)
      ..addListener(() {
        setState(() {});
      });

    changeAnimationController = AnimationController(duration: Duration(milliseconds: 300), vsync: this)
      ..addListener(() {
        setState(() {});
      });

    initAnimationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return HiCoordinate(
      data: widget.data,
      width: widget.width,
      height: widget.height,
      axisColor: widget.axisColor,
      axisWidth: widget.axisWidth,
      labelStyle: widget.axisLabelStyle,
      onHorizontalDrag: () {
        hideTip();
      },
      builder: (context, cormax, cormin, translateX) => GestureDetector(
        onTapUp: _onTapUp,
        child: Stack(
          overflow: Overflow.visible,
          children: [
            Positioned.fill(
              child: Container(
                child: CustomPaint(
                  key: contentKey,
                  willChange: true,
                  painter: _HiBarChartPaint(
                    context: context,
                    data: widget.data,
                    translateX: translateX,
                    cormax: cormax,
                    cormin: cormin,
                    lineColor: widget.lineColor,
                    lineWidth: widget.lineWidth,
                    onRectsChanged: (rects) {
                      this.rects = rects;
                    },
                    progress: initAnimationController.value,
                    changeList: changeList,
                    changeProgress: changeAnimationController.value,
                  ),
                ),
              ),
            ),
            Positioned(
              top: isShowTip ? tipTop : -99999,
              left: tipLeft,
              child: Container(
                key: tipKey,
                padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                constraints: BoxConstraints(minWidth: 60),
                decoration: BoxDecoration(
                  color: Colors.black45,
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      tipTitle ?? '',
                      style: const TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
                    ),
                    Text(
                      tipValue ?? '',
                      style: const TextStyle(fontSize: 14, color: Colors.white),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _onTapUp(TapUpDetails details) {
    final width = contentKey.currentContext.size.width;
    final height = contentKey.currentContext.size.height;

    final localPosition = details.localPosition;
    final transerPosition = Offset(localPosition.dx, height - localPosition.dy);

    //是否點選到了點
    final finder = rects?.firstWhere((rect) => rect.contains(transerPosition), orElse: () => null);

    if (finder != null) {
      final index = rects.indexOf(finder);
      tipTitle = widget.data[index].title;
      tipValue = widget.data[index].value.toString();

      setState(() {
        isShowTip = true;
        autoCloseTip();
      });

      tipTop = localPosition.dy;

      if (localPosition.dx + tipKey.currentContext.size.width > width) {
        tipLeft = localPosition.dx - (localPosition.dx + tipKey.currentContext.size.width - width);
      } else {
        tipLeft = localPosition.dx;
      }

      setState(() {});
    } else {
      hideTip();
    }
  }

  //關閉提示計時器
  void cancelTipTimer() {
    if (tipTimer != null && tipTimer.isActive) {
      tipTimer.cancel();
    }
  }

  //自動延遲關閉提示
  void autoCloseTip() {
    cancelTipTimer();

    tipTimer = Timer(Duration(seconds: 3), hideTip);
  }

  //關閉提示
  void hideTip() {
    if (isShowTip) {
      setState(() {
        isShowTip = false;
      });
    }
  }

  @override
  void didUpdateWidget(covariant HiBarChart oldWidget) {
    super.didUpdateWidget(oldWidget);

    //更新資料集
    if (oldWidget.data != widget.data) {
      changeList = Util.compareDiffData(oldWidget.data, widget.data);
      changeAnimationController.forward(from: 0);
    }
  }

  @override
  void dispose() {
    initAnimationController.dispose();

    changeAnimationController.dispose();

    cancelTipTimer();
    super.dispose();
  }
}

class _HiBarChartPaint extends CustomPainter {
  final List<ChartBean> data;
  final BuildContext context;

  final Color lineColor;
  final lineWidth;

  final ValueChanged<List<Rect>> onRectsChanged;
  final double translateX;

  final int cormax, cormin;

  final double progress; //初始化動畫進度
  final double changeProgress; //資料集變更動畫進度
  final List<DiffValue> changeList;

  Paint _barPaint;

  _HiBarChartPaint({
    @required this.context,
    @required this.data,
    this.translateX = 0,
    this.onRectsChanged,
    this.lineWidth,
    this.lineColor,
    this.cormin,
    this.cormax,
    this.progress,
    this.changeList,
    this.changeProgress,
  }) {
    init();
  }

  init() {
    _barPaint = Paint()
      ..color = lineColor ?? Theme.of(context).primaryColor
      ..strokeWidth = lineWidth + 4
      ..style = PaintingStyle.fill
      ..strokeCap = StrokeCap.round;
  }

  @override
  void paint(Canvas canvas, Size size) {
    canvas.save();
    //調整畫布為正常笛卡爾座標系
    canvas.scale(1, -1);
    canvas.translate(0, -size.height);

    _drawBar(canvas, size);

    canvas.restore();
  }

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

  //繪製柱形圖
  void _drawBar(Canvas canvas, Size size) {
    canvas.save();
    canvas.clipRect(Rect.fromLTWH(constant.pointSpace / 2, 0, size.width - constant.pointSpace / 2, size.height));

    List<Rect> rects = [];

    for (final item in data) {
      final index = data.indexOf(item);

      double height = size.height * item.value / (cormax - cormin) * progress;

      final finder = changeList?.firstWhere((element) => (element.index == index), orElse: () => null);

      if (finder != null) {
        final oldHeight = size.height * finder.value / (cormax - cormin) * progress;
        height = oldHeight + (height - oldHeight) * changeProgress;
      }

      Rect rect = Rect.fromLTWH((index + 1) * constant.pointSpace + constant.pointSpace / 2 + translateX, 0, constant.pointSpace / 2, height);

      canvas.drawRect(rect, _barPaint);

      rects.add(rect);
    }

    canvas.restore();

    if (onRectsChanged != null) {
      onRectsChanged(rects);
    }
  }
}

複製程式碼

該類負責繪製柱狀圖內容區域及相關點選事件

相關文章