| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- import 'dart:async';
- import 'package:flutter/material.dart';
- import 'package:k_chart_plus/chart_translations.dart';
- import 'package:k_chart_plus/components/popup_info_view.dart';
- import 'package:k_chart_plus/k_chart_plus.dart';
- import 'renderer/base_dimension.dart';
- enum MainState { MA, BOLL, SAR }
- enum SecondaryState { MACD, KDJ, RSI, WR, CCI }
- class TimeFormat {
- static const List<String> YEAR_MONTH_DAY = [yyyy, '-', mm, '-', dd];
- static const List<String> YEAR_MONTH_DAY_WITH_HOUR = [
- yyyy,
- '-',
- mm,
- '-',
- dd,
- ' ',
- HH,
- ':',
- nn
- ];
- }
- class KChartWidget extends StatefulWidget {
- final List<KLineEntity>? datas;
- final Set<MainState> mainStateLi;
- final bool volHidden;
- final Set<SecondaryState> secondaryStateLi;
- // final Function()? onSecondaryTap;
- final bool isLine;
- final bool
- isTapShowInfoDialog; //Whether to enable click to display detailed data
- final bool hideGrid;
- final bool showNowPrice;
- final bool showInfoDialog;
- final bool materialInfoDialog; // Material Style Information Popup
- final ChartTranslations chartTranslations;
- final List<String> timeFormat;
- final double mBaseHeight;
- // It will be called when the screen scrolls to the end.
- // If true, it will be scrolled to the end of the right side of the screen.
- // If it is false, it will be scrolled to the end of the left side of the screen.
- final Function(bool)? onLoadMore;
- final int fixedLength;
- final List<int> maDayList;
- final int flingTime;
- final double flingRatio;
- final Curve flingCurve;
- final Function(bool)? isOnDrag;
- final ChartColors chartColors;
- final ChartStyle chartStyle;
- final VerticalTextAlignment verticalTextAlignment;
- final bool isTrendLine;
- final double xFrontPadding;
- /// 外部调用 .value = !.value 即可触发隐藏详情
- final ValueNotifier<bool>? dismissInfoNotifier;
- /// 当前价格字符串(来自 WS ticker),覆盖图表默认的 toStringAsFixed 显示
- final String? nowPriceStr;
- KChartWidget(
- this.datas,
- this.chartStyle,
- this.chartColors, {
- required this.isTrendLine,
- this.xFrontPadding = 100,
- this.dismissInfoNotifier,
- this.nowPriceStr,
- this.mainStateLi = const <MainState>{},
- this.secondaryStateLi = const <SecondaryState>{},
- // this.onSecondaryTap,
- this.volHidden = false,
- this.isLine = false,
- this.isTapShowInfoDialog = false,
- this.hideGrid = false,
- this.showNowPrice = true,
- this.showInfoDialog = true,
- this.materialInfoDialog = true,
- this.chartTranslations = const ChartTranslations(),
- this.timeFormat = TimeFormat.YEAR_MONTH_DAY,
- this.onLoadMore,
- this.fixedLength = 2,
- this.maDayList = const [5, 10, 20],
- this.flingTime = 600,
- this.flingRatio = 0.5,
- this.flingCurve = Curves.decelerate,
- this.isOnDrag,
- this.verticalTextAlignment = VerticalTextAlignment.left,
- this.mBaseHeight = 360,
- });
- @override
- _KChartWidgetState createState() => _KChartWidgetState();
- }
- class _KChartWidgetState extends State<KChartWidget>
- with TickerProviderStateMixin {
- final StreamController<InfoWindowEntity?> mInfoWindowStream =
- StreamController<InfoWindowEntity?>();
- double mScaleX = 1.0, mScrollX = 0.0, mSelectX = 0.0;
- double mHeight = 0, mWidth = 0;
- AnimationController? _controller;
- Animation<double>? aniX;
- //For TrendLine
- List<TrendLine> lines = [];
- double? changeinXposition;
- double? changeinYposition;
- double mSelectY = 0.0;
- bool waitingForOtherPairofCords = false;
- bool enableCordRecord = false;
- double getMinScrollX() {
- return mScaleX;
- }
- double _lastScale = 1.0;
- bool isScale = false, isDrag = false, isLongPress = false, isOnTap = false;
- @override
- void initState() {
- super.initState();
- widget.dismissInfoNotifier?.addListener(_onDismiss);
- }
- void _onDismiss() {
- if (isOnTap || isLongPress) {
- isOnTap = false;
- isLongPress = false;
- mInfoWindowStream.sink.add(null);
- notifyChanged();
- }
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- }
- @override
- void dispose() {
- widget.dismissInfoNotifier?.removeListener(_onDismiss);
- mInfoWindowStream.sink.close();
- mInfoWindowStream.close();
- _controller?.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- if (widget.datas != null && widget.datas!.isEmpty) {
- mScrollX = mSelectX = 0.0;
- mScaleX = 1.0;
- }
- final BaseDimension baseDimension = BaseDimension(
- mBaseHeight: widget.mBaseHeight,
- volHidden: widget.volHidden,
- secondaryStateLi: widget.secondaryStateLi,
- mainStateLi: widget.mainStateLi,
- );
- final _painter = ChartPainter(
- widget.chartStyle,
- widget.chartColors,
- baseDimension: baseDimension,
- lines: lines, //For TrendLine
- sink: mInfoWindowStream.sink,
- xFrontPadding: widget.xFrontPadding,
- isTrendLine: widget.isTrendLine, //For TrendLine
- selectY: mSelectY, //For TrendLine
- datas: widget.datas,
- scaleX: mScaleX,
- scrollX: mScrollX,
- selectX: mSelectX,
- isLongPass: isLongPress,
- isOnTap: isOnTap,
- isTapShowInfoDialog: widget.isTapShowInfoDialog,
- mainStateLi: widget.mainStateLi,
- volHidden: widget.volHidden,
- secondaryStateLi: widget.secondaryStateLi,
- isLine: widget.isLine,
- hideGrid: widget.hideGrid,
- showNowPrice: widget.showNowPrice,
- fixedLength: widget.fixedLength,
- maDayList: widget.maDayList,
- verticalTextAlignment: widget.verticalTextAlignment,
- nowPriceStr: widget.nowPriceStr,
- );
- return LayoutBuilder(
- builder: (context, constraints) {
- mHeight = constraints.maxHeight;
- mWidth = constraints.maxWidth;
- return GestureDetector(
- onTapUp: (details) {
- // if (!widget.isTrendLine && widget.onSecondaryTap != null && _painter.isInSecondaryRect(details.localPosition)) {
- // widget.onSecondaryTap!();
- // }
- if (!widget.isTrendLine &&
- _painter.isInMainRect(details.localPosition)) {
- // 在主图区域内任意点击:始终切换到点击位置对应的蜡烛详情,
- // 不再二次点击隐藏(用户期望点哪里就显示哪里的详情)
- isOnTap = true;
- if (widget.isTapShowInfoDialog) {
- mSelectX = details.localPosition.dx;
- notifyChanged();
- }
- } else if (isOnTap) {
- // 点击主图以外区域(副图/成交量等)→ 隐藏
- isOnTap = false;
- mInfoWindowStream.sink.add(null);
- notifyChanged();
- }
- if (widget.isTrendLine && !isLongPress && enableCordRecord) {
- enableCordRecord = false;
- Offset p1 = Offset(getTrendLineX(), mSelectY);
- if (!waitingForOtherPairofCords) {
- lines.add(TrendLine(
- p1, Offset(-1, -1), trendLineMax!, trendLineScale!));
- }
- if (waitingForOtherPairofCords) {
- var a = lines.last;
- lines.removeLast();
- lines.add(TrendLine(a.p1, p1, trendLineMax!, trendLineScale!));
- waitingForOtherPairofCords = false;
- } else {
- waitingForOtherPairofCords = true;
- }
- notifyChanged();
- }
- },
- // 统一使用 onScale 处理单指拖拽和双指缩放。
- // 在 onScaleUpdate 中动态检测 pointerCount 切换模式,
- // 不依赖 onScaleStart 的初始判断(第二个手指可能晚于 start 按下)。
- onScaleStart: (details) {
- isOnTap = false;
- _stopAnimation();
- _onDragChanged(true);
- },
- onScaleUpdate: (details) {
- if (isLongPress) return;
- if (details.scale != 1.0) {
- // 双指缩放(scale != 1.0 表示有缩放变化,不管角度)
- isScale = true;
- mScaleX = (_lastScale * details.scale).clamp(0.5, 2.2);
- notifyChanged();
- } else if (!isScale) {
- // 单指水平拖拽
- mScrollX = (details.focalPointDelta.dx / mScaleX + mScrollX)
- .clamp(0.0, ChartPainter.maxScrollX)
- .toDouble();
- // 滑到左端 90% 时提前触发加载更早数据
- if (ChartPainter.maxScrollX > 0 &&
- mScrollX >= ChartPainter.maxScrollX * 0.9 &&
- widget.onLoadMore != null) {
- widget.onLoadMore!(false);
- }
- notifyChanged();
- }
- },
- onScaleEnd: (details) {
- if (isScale) {
- isScale = false;
- _lastScale = mScaleX;
- }
- _onDragChanged(false);
- if (!isScale) {
- var velocity = details.velocity.pixelsPerSecond.dx;
- _onFling(velocity);
- }
- },
- onLongPressStart: (details) {
- isOnTap = false;
- isLongPress = true;
- if ((mSelectX != details.localPosition.dx ||
- mSelectY != details.globalPosition.dy) &&
- !widget.isTrendLine) {
- mSelectX = details.localPosition.dx;
- notifyChanged();
- }
- //For TrendLine
- if (widget.isTrendLine && changeinXposition == null) {
- mSelectX = changeinXposition = details.localPosition.dx;
- mSelectY = changeinYposition = details.globalPosition.dy;
- notifyChanged();
- }
- //For TrendLine
- if (widget.isTrendLine && changeinXposition != null) {
- changeinXposition = details.localPosition.dx;
- changeinYposition = details.globalPosition.dy;
- notifyChanged();
- }
- },
- onLongPressMoveUpdate: (details) {
- if ((mSelectX != details.localPosition.dx ||
- mSelectY != details.globalPosition.dy) &&
- !widget.isTrendLine) {
- mSelectX = details.localPosition.dx;
- mSelectY = details.localPosition.dy;
- notifyChanged();
- }
- if (widget.isTrendLine) {
- mSelectX =
- mSelectX + (details.localPosition.dx - changeinXposition!);
- changeinXposition = details.localPosition.dx;
- mSelectY =
- mSelectY + (details.globalPosition.dy - changeinYposition!);
- changeinYposition = details.globalPosition.dy;
- notifyChanged();
- }
- },
- onLongPressEnd: (details) {
- isLongPress = false;
- enableCordRecord = true;
- mInfoWindowStream.sink.add(null);
- notifyChanged();
- },
- child: Stack(
- children: <Widget>[
- CustomPaint(
- size: Size(double.infinity, baseDimension.mDisplayHeight),
- painter: _painter,
- ),
- if (widget.showInfoDialog) _buildInfoDialog()
- ],
- ),
- );
- },
- );
- }
- void _stopAnimation({bool needNotify = true}) {
- if (_controller != null && _controller!.isAnimating) {
- _controller!.stop();
- _onDragChanged(false);
- if (needNotify) {
- notifyChanged();
- }
- }
- }
- void _onDragChanged(bool isOnDrag) {
- isDrag = isOnDrag;
- if (widget.isOnDrag != null) {
- widget.isOnDrag!(isDrag);
- }
- }
- void _onFling(double x) {
- _controller = AnimationController(
- duration: Duration(milliseconds: widget.flingTime), vsync: this);
- aniX = null;
- aniX = Tween<double>(begin: mScrollX, end: x * widget.flingRatio + mScrollX)
- .animate(CurvedAnimation(
- parent: _controller!.view, curve: widget.flingCurve));
- aniX!.addListener(() {
- mScrollX = aniX!.value;
- if (mScrollX <= 0) {
- mScrollX = 0;
- if (widget.onLoadMore != null) {
- widget.onLoadMore!(true);
- }
- _stopAnimation();
- } else if (mScrollX >= ChartPainter.maxScrollX) {
- mScrollX = ChartPainter.maxScrollX;
- if (widget.onLoadMore != null) {
- widget.onLoadMore!(false);
- }
- _stopAnimation();
- }
- notifyChanged();
- });
- aniX!.addStatusListener((status) {
- if (status == AnimationStatus.completed ||
- status == AnimationStatus.dismissed) {
- _onDragChanged(false);
- notifyChanged();
- }
- });
- _controller!.forward();
- }
- void notifyChanged() => setState(() {});
- late List<String> infos;
- Widget _buildInfoDialog() {
- return StreamBuilder<InfoWindowEntity?>(
- stream: mInfoWindowStream.stream,
- builder: (context, snapshot) {
- if ((!isLongPress && !isOnTap) ||
- widget.isLine == true ||
- !snapshot.hasData ||
- snapshot.data?.kLineEntity == null) return SizedBox();
- KLineEntity entity = snapshot.data!.kLineEntity;
- final dialogWidth = mWidth / 3;
- if (snapshot.data!.isLeft) {
- return Positioned(
- top: 25,
- left: 10.0,
- child: PopupInfoView(
- entity: entity,
- width: dialogWidth,
- chartColors: widget.chartColors,
- chartTranslations: widget.chartTranslations,
- materialInfoDialog: widget.materialInfoDialog,
- timeFormat: widget.timeFormat,
- fixedLength: widget.fixedLength,
- ),
- );
- }
- return Positioned(
- top: 25,
- right: 10.0,
- child: PopupInfoView(
- entity: entity,
- width: dialogWidth,
- chartColors: widget.chartColors,
- chartTranslations: widget.chartTranslations,
- materialInfoDialog: widget.materialInfoDialog,
- timeFormat: widget.timeFormat,
- fixedLength: widget.fixedLength,
- ),
- );
- },
- );
- }
- }
|