Flutter-滑动柱状图
本文最后更新于 411 天前

1. 调用处

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:plano_flutter/helpers/router/flutter_binding.dart';
import 'package:plano_flutter/models/event_bus.dart';
import 'package:plano_flutter/modules/home/models/week_slider_chart_click_config.dart';
import 'package:plano_flutter/modules/home/models/week_slider_chart_config.dart';
import 'package:plano_flutter/modules/home/widget/week_slider_chart.dart';
import 'package:screen_adapter/src/size_extension.dart';

class OutDoorSliderWeekChartWidget extends StatefulWidget {
  List<WeekSliderChartConfig> realSource;
  bool hasShowGuide;
  Function? doLoadNext;
  Function? onPageChange;
  OutDoorSliderWeekChartWidget(this.realSource, {this.onPageChange,this.doLoadNext,this.hasShowGuide = false,Key? key}) : super(key: key);

  @override
  State<OutDoorSliderWeekChartWidget> createState() => _OutDoorSliderWeekChartWidgetState();
}

GlobalKey key = GlobalKey();
class _OutDoorSliderWeekChartWidgetState extends State<OutDoorSliderWeekChartWidget> {
  bool hasShowGuide = false;

  late StreamSubscription _streamSubscription;
  @override
  void initState() {
    hasShowGuide = widget.hasShowGuide;
    super.initState();
  }

  int _lastPage = 0;
  // 所有的矩形集合用于判断 点击范围
  List<WeekSliderChartClickConfig> rectList = [];
  // 当前选中的矩形 id
  int _curTapId = -1;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      key: key,
      onTapDown: (TapDownDetails details) {
        Offset globalPosition = details.globalPosition;
        RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
        Offset? localPosition = renderBox?.globalToLocal(globalPosition);
        if(localPosition != null) {
          bool hasFit = false;
          rectList.forEach((config) {
            if(config.rect.contains(localPosition)) {
              hasFit = true;
              _curTapId = config.id;
            }
          });
          setState(() {
            if(!hasFit){
              _curTapId = -1;
            }
          });
        }
      },
      child: Container(
        clipBehavior: Clip.none,
        width: MediaQuery.of(context).size.width,
        height: 220.h,
        child: Center(
          child: PageView.builder(
            reverse: true,
            onPageChanged: (page) {
              // 加载数据 添加一个 仅仅往左滑 在加载
              if(page == widget.realSource.length - 2 && _lastPage < page) {
                widget.doLoadNext?.call();
              }
              _lastPage = page;
              if(hasShowGuide) {
                hasShowGuide = false;
                widget.onPageChange?.call();
              }
            },
            itemCount: widget.realSource.length,
            itemBuilder: (BuildContext context, int index) {
              return CustomPaint(
                painter: WeekColumnarPainter(
                  model: widget.realSource[index],
                  tapConfig: rectList,
                  tapId: _curTapId,
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

2. 自定义组件

import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
import 'package:plano_flutter/modules/home/models/outdoor_activities.dart';
import 'package:plano_flutter/modules/home/models/week_slider_chart_click_config.dart';
import 'package:plano_flutter/modules/home/models/week_slider_chart_config.dart';
import 'package:plano_flutter/res/colors.dart';
import 'package:screen_adapter/src/size_extension.dart';

class WeekColumnarPainter extends CustomPainter {
  late WeekSliderChartConfig model;

  late Paint dashLinePaint;
  late Paint dashTextPaint;
  late Paint dashBottomLinePaint;
  late Paint timeTextPaint;
  late Paint normalRectPaint;
  late Paint upRectPaint;
  late Paint downRectPaint;
  late Paint tapDashPaint;
  late Paint tapTextBgPaint;
  late Paint arrowPaint;

  // 顶部空的距离用于绘制 点击柱状图显示具体数值
  double topSpace = 40.h;
  int tapId = -1;

  // 点击后顶部绘制数值的 垂直虚线的高度 default
  double tapDashH = 17.h;
  // 文本与背景的间距
  double wPadding = 6.w;
  double hPadding = 2.h;


  // 为了能够显示全点击后 顶部显示的具体数值,左侧需要空出  使用的地方不要使用左 padding或margin
  // 具体值根据效果设置
  double leftReserve = 18.w;

  List<WeekSliderChartClickConfig> tapConfig = [];


  var now = DateTime.now();
  var previousDay;
  var formatter = intl.DateFormat('MM/dd');

  bool hasDrawCenterDashLine = false;
  WeekColumnarPainter({
    required this.model,
    required this.tapConfig,
    this.tapId = -1,
  }) {
    previousDay = now.subtract(Duration(days: 1));

    dashLinePaint = Paint()..color = model.dashColor..strokeWidth = 1..style = PaintingStyle.stroke;
    dashTextPaint = Paint()..color = model.dashColor;

    dashBottomLinePaint = Paint()..color = model.dashBottomColor..strokeWidth = 1..style = PaintingStyle.stroke;

    timeTextPaint = Paint()..color = model.textColor;

    normalRectPaint = Paint()..color = model.normalColor;
    upRectPaint = Paint()..color = model.upColor;
    downRectPaint = Paint()..color = model.downColor;

    tapDashPaint = Paint()..color = model.tapDashColor..strokeWidth = 1..style = PaintingStyle.stroke;
    tapTextBgPaint = Paint()..color = model.tapTextBgColor..style = PaintingStyle.fill;

    arrowPaint = Paint()..color = CommonColors.iconButtonColor..style = PaintingStyle.fill;


    hasDrawCenterDashLine = false;
    chartHeight = List.generate(model.source.length, (index) => TapWidgetConfig());
  }

  late List<TapWidgetConfig> chartHeight;
  @override
  void paint(Canvas canvas, Size size) {
    TextPainter dashTextPainter = doGetTextPainter("2小时",model.dashTextSize,model.dashTextColor);
    dashTextPainter.layout();

    // 设置左右padding
    double rightGap = size.width - dashTextPainter.width - model.rightPadding;
    // 为了能够显示全点击后 顶部显示的具体数值,左侧需要空出  使用的地方不要使用左 padding或margin
    double leftGap = model.leftPadding + leftReserve;
    // 最终绘制区域
    double contentW = size.width - dashTextPainter.width - model.leftPadding - model.rightPadding - leftReserve;

    // itemGap = 0.7itemW 计算出的 11
    double itemW = contentW / 10;
    double itemGap = (contentW - 7 * itemW) / 6;

    double startX = leftGap;


    double realH = 0;
    // 绘制虚线下的文本
    for(int i = 0;i < model.source.length;i++) {
      Day weekModel = model.source[i];

      // 2023-10-25
      // 12 换成数组里最大的那个  mock数据 最大是这个
      String timeStr = weekModel.dateStr??"";
      TextPainter textPainter = doGetTextPainter("timeStr",model.textSize,model.textColor);
      textPainter.layout();
      double textHeight = textPainter.height;
      double textWidth = textPainter.width;

      // 使得矩形中心与 文本中心对齐
      double rectCenter = startX + itemW / 2;
      Offset offset = Offset(rectCenter - textWidth / 2, size.height - textHeight - 1.h);
      textPainter.paint(canvas, offset);

      // 修正矩形等 最底部高度
      realH = size.height - textHeight - 6.h;

      // 背景默认矩形
      RRect rRect = RRect.fromLTRBAndCorners(startX, topSpace, startX + itemW, realH, topLeft: Radius.circular(5), topRight: Radius.circular(5));
      canvas.drawRRect(rRect, normalRectPaint);

      // 绘制中间虚线
      drawDashLine(canvas,rightGap,(realH - topSpace) / 2 + topSpace,model.dashGap,model.dashWidth,dashLinePaint);
      if(!hasDrawCenterDashLine) {
        hasDrawCenterDashLine = true;
        // 绘制中心虚线对应的文本
        Offset dashOffset = Offset(rightGap + 5, (realH - dashTextPainter.height - topSpace) / 2 + topSpace);
        dashTextPainter.paint(canvas, dashOffset);
      }

      /*
            y>=2时  x = 100 - 100 / y
            y<2时  x = y * 25
       */
      double hour = weekModel.hours??0;
      // 测试打开
      // if(formatDate(weekModel.dateStr??"") == "06/07"){
      //   hour = 10;
      // }
      double ratio = 1.0;
      if(hour<2) {
        ratio = hour * 25 / 100;
      } else if(hour >= 24){
        ratio = 1.0;
      } else {
        ratio = (100 - 100 / hour) / 100;
      }

      double barTopHeight = topSpace + (realH - topSpace) * (1 - ratio);
      chartHeight[i]..chartCenter = rectCenter..chartHeight = barTopHeight;
      RRect rect = RRect.fromLTRBAndCorners(startX, barTopHeight , startX + itemW, realH, topLeft: Radius.circular(5), topRight: Radius.circular(5));
      // 2小时是分界线
      canvas.drawRRect(rect,hour >= 2 ? upRectPaint : downRectPaint);
      tapConfig.add(WeekSliderChartClickConfig()
        ..rect = rect
        ..id = weekModel.hashCode
      );

      startX += itemW + itemGap;
    }

    // 底部虚线
    drawDashLine(canvas,rightGap,realH,model.dashBottomGap,model.dashWidth,dashBottomLinePaint);

    for(int i = 0;i{weekModel.hours??0}小时",model.tapTextSize,model.tapTextColor,fontWeight: model.tapTextWeight);
        textPainter.layout();
        double textHeight = textPainter.height;
        double textWidth = textPainter.width;

        // 16.w 同上是与text的间距
        double bgWidth = textWidth + wPadding * 2;
        RRect rect = RRect.fromLTRBAndCorners(
            config.chartCenter - bgWidth / 2,
            config.chartHeight - tapDashH - textHeight - hPadding * 2,
            config.chartCenter + bgWidth / 2 ,
            config.chartHeight - tapDashH,
            topLeft: Radius.circular(4.r),
            topRight: Radius.circular(4.r),
            bottomLeft: Radius.circular(4.r),
            bottomRight: Radius.circular(4.r));
        canvas.drawRRect(rect,tapTextBgPaint);

        // 使得矩形中心与 文本中心对齐 2.h 是Text与其圆角背景的间距
        Offset offset = Offset(config.chartCenter - textWidth / 2,config.chartHeight - tapDashH - textHeight - hPadding);
        textPainter.paint(canvas, offset);

        drawVerticalDashLine(canvas, config.chartCenter, config.chartHeight,model.dashGap, 2, tapDashPaint);
      }
    }
    // 绘制点击时 顶部数值

    // 绘制引导  箭头自己绘制
    // if(true) {
    //   Path path = Path()
    //     ..moveTo(leftGap, 3 + topSpace)
    //     /// -
    //     ..lineTo(leftGap + 18.w, 3 + topSpace)
    //     /// |
    //     ..lineTo(leftGap + 18.w, 0 + topSpace)
    //     /// \
    //     ..lineTo(leftGap + 23.w, 5 + topSpace)
    //     /// /
    //     ..lineTo(leftGap + 18.w, 10 + topSpace)
    //     /// |
    //     ..lineTo(leftGap + 18.w, 7 + topSpace)
    //     /// -
    //     ..lineTo(leftGap, 7 + topSpace)
    //     ..close();
    //   canvas.drawPath(path, arrowPaint);
    //
    //   TextPainter tipPainter = doGetTextPainter("滑动查看更多",model.textSize,model.textColor);
    //   tipPainter.layout();
    //   double textHeight = tipPainter.height;
    //   Offset offset = Offset(leftGap + 27.w, topSpace - textHeight / 2);
    //   tipPainter.paint(canvas, offset);
    // }
  }

  String formatDate(String dateStr) {
    if(dateStr.isEmpty)
      return "";
    DateTime dateTime = DateTime.parse(dateStr);
    String month = dateTime.month.toString().padLeft(2, '0');
    String day = dateTime.day.toString().padLeft(2, '0');
    return 'month/day';
  }

  TextPainter doGetTextPainter(String text,double fontSize,Color fontColor,{FontWeight? fontWeight}) {
    TextSpan textSpan = TextSpan(
      text: text,
      style: TextStyle(
        fontSize: fontSize,
        color: fontColor,
        fontWeight: fontWeight,
        height: 1.0
      ),
    );
    TextPainter textPainter = TextPainter(
        text: textSpan,
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr
    );
    return textPainter;
  }

  void drawDashLine(Canvas canvas,double w,double h,double gap,double dashWidth,Paint paint) {
    Path path = Path();
    double startW = leftReserve;
    for(;startW <= w;startW += dashWidth + gap){
      path.moveTo(startW, h);
      path.lineTo(startW + dashWidth, h);
    }
    canvas.drawPath(path, paint);
  }

  // x固定
  void drawVerticalDashLine(Canvas canvas,double x,double h,double gap,double dashHeight,Paint paint) {
    Path path = Path();
    double startH = h;
    for(;startH >= h - tapDashH;startH -= dashHeight + gap){
      path.moveTo(x, startH);
      path.lineTo(x, startH - dashHeight);
    }
    canvas.drawPath(path, paint);
  }


  @override
  bool shouldRepaint(covariant WeekColumnarPainter oldDelegate) {
    // 数据不同或点击或数据变更进行刷新
    return oldDelegate.model.hashCode != model.hashCode
        || oldDelegate.tapId != tapId;
  }
}

// 绘制选中 顶部指示器
class TapWidgetConfig {
  double chartHeight = 0;
  double chartCenter = 0;
}

3. 配置项

import 'package:flutter/material.dart';
import 'package:plano_flutter/modules/home/models/outdoor_activities.dart';


class WeekSliderChartConfig {
  List<Day> source = [];

  bool hasShowGuide = false;

  double leftPadding = 0;
  double rightPadding = 0;
  double factor = 0;
  double maxWidth = 0;

  double dashWidth = 0;

  int dashTime = 0;
  Color dashColor = Color(0xff59C889);
  Color dashTextColor = Color(0xff59C889);
  double dashTextSize = 0;
  double dashGap = 0;

  Color normalColor = Color(0xffF6F8FA);
  Color upColor = Color(0xff59C889);
  Color downColor = Color(0xffFFAA4D);

  Color dashBottomColor = Color(0xffcccccc);
  double dashBottomGap = 0;

  Color textColor = Color(0xff999999);
  double textSize = 0;

  // 顶部点击显示的数值
  Color tapDashColor = Color(0xff999999);
  Color tapTextColor = Color(0xff333333);
  double tapTextSize = 14;
  FontWeight tapTextWeight = FontWeight.bold;
  Color tapTextBgColor = Color(0xffeeeeee);


  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is WeekSliderChartConfig &&
          runtimeType == other.runtimeType &&
          source == other.source;

  @override
  int get hashCode => source.hashCode;
}


class Day {
  Day();

  String? dateStr;
  double? hours;

  factory Day.fromJson(Map<String, dynamic> json) =>
  _DayFromJson(json);

  toJson() => _DayToJson(this);

  @override
  bool operator ==(Object other) =>
  identical(this, other) ||
  other is Day &&
  runtimeType == other.runtimeType &&
  dateStr == other.dateStr &&
  hours == other.hours;

  @override
  int get hashCode => dateStr.hashCode ^ hours.hashCode;
}

iichen:https://iichen.cn/?p=708
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇