import 'dart:convert'; import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:sport/bean/jog/detail.dart'; import 'package:sport/pages/run/location.dart'; import 'package:sport/pages/run/map.dart'; import 'package:sport/pages/run/map_replay_page.dart'; import 'package:sport/pages/run/run_data.dart'; import 'package:sport/pages/run/run_page.dart'; import 'package:sport/pages/run/run_share.dart'; import 'package:sport/pages/run/setting_page.dart'; import 'package:sport/pages/run/statistics.dart'; import 'package:sport/provider/user_model.dart'; import 'package:sport/router/navigator_util.dart'; import 'package:sport/services/api/inject_api.dart'; import 'package:sport/services/app_lifecycle_state.dart'; import 'package:sport/utils/DateFormat.dart'; import 'package:sport/utils/sport_utils.dart'; import 'package:sport/widgets/decoration.dart'; import 'package:sport/widgets/image.dart'; import 'package:sport/widgets/linear_progress_indicator.dart' as progress; import 'package:sport/widgets/loading.dart'; import 'package:sport/widgets/misc.dart'; import 'package:sport/widgets/popmenu_bg.dart'; import 'chart.dart'; class RunDetailPage extends StatefulWidget { final int id; final bool post; final bool share; final JogDetail? jogDetail; const RunDetailPage({Key? key, this.id = 0, this.post = false, this.share = false, this.jogDetail}) : super(key: key); @override State createState() => _PageState(); } class _PageState extends LifecycleState with RunSetting, InjectApi, TickerProviderStateMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey mapKey = GlobalKey(); bool _loading = true; JogDetail? _jogDetail; double _opacity = 0; double _expandedHeight = 0; int _brightness = 0; ValueNotifier _notifierMapNotification = ValueNotifier(MapNotification(0, 0, 0)); @override bool autoLoadSetting() { return false; } @override void initState() { super.initState(); refreshSetting(); _showData(); } _showData() async { List points = []; _jogDetail = widget.jogDetail; if (_jogDetail == null) { var result = await api.jogShowRecord(widget.id); if (result.data != null) { _jogDetail = result.data!; } } if (_jogDetail != null) { JogDetail data = _jogDetail!; totalDistance = data.distance?.toDouble() ?? 0; totalTime = data.duration ?? 0; totalConsume = data.consume ?? 0; totalStep = data.step ?? 0; met = data.met ?? 0; timeStart = DateTime.parse(data.begin ?? ""); timeEnd = DateTime.parse(data.end ?? ""); points = data.points ?? []; if (data.stepInfo != null) stepRateList = json.decode(data.stepInfo ?? "[]").cast(); if (data.altitudeInfo != null) altitudeList = json.decode(data.altitudeInfo ?? "[]").cast(); var runData = RunData(0); if (points.length > 1) { runData.calPoints(points); } _pointsKm.addAll(runData.pointsKm); _pointsKmTime.addAll(runData.pointsKmTime); totalAltitude = runData.totalAltitude; marathonHalf = runData.marathonHalf; marathonAll = runData.marathonAll; } if (stepRateList.isNotEmpty == true) stepMax = stepRateList.reduce((value, element) => value > element ? value : element); stepAvg = totalStep ~/ (totalTime / 60); if (_pointsKmTime.isNotEmpty == true) { kmTime = _pointsKmTime.values.fold(0, (value, element) => value + element.inSeconds); avg = kmTime ~/ _pointsKmTime.length; min = _pointsKmTime.values.reduce((value, element) => value.inSeconds < element.inSeconds ? value : element).inSeconds; max = _pointsKmTime.values.reduce((value, element) => value.inSeconds > element.inSeconds ? value : element).inSeconds; // print("1111111111111111 $_pointsKmTime 22222222222222222 avg $avg $min"); } setState(() { _loading = false; this.points = points; }); } final Map _pointsKm = {}; final Map _pointsKmTime = {}; List points = []; List stepRateList = []; List altitudeList = []; double totalDistance = 0; double totalAltitude = 0; int totalTime = 0; int totalStep = 0; int stepAvg = 0; int stepMax = 0; int totalConsume = 0; double met = 0.0; int kmTime = 0; int avg = 0; double avgSpeed = 0.0; int min = 0; int max = 1; DateTime? timeStart; DateTime? timeEnd; int marathonHalf = 0; int marathonAll = 0; @override void dispose() { super.dispose(); } _share() async { var path = await mapKey.currentState?.takeSnapshot(); if (path == null) return; if (_jogDetail == null) return; NavigatorUtil.goPage( context, (context) => RunShare( detail: _jogDetail!, map: path, )); } @override Widget build(BuildContext context) { var maxKm = _pointsKmTime.isNotEmpty ? _pointsKmTime.keys.reduce((value, element) => value > element ? value : element) : 0; var map = points.isNotEmpty == true ? NotificationListener( onNotification: (map) { _notifierMapNotification.value = map; return false; }, child: MapWidget( key: mapKey, distance: totalDistance, runMapType: runMapType, runMapKm: runMapKm, points: points, pointsKm: _pointsKm, ), ) : Container(); const _padding = 16.0; var header = Consumer( builder: (_, model, __) => Container( margin: const EdgeInsets.fromLTRB(12.0, 30, 12.0, 0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: _padding), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "趣动户外跑", style: Theme.of(context).textTheme.bodyText1!, ), const SizedBox( height: 45.0, ), Row( textBaseline: TextBaseline.alphabetic, mainAxisSize: MainAxisSize.min, children: [ ValueListenableBuilder( valueListenable: _notifierMapNotification, builder: (_, data, __) { return Text( "${formatNum(data.distance / 1000, 2)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 45.0, fontFamily: "DIN"), strutStyle: fixedLine, ); }, ), Text(" 公里", style: Theme.of(context).textTheme.subtitle1!), marathonAll > 0 ? Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(3), color: COLOR_RUN_FAST), margin: const EdgeInsets.only(left: 10), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Center( child: Text( "全马", style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white), )), ) : marathonHalf > 0 ? Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(3), color: COLOR_RUN_SLOW), margin: const EdgeInsets.only(left: 10), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Center( child: Text( "半马", style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white), )), ) : Container() ], ), ], ), Container( child: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( decoration: BoxDecoration(color: Colors.grey, shape: BoxShape.circle), child: CircleAvatar( backgroundColor: Colors.black26, backgroundImage: userAvatarProvider(model.user.avatar), radius: 30.0, ), transform: Matrix4.translationValues(0, -24, 0), ), Text(model.user.name, style: Theme.of(context).textTheme.headline1!), const SizedBox( height: 12.0, ), Text( "${SportUtils.toDateTime(timeStart)} ${SportUtils.toEndTime(timeStart)} ~ ${SportUtils.toEndTime(timeEnd)}", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ), ], ), ), ], ), ), ); double infoHeight = 230 * MediaQuery.of(context).textScaleFactor; final double _iconSize = 40.0; List _kmList = []; if (_pointsKmTime.isNotEmpty) { List keys = _pointsKmTime.keys.toList(); int second = 0; int totalSecond = 0; for (var i = 0; i < keys.length; i++) { int e = keys[i]; second += _pointsKmTime[e]?.inSeconds ?? 0; // print("11111111111111 --- $e $second"); totalSecond += _pointsKmTime[e]?.inSeconds ?? 0; _kmList.add(Padding( padding: const EdgeInsets.symmetric(vertical: 1.0), child: Row( mainAxisSize: MainAxisSize.max, children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 16), child: Padding( padding: const EdgeInsets.only(top: 1.0), child: Text( "${e.toString().padLeft(2, "0")}", style: Theme.of(context).textTheme.subtitle1!.copyWith(fontFamily: "DIN"), ), ), ), Expanded( child: Container( height: 12.0, color: Color(0xfff1f1f1), margin: const EdgeInsets.symmetric(horizontal: 10.0), child: CustomPaint( painter: progress.LinearProgressIndicator((_pointsKmTime[e]?.inSeconds ?? 0) == min ? [Color(0xffFFC57A), Color(0xffFF5B1D)] : [Color(0xffFFF4CE), Color(0xffFFC400)], (_pointsKmTime[e]?.inSeconds ?? 0) / max * 0.9), ), ), ), ConstrainedBox( constraints: BoxConstraints(minWidth: 50), child: Text( "${SportUtils.pace(SportUtils.calPace((_pointsKmTime[e]?.inSeconds ?? 0), 1))}", style: Theme.of(context).textTheme.subtitle1!, ), ), ], ), )); if ((i + 1) % 5 == 0) { String tips = "近5公里用时 ${DateFormat.toTime(second)}"; if (i > 5) { tips += " ${i + 1}公里用时 ${DateFormat.toTime(totalSecond)}"; } _kmList.add(Padding( padding: const EdgeInsets.fromLTRB(26, 5, 0, 15), child: Container( height: 18.0, child: Text( tips, maxLines: 1, style: Theme.of(context).textTheme.bodyText1?.copyWith(fontSize: 11), )), )); second = 0; } } } double mapHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top - 24.0 - infoHeight; return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: Scaffold( key: _scaffoldKey, body: LoadingWidget( loading: _loading, willPop: false, child: CustomScrollView( shrinkWrap: widget.share, physics: ClampingScrollPhysics(), slivers: [ SliverAppBar( expandedHeight: widget.share? 300:mapHeight, pinned: true, forceElevated: true, automaticallyImplyLeading: false, title: widget.share ? null : Row( children: [ GestureDetector( onTap: () { Navigator.maybePop(context); }, child: Container(width: _iconSize, height: _iconSize, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: arrowBack()), ), Expanded( child: Container( height: 1, )), PopupMenuTheme( data: PopupMenuThemeData(shape: PopmenuShape(borderRadius: BorderRadius.all(Radius.circular(10.0)))), child: PopupMenuButton( offset: Offset(-10, kToolbarHeight / 2 + 25), onSelected: (val) { NavigatorUtil.goPage( context, (context) => MapReplayPage( distance: totalDistance, duration: totalTime, kcal: totalConsume, runMapType: runMapType, runMapKm: runMapKm, points: points, pointsKm: _pointsKm, begin: timeStart?.millisecondsSinceEpoch ?? 0, showType: val == "1" ? 1 : 0, )); }, itemBuilder: (context) { return divideMenus([ PopupMenuItem( value: "1", child: Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Text( "生成视频(20秒)", style: Theme.of(context).textTheme.subtitle1!, ), )), ), menuDivider(), PopupMenuItem( value: "0", child: Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Text( "生成视频(${totalDistance ~/ 1000 * 4 + 4}秒)", style: Theme.of(context).textTheme.subtitle1!, ), )), ), ]); }, child: Container(width: _iconSize, height: _iconSize, margin: EdgeInsets.only(right: 12.0), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: Image.asset("lib/assets/img/topbar_icon_trajectory.png"))), ), GestureDetector( onTap: () { _share(); }, child: Container(width: _iconSize, height: _iconSize, margin: EdgeInsets.only(right: 12.0), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: Image.asset("lib/assets/img/bbs_icon_share.png")), ), GestureDetector( onTap: () async { await NavigatorUtil.goPage(context, (context) => SettingPage()); refreshSetting(); }, child: Container(width: _iconSize, height: _iconSize, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: Image.asset("lib/assets/img/setgoals_icon_set.png")), ), ], ), flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.pin, background: Container( color: Theme.of(context).scaffoldBackgroundColor, child: Stack( children: [Positioned(top: 0, left: 0, right: 0, bottom: 50.0, child: map), Align(alignment: Alignment.bottomCenter, child: header)], ), )), ), SliverToBoxAdapter( child: Container( height: infoHeight, margin: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 12.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only(bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10)), ), child: Column( children: [ Container( margin: const EdgeInsets.symmetric(vertical: 20.0), height: 20, child: Stack( fit: StackFit.expand, children: [ Center( child: Container( height: 3, width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ COLOR_RUN_FAST, COLOR_RUN_MIDDLE, COLOR_RUN_SLOW, ], ), ), ), ), if (min != 0) Positioned( top: 0, bottom: 0, left: _padding, child: Container( color: Colors.white, height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Center( child: Text( // "${SportUtils.toEndTime(timeStart)}开始", "快 ${SportUtils.pace(SportUtils.calPace((min), 1))}", style: Theme.of(context).textTheme.bodyText1!, ), ), ), ), if (max != 0) Positioned( top: 0, bottom: 0, right: _padding, child: Container( color: Colors.white, height: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Center( child: Text( // "${SportUtils.toEndTime(timeEnd)}结束", "慢 ${SportUtils.pace(SportUtils.calPace((max), 1))}", style: Theme.of(context).textTheme.bodyText1!, ), ), ), ), ], ), ), Padding( padding: const EdgeInsets.only(left: 20.0), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${DateFormat.toTime(totalTime)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "时长", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ), Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${met.toStringAsFixed(1)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "MET值", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ), Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( "${SportUtils.pace11(SportUtils.calPace(totalTime, totalDistance / 1000))}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "′", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0), ), Text( "${SportUtils.pace12(SportUtils.calPace(totalTime, totalDistance / 1000))}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "″", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0), ), ], ), Text( "配速(每公里用时)", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ), ], ), ), const SizedBox( height: 20.0, ), Padding( padding: const EdgeInsets.only(left: 20.0), child: Row( children: [ Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "$totalConsume", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "消耗(大卡)", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ), Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( totalStep == 0 ? "0" : "${totalStep ~/ (totalTime / 60)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "步频(步/分钟)", style: Theme.of(context).textTheme.bodyText1!, ), ], ), ), Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${totalTime == 0 ? 0 : (totalDistance / 1000.0 / totalTime * 3600).toStringAsFixed(1)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "时速(公里/小时)", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ), ], ), ), const SizedBox( height: 20.0, ), Center( child: Container( decoration: BoxDecoration(color: Theme.of(context).backgroundColor, borderRadius: BorderRadius.circular(radius)), margin: EdgeInsets.symmetric(vertical: 10.0), padding: EdgeInsets.fromLTRB(30.0, 2, 30.0, 0), child: Row( mainAxisSize: MainAxisSize.min, children: [ Image.asset( "lib/assets/img/run_icon_today.png", color: Color(0xffC5C5C5), ), SizedBox( width: 12.0, ), Row( children: [ Text( "步幅 ", style: Theme.of(context).textTheme.subtitle2!, ), Text( totalStep == 0 ? "0" : "${(totalDistance / totalStep * 100).toInt()}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( " 厘米", style: Theme.of(context).textTheme.bodyText1!, ), ], ), Container( width: 1, height: 20.0, color: Color(0xffdcdcdc), margin: EdgeInsets.symmetric(horizontal: 20.0), ), Row( children: [ Text( "步数 ", style: Theme.of(context).textTheme.subtitle2!, ), Text( "${totalStep}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( " 步", style: Theme.of(context).textTheme.bodyText1!, ), ], ), ], ), ), ), ], ), ), ), if (_pointsKmTime.isNotEmpty == true) SliverToBoxAdapter( child: BoxWidget( title: "配速", icon: "run_icon_tile1.png", child: Column( mainAxisSize: MainAxisSize.min, children: [ if (marathonHalf > 0) Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(bottom: 16.0, top: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${DateFormat.toTime(marathonHalf)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"), ), Text( "半马总用时", style: Theme.of(context).textTheme.bodyText1!, ) ], mainAxisSize: MainAxisSize.min, ), ), ), if (marathonAll > 0) Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(bottom: 16.0, top: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${DateFormat.toTime(marathonHalf)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"), ), Text( "全马总用时", style: Theme.of(context).textTheme.bodyText1!, ) ], mainAxisSize: MainAxisSize.min, ), ), ), CustomPaint( foregroundPainter: AvgPainter(context, avg * 1.0 / max * 0.9, avg, _pointsKmTime.length, MediaQuery.of(context).textScaleFactor), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "公里", style: Theme.of(context).textTheme.bodyText1!, ), Expanded( child: Container(height: 18,), ), Text( "配速", style: Theme.of(context).textTheme.bodyText1!, ), const SizedBox( width: 20.0, ), ], ), const SizedBox( height: 12.0, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: _kmList, ), // if (_pointsKmTime.length > 0 && totalTime > 0) // Row( // mainAxisAlignment: MainAxisAlignment.spaceAround, // children: [ // RichText( // text: TextSpan(children: [TextSpan(text: "$maxKm公里累计用时 ", style: Theme.of(context).textTheme.bodyText1!), TextSpan(text: "${DateFormat.toTime(kmTime)}", style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Theme.of(context).accentColor))]), // ), // if (totalTime - kmTime > 0) // RichText( // text: TextSpan(children: [TextSpan(text: "最后不足1公里用时 ", style: Theme.of(context).textTheme.bodyText1!), TextSpan(text: "${DateFormat.toTime((totalTime) - kmTime)}", style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Theme.of(context).accentColor))]), // ), // ], // ), if (_pointsKmTime.length > 0 && totalTime > 0 && (totalTime - kmTime > 20)) Padding( padding: const EdgeInsets.fromLTRB(26, 5, 0, 15), child: Container( height: 18.0, child: Text( "最后不足1公里用时 ${DateFormat.toTime((totalTime) - kmTime)}", style: Theme.of(context).textTheme.bodyText1?.copyWith(fontSize: 11), )), ), const SizedBox( height: 16.0, ), ], ), ), ], )), ), SliverToBoxAdapter( child: BoxWidget( title: "步频", icon: "run_icon_tile2.png", child: Column( children: [ Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "$stepAvg", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"), ), Text( "平均步频(步/分钟)", style: Theme.of(context).textTheme.bodyText1!, ) ], ), const SizedBox( width: 37.0, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "$stepMax", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"), ), Text( "最高步频(步/分钟)", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ], ), RunChart( values: stepRateList.map((e) => e.toDouble()).toList(), gradient: false, ), ], )), ), SliverToBoxAdapter( child: BoxWidget( title: "运动海拔", icon: "run_icon_tile3.png", child: Column( children: [ Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${(totalAltitude).toStringAsFixed(1)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"), ), Text( "累计爬升(米)", style: Theme.of(context).textTheme.bodyText1!, ) ], ), ], ), RunChart( values: altitudeList.map((e) => e.toDouble()).toList(), // values: [-10,20,0,0,1,2,3,4,5,6,8,9,8,10,-6,-5,-20, 20, 30], gradient: true, ), ], )), ), SliverToBoxAdapter( child: Container( height: 50, ), ), ], ), ), ), ); } } class AvgPainter extends CustomPainter { final BuildContext context; final int pace; final double progress; final int length; final double dpi; AvgPainter(this.context, this.progress, this.pace, this.length, this.dpi); final Paint _paint = Paint() ..color = Color(0xff999999) ..strokeWidth = 0.5 ..isAntiAlias = true; final ui.ParagraphStyle _valueStyle = ui.ParagraphStyle( textAlign: TextAlign.left, fontSize: 10, ); @override void paint(Canvas canvas, Size size) { if (length == 0) return; double dpr = ui.window.devicePixelRatio; double p = math.min(.9, progress); double startX = 26 + (size.width - 86) * p; double startY = 34; double endY = size.height - 8 * dpr; var dashWidth = 3; var dashSpace = 3; final space = (dashSpace + dashWidth); double height = (endY - startY); int split = length ~/ 5; double lineHeight = (height - split * 38 / 2.0 * dpr) / length; double lineSplit = lineHeight * 5; // print("111111111111111 height: $height, sss: $split lineHeight: $lineHeight"); // canvas.drawRect(Rect.fromLTWH(26, startY + lineHeight * 5, 100, 38), _paint); // canvas.clipRect(Rect.fromLTWH(26, startY + lineHeight * 5, 1000, 38)); double ss = 0; while (startY < endY) { if (ss > lineSplit - 5) { startY += 42; ss = 0; } canvas.drawLine(Offset(startX, startY), Offset(startX, math.min(startY + dashWidth, endY)), _paint); startY += space; ss += space; } var text = TextPainter( textAlign: TextAlign.center, text: TextSpan(children: [ TextSpan(text: "平均配速", style: Theme.of(context).textTheme.bodyText1!), TextSpan(text: "\n${SportUtils.pace(SportUtils.calPace((this.pace), 1))}", style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 10)), ]), textDirection: TextDirection.ltr) ..layout(maxWidth: size.width); text.paint(canvas, Offset(startX - text.minIntrinsicWidth / 2, 0)); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } }