Flutter--手动绘制k线图的思路

本文介绍了如何使用Flutter实现一个数据可视化页面,通过`CustomPaint`组件动态绘制大量数据,并仅渲染局部区域,提高了性能。关键部分包括`PaintPage`和`PaintState`的实现,以及滚动监听和数据分块显示策略。

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


import 'dart:math';

import 'package:flutter/material.dart';
import 'package:ripp_link/page/base/base_state_page.dart';
import 'package:ripp_link/widget/base/dialog_box/tips_card.dart';

class PaintPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _PaintState();

}

class _PaintState extends State<PaintPage> {

  List<DataItem> items = [];

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

    for (int i = 0; i < 1000; i++) {
      int random1 = Random.secure().nextInt(100);
      int random2 = Random.secure().nextInt(100);
      items.add(DataItem("item:$i", min(random1, random2), max(random1, random2)));
    }

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: SizedBox(width: items.length * 8.0, height: 300, child: GestureDetector(
          child: CustomPaint(
            painter: _Painter(items),
          ),
          onTapDown: (details) {
            updateDx(getPoint(context, details.globalPosition), false);
          },
          onTapUp: (details) {
            // updateDx(getPoint(context, details.globalPosition), true);
          },
        ),),
      ),
    );
  }


  Offset getPoint(BuildContext context, Offset globalPosition) {
    RenderBox renderBox = context.findRenderObject();
    return renderBox.globalToLocal(globalPosition);
  }

  void updateDx(Offset value, bool isEnd) {
    int index = (value.dx%8 == 0 ? value.dx/8 : value.dx / 8 + 1).round();
    TipsCard.normal(title: items[index].title);

  }

}

class DataItem {
  final int max;
  final int min;
  final String title;

  DataItem(this.title, this.min, this.max);
}

class _Painter extends CustomPainter {

  final List<DataItem> items;

  _Painter(this.items);

  var lineP = Paint()
    ..strokeCap = StrokeCap.butt
    ..strokeWidth = 6
    ..color = Colors.red;

  @override
  void paint(Canvas canvas, Size size) {
    double height = size.height;
    double dx = 0;

    for (DataItem item in items) {
      canvas.drawLine(Offset(dx, item.max / 100 * height), Offset(dx, item.min / 100 * height) , lineP);
      dx = dx + 8;
    }
  }

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

}

只渲染局部数据


import 'dart:math';

import 'package:flutter/material.dart';

const _lineWidth = 6.0;
const _lineSpace = 2.0;

class PaintPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _PaintState();

}



class _PaintState extends State<PaintPage> {

  List<DataItem> items = [];
  

  Offset startPosition;

  int start = 0;
  int end = 0;

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

    for (int i = 0; i < 1000; i++) {
      int random1 = Random.secure().nextInt(100);
      int random2 = Random.secure().nextInt(100);
      items.add(DataItem("item:$i", min(random1, random2), max(random1, random2)));
    }

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: SizedBox(height: 300, child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
        double maxWidth = constraints.maxWidth;
        
        int maxLen = (maxWidth / (_lineSpace + _lineWidth)).round();

        List<DataItem> drawItems = items.sublist(start, min(start + maxLen, items.length));
        return Stack(children: [

          Positioned(child: CustomPaint(
            painter: _Painter(drawItems),
          ), top: 0, bottom: 0, left: 0, right: 0,),
          Positioned(child: GestureDetector(
            onPanUpdate: (details){
              updateDx(getPoint(context, details.globalPosition));
            },
            onPanStart: (DragStartDetails details) {
              updateDx(getPoint(context, details.globalPosition));
            },
            onPanEnd: (DragEndDetails details) {
              startPosition = null;
            },
            onTap: () {

            },
          ), top: 0, bottom: 0, left: 0, right: 0,),

        ],);
      },)),
    );
  }


  Offset getPoint(BuildContext context, Offset globalPosition) {
    RenderBox renderBox = context.findRenderObject();
    return renderBox.globalToLocal(globalPosition);
  }

  void updateDx(Offset point) {
    startPosition ??= point;
    double dx = point.dx - startPosition.dx;
    int index = (dx/8).round();

    setState(() {
      int v = start - index;
      if (v < 0) {
        start = 0;
      } else if (v >= 0 && v <= items.length) {
        start = v;
      } else {
        start = items.length;
      }
    });

    print("start : $start");
  }

}

class DataItem {
  final int max;
  final int min;
  final String title;

  DataItem(this.title, this.min, this.max);
}

class _Painter extends CustomPainter {

  final List<DataItem> items;

  _Painter(this.items);

  var lineP = Paint()
    ..strokeCap = StrokeCap.butt
    ..strokeWidth = _lineWidth
    ..color = Colors.red;

  @override
  void paint(Canvas canvas, Size size) {
    double height = size.height;
    double dx = 0;

    for (DataItem item in items) {
      canvas.drawLine(Offset(dx, item.max / 100 * height), Offset(dx, item.min / 100 * height) , lineP);
      dx = dx + _lineWidth + _lineSpace;
    }
  }

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

}