本文最后更新于 533 天前
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;
}