import 'dart:math'; import 'dart:ui'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_swiper/flutter_swiper.dart'; import 'package:sport/bean/sport_detail.dart'; import 'package:sport/bean/sport_index.dart'; import 'package:sport/services/api/inject_api.dart'; import 'package:sport/services/api/resp.dart'; import 'package:sport/widgets/appbar.dart'; import 'package:sport/widgets/decoration.dart'; import 'package:sport/widgets/misc.dart'; import 'package:sport/widgets/progress_bar.dart'; class StrengthPage extends StatefulWidget { @override State createState() => _PageState(); } class _PageState extends State with InjectApi { ValueNotifier _valueNotifierIndex = ValueNotifier(26); SwiperController _swiperController; ScrollController _scrollController; @override void initState() { _swiperController = SwiperController(); _scrollController = ScrollController(); super.initState(); } @override void dispose() { _swiperController?.dispose(); _valueNotifierIndex?.dispose(); _scrollController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: [ buildSliverAppBar(context, "运动强度", backgroundColor: Theme.of(context).scaffoldBackgroundColor), SliverToBoxAdapter( child: Container( margin: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0), decoration: circular(), child: FutureBuilder( future: createFutureType(0), builder: (BuildContext context, AsyncSnapshot> snapshot) => Column( children: [ Center( child: CustomPaint( painter: _Bg(), child: Container( width: 200, height: 200, child: Center( child: CircularProgressBar( percent: strengthToValue(snapshot?.data?.data?.sum?.consume ?? 0) / 12.0, radius: 69.0, center: Column( children: [ SizedBox( height: 10.0, ), Text( "${strengthToValue(snapshot?.data?.data?.sum?.consume ?? 0).toStringAsFixed(1)}", style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0), strutStyle: fixedLine, ), Text( "卡/分", style: Theme.of(context).textTheme.subtitle2, strutStyle: fixedLine, ) ], mainAxisSize: MainAxisSize.min, ), ), ), ), ), ), SizedBox( width: 15.0, ), Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Row( children: [ Row( children: [ Container( width: 10, height: 10, decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xffFFE600)), ), const SizedBox( width: 5.0, ), Text( "低强度", style: Theme.of(context).textTheme.bodyText1, strutStyle: fixedLine, ), const SizedBox( width: 12.0, ), Container( width: 10, height: 10, decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xffFFAA00)), ), const SizedBox( width: 5.0, ), Text( "强度适中", style: Theme.of(context).textTheme.bodyText1, strutStyle: fixedLine, ), const SizedBox( width: 12.0, ), Container( width: 10, height: 10, decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xffFF7323)), ), const SizedBox( width: 5.0, ), Text( "高强度", style: Theme.of(context).textTheme.bodyText1, strutStyle: fixedLine, ) ], ), Text( "单位:卡/分钟", style: Theme.of(context).textTheme.bodyText1, strutStyle: fixedLine, ) ], mainAxisAlignment: MainAxisAlignment.spaceBetween, ), ), Divider(), Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( children: [ Text( "${snapshot?.data?.data?.sum?.consume ?? 0}", style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0), ), Text( "运动消耗 (卡)", style: Theme.of(context).textTheme.bodyText2, ), ], mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, ), Column( children: [ Text( "${snapshot?.data?.data?.sum?.durationMinute ?? 0}", style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0), ), Text( "运动时长 (分钟)", style: Theme.of(context).textTheme.bodyText2, ), ], mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, ) ], ), ) ], ), ), ), ), SliverToBoxAdapter( child: Container( height: 140.0, margin: const EdgeInsets.only(top: 12.0), child: CustomPaint( painter: _ListBg(), child: FutureBuilder( future: createFutureType(1).asStream().map((event) => convert(event)).last, builder: (BuildContext context, AsyncSnapshot> snapshot) { var list = snapshot?.data; if (list == null || list?.isEmpty == true) return Container(); return ValueListenableBuilder( valueListenable: _valueNotifierIndex, builder: (BuildContext context, int value, Widget child) { return SingleChildScrollView( reverse: true, child: Padding( padding: EdgeInsets.only(top: 10.0), child: Row( children: list?.map((e) { var index = list.indexOf(e); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { _valueNotifierIndex.value = index; var page = max(0, strengthArr.indexOf(strengthToLabel(list[index].data))); _swiperController?.move(page); }, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ CustomPaint( child: SizedBox( width: MediaQuery.of(context).size.width / 7.0, height: 100.0, ), painter: _Dot(list, index), ), Container( height: 30.0, decoration: index == value ? BoxDecoration( border: Border(bottom: BorderSide(color: Theme.of(context).accentColor, width: 3.0)), ) : null, child: Center( child: Text( "${e.date}", style: e.date == "今日" ? Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor) : Theme.of(context).textTheme.subtitle1, ))), ], )); })?.toList(), )), scrollDirection: Axis.horizontal, ); }, ); }, ), )), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(top: 12.0), child: Container( width: double.infinity, height: 145.0, child: Swiper( controller: _swiperController, loop: false, itemBuilder: (BuildContext context, int index) { return Container( margin: const EdgeInsets.all(12.0), // decoration: circular(), // padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( index == 0 ? "低强度" : index == 1 ? "强度适中" : "高强度", style: Theme.of(context).textTheme.headline1, ), SizedBox( height: 5.0, ), Text( index == 0 ? "感觉舒适,呼吸有节奏,能自由进行交谈。该级别适用于恢复和基础心血功能训练,可以提高心脏的泵血能力和肌肉使用氧气的能力" : index == 1 ? "该级别训练强度适中,较难以进行交谈,呼吸加重。可以适当增强心肺功能,获得更强耐力" : "感觉难受,且呼吸急促,难以长时间维持该强度训练。可以增强力量和肌肉耐力,提高厌氧能力和乳酸阈值", style: Theme.of(context).textTheme.bodyText2, ), ], ), ); }, itemCount: 3), ), ), ) ], ), ); } Future> createFutureType(int type) async { Future> data; var time = DateTime.now(); switch (type) { case 0: data = api.getSportRecordListOneDay('${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}'); break; case 1: DateTime end = DateTime(time.year, time.month, time.day + 3); DateTime start = DateTime(time.year, time.month, end.day - 30); data = api.getSportRecordListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}', '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}'); break; } return data; } List<_DataItem> convert(RespData data) { List<_DataItem> items = []; List records = data?.data?.records ?? []; var time = DateTime.now(); DateTime end = DateTime(time.year, time.month, time.day + 3); for (var i = 0; i < 30; i++) { var day = DateTime(end.year, end.month, end.day - i); String tag = '${day.year}-${'${day.month}'.padLeft(2, '0')}-${'${day.day}'.padLeft(2, '0')}'; String date = (time.month == day.month && time.day == day.day) ? '今日' : (day.day == 1) ? '${day.month}.${day.day}' : '${day.day}'; int total = 0; for (var record in records) { if (tag == record.createdAt.split(" ")[0]) { total += record.consume; } } items.add(_DataItem(tag, date, total)); if (i == 3) { _swiperController?.move(max(0, strengthArr.indexOf(strengthToLabel(total)))); } } return items.reversed.toList(); } } class _DataItem { final String tag; final String date; final int data; _DataItem(this.tag, this.date, this.data); } class _Bg extends CustomPainter { final Paint _paint = Paint() ..color = const Color(0xffDCDCDC) ..strokeWidth = 0.5 ..style = PaintingStyle.stroke ..isAntiAlias = true; final ParagraphStyle _valueStyle = ParagraphStyle( textAlign: TextAlign.center, fontSize: 12, ); @override void paint(Canvas canvas, Size size) { final double cx = size.width / 2; final double cy = size.height / 2; final double radius = (size.width - 40) / 2; canvas.drawCircle(Offset(cx, cy), radius, _paint); canvas.save(); for (int i = 0; i < 4; i++) { canvas.translate(cx, cy); canvas.rotate(pi * 2 / 3); canvas.translate(-cx, -cy); canvas.drawLine(Offset(cx, cy - radius), Offset(cx, cy - radius + 4), _paint); } canvas.restore(); ParagraphBuilder pb = ParagraphBuilder(_valueStyle) ..pushStyle(ui.TextStyle(color: Color(0xff999999))) ..addText("0"); ParagraphConstraints constraints = ParagraphConstraints(width: 20.0); Paragraph paragraph = pb.build()..layout(constraints); paragraph.computeLineMetrics().forEach((element) { canvas.drawParagraph(paragraph, Offset(cx - element.width - 4, 2)); }); pb = ParagraphBuilder(_valueStyle) ..pushStyle(ui.TextStyle(color: Color(0xff999999))) ..addText("4.0"); constraints = ParagraphConstraints(width: 20.0); paragraph = pb.build()..layout(constraints); paragraph.computeLineMetrics().forEach((element) { canvas.drawParagraph(paragraph, Offset(size.width - 25, 140)); }); pb = ParagraphBuilder(_valueStyle) ..pushStyle(ui.TextStyle(color: Color(0xff999999))) ..addText("8.0"); constraints = ParagraphConstraints(width: 20.0); paragraph = pb.build()..layout(constraints); paragraph.computeLineMetrics().forEach((element) { canvas.drawParagraph(paragraph, Offset(6, 140.0)); }); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; } class _ListBg extends CustomPainter { final Paint _paint = Paint() ..color = const Color(0xffDCDCDC) ..strokeWidth = 0.5 ..isAntiAlias = true; final ParagraphStyle _valueStyle = ParagraphStyle( textAlign: TextAlign.right, fontSize: 12, ); @override void paint(Canvas canvas, Size size) { final double height = size.height - 40; final int split = 3; final double splitHeight = height / split; double _startY = 10; for (var i = 0; i < split; i++) { canvas.drawLine(Offset(0, _startY), Offset(size.width, _startY), _paint); _startY += splitHeight; ParagraphBuilder pb = ParagraphBuilder(_valueStyle) ..pushStyle(ui.TextStyle(color: Color(0xff999999))) ..addText("${(8.0 - (4.0 * i)).toStringAsFixed(1)}"); ParagraphConstraints constraints = ParagraphConstraints(width: size.width - 10); Paragraph paragraph = pb.build()..layout(constraints); paragraph.computeLineMetrics().forEach((element) { canvas.drawParagraph(paragraph, Offset(0, splitHeight * i + element.baseline / 2 + 14)); }); } canvas.drawLine(Offset(0, size.height - 30), Offset(size.width, size.height - 30), _paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; } class _Dot extends CustomPainter { final List<_DataItem> items; final int index; static const Color _color = const Color(0xffFFC400); final Paint _paint = Paint() ..maskFilter = MaskFilter.blur(BlurStyle.solid, 3) ..isAntiAlias = true; final Paint _line = Paint() ..color = _color ..strokeWidth = 0.5 ..isAntiAlias = true; final Paint _polygonal = Paint() ..color = _color ..strokeWidth = 1 ..isAntiAlias = true; _Dot(this.items, this.index); @override void paint(Canvas canvas, Size size) { if (index > items.length - 4) return; double _height = size.height; final double cx = size.width / 2; final double progress = min(1.0, items[index].data / 60.0 / 12.0); final double cy = _height * (1.0 - progress); print("$progress $cy ${items[index].data}"); final double radius = size.width * 0.15 / 2; _paint.color = _color; canvas.drawCircle(Offset(cx, cy), radius, _paint); if (index == items.length - 4) { double _h = cy; while (_h < _height) { canvas.drawLine(Offset(cx, _h), Offset(cx, min(_height, _h + 3)), _line); _h += 5; } } for (var i = index - 1; i < index + 1; i += 2) { if (i < 0) continue; if (i >= items.length) continue; double _cx = (cx - size.width) * (index - i); double _cy = size.height * (1.0 - min(1.0, items[i].data / 60.0 / 12.0)); canvas.drawLine(Offset(_cx, _cy), Offset(cx, cy), _polygonal); } } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }