import 'dart:math' as math; import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart' as extended; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; import 'package:flutter_timeline/flutter_timeline.dart'; import 'package:flutter_timeline/indicator_position.dart'; import 'package:sport/bean/game.dart'; import 'package:sport/bean/game_record.dart'; import 'package:sport/bean/sport_detail.dart'; import 'package:sport/provider/lib/provider_widget.dart'; import 'package:sport/provider/lib/view_state_lifecycle.dart'; import 'package:sport/provider/sport_history_model.dart'; import 'package:sport/services/api/inject_api.dart'; import 'package:sport/services/api/resp.dart'; import 'package:sport/utils/DateFormat.dart'; import 'package:sport/widgets/appbar.dart'; import 'package:sport/widgets/decoration.dart'; import 'package:sport/widgets/error.dart'; import 'package:sport/widgets/loading.dart'; import 'package:sport/widgets/misc.dart'; import 'package:sport/widgets/space.dart'; class SportHistoryPage extends StatefulWidget { final GameInfoData details; SportHistoryPage(this.details); @override State createState() => _PageState(); } class _PageState extends State with TickerProviderStateMixin { ScrollController _controller; TabController _tabController; double _expandedHeight = 230.0; int _brightness = 0; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _controller = ScrollController() ..addListener(() { if (_controller.position.pixels >= _expandedHeight / 2) { if (_brightness == 0) { setState(() { _brightness = 1; }); } } else { if (_brightness == 1) { setState(() { _brightness = 0; }); } } }); } @override void dispose() { _tabController?.dispose(); _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final double tabHeader = 40.0; final double statusBarHeight = MediaQuery.of(context).padding.top; final double pinnedHeaderHeight = //statusBar height statusBarHeight + //pinned SliverAppBar height in header kToolbarHeight + tabHeader; return Scaffold( backgroundColor: Colors.white, body: extended.NestedScrollView( controller: _controller, pinnedHeaderSliverHeightBuilder: () { return pinnedHeaderHeight; }, innerScrollPositionKeyBuilder: () { String index = 'Tab${_tabController?.index}'; return Key(index); }, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( pinned: true, expandedHeight: _expandedHeight, backgroundColor: Colors.white, forceElevated: innerBoxIsScrolled, titleSpacing: 0, elevation: 0, iconTheme: IconThemeData(color: _brightness == 0 ? Colors.white : Colors.black), brightness: _brightness == 0 ? Brightness.dark : Brightness.light, leading: IconButton( icon: Image.asset("lib/assets/img/topbar_return${_brightness == 0 ? "_white" : ""}.png"), onPressed: () { Navigator.of(context).maybePop(); }, ), title: _brightness == 0 ? Text("") : Text( "${widget.details.name}", style: titleStyle, ), flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.pin, background: Container( child: Stack( fit: StackFit.expand, children: [ CachedNetworkImage( imageUrl: widget.details.coverHorizontal, fit: BoxFit.fill, ), BackdropFilter( filter: ImageFilter.blur(sigmaX: 30.0, sigmaY: 30.0), child: Center( child: Container( color: Colors.black.withOpacity(.3), ), ), ), Center( child: Column( children: [ ClipRRect( child: CachedNetworkImage( width: 90.0, height: 90.0, imageUrl: widget.details.cover, fit: BoxFit.cover, ), borderRadius: new BorderRadius.all(Radius.circular(6.0)), ), Padding( padding: const EdgeInsets.all(12.0), child: Text( widget.details.name, style: Theme.of(context).textTheme.headline4, ), ) ], mainAxisSize: MainAxisSize.min, ), ), // Positioned( // left: 0, // right: 0, // bottom: 48, // child: Container( // height: 10, // decoration: BoxDecoration(borderRadius: BorderRadius.vertical(top: Radius.circular(10)), color: Colors.white), // ), // ), ], ), )), bottom: PreferredSize( preferredSize: Size.fromHeight(40.0), child: Container( // color: Colors.white, // transform: Matrix4.translationValues(0, -10, 0), decoration: BoxDecoration(borderRadius: BorderRadius.vertical(top: Radius.circular(10)), color: Colors.white), child: Center( child: Container( margin: EdgeInsets.only(top: 10.0), height: 36.0, child: TabBar( controller: _tabController, isScrollable: true, labelPadding: EdgeInsets.symmetric(horizontal: 20), indicatorWeight: 3, indicatorPadding: EdgeInsets.symmetric(horizontal: 6), tabs: [ Tab( text: '数据', ), Tab(text: '记录') ], ), ), )), ), ) ]; }, body: TabBarView( controller: _tabController, children: [ NestedScrollViewInnerScrollPositionKeyWidget( const Key('Tab0'), _DataPage(details: widget.details), ), NestedScrollViewInnerScrollPositionKeyWidget( const Key('Tab1'), _ListPage(details: widget.details), ) ]), ), ); } } class _DataPage extends StatefulWidget { final GameInfoData details; const _DataPage({Key key, this.details}) : super(key: key); @override State createState() => _DataPageState(); } class _DataPageState extends State<_DataPage> with InjectApi { Future> _future; @override void initState() { super.initState(); _future = api.getGameRecord(widget.details.id); } @override Widget build(BuildContext context) { return SingleChildScrollView( child: FutureBuilder( future: _future, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.connectionState != ConnectionState.done) return RequestLoadingWidget(); var _data = snapshot.data?.data; if (_data == null) return Container(); return Column( children: [_group("今日运动", _data.today), _group("累计运动", _data.sum)], ); }, ), ); } Widget _group(String name, RecordsTodaySum sum) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Column(children: [ Container( width: double.infinity, height: 24.0, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Align( alignment: Alignment.centerLeft, child: Text( name, style: Theme.of(context).textTheme.bodyText2, ), ), ), color: Color(0xfff1f1f1), ), const SizedBox( height: 12.0, ), _row("datasummary_icon_duration", "累计时长", "${sum.durationMinute}", "分钟"), _row("datasummary_icon_score", "最高评分", "${sum.scoreMax}", ""), _row("datasummary_icon_frequency", "累计次数", "${sum.times}", "次"), _row("datasummary_icon_consume", "累计消耗", "${sum.consume}", "卡"), _row("datasummary_icon_greaseefficiency", "燃脂效率", "${sum.consumeRate.toStringAsFixed(1)}", "卡/分钟"), Divider(), _row("datasummary_icon_squat", "下蹲频率", "${sum.crouchRate}", "次/分钟"), _row("datasummary_icon_jump", "跳跃频率", "${sum.jumpRate}", "次/分钟"), _row("datasummary_icon_steps", "游戏步数", "${sum.stepCount}", "步"), ]), ); } Widget _row(String icon, String name, String value, String unit) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), child: Row( children: [ Image.asset("lib/assets/img/$icon.png"), const SizedBox( width: 8.0, ), Expanded(child: Text(name, style: Theme.of(context).textTheme.subtitle1)), ConstrainedBox( constraints: BoxConstraints(minWidth: 100.0), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(value, style: Theme.of(context).textTheme.headline1), Text(unit, style: Theme.of(context).textTheme.subtitle2), ], )) ], ), ); } } class _ListPage extends StatefulWidget { final GameInfoData details; const _ListPage({Key key, this.details}) : super(key: key); @override State createState() => _ListPageState(); } class _ListPageState extends ViewStateLifecycle<_ListPage, SportHistoryModel> { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { var color = const Color(0xffFFC400); var dot = Container( width: 7.0, height: 7.0, decoration: BoxDecoration(color: color, shape: BoxShape.circle, boxShadow: [BoxShadow(color: color, blurRadius: 5, offset: Offset(0, 0))]), ); return TimelineTheme( data: TimelineThemeData(lineColor: const Color(0xffFFC400), strokeWidth: 1), child: ProviderWidget( model: model, onModelReady: (model) => model.initData(), builder: (_, model, __) { var list = model.list; if(list.isEmpty) return RequestErrorWidget(null, msg: "暂无记录",); var map = Map.fromIterable(list, key: (key) => key.tag, value: (value) { return list.where((item) => item.tag == value.tag).toList(); }); // print("11111111111111111111 $list"); // print("$map"); return Timeline( primary: true, padding: EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 12.0), indicatorSize: 12.0, events: map.keys.map((e) { var gameList = map[e]; return TimelineEventDisplay( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 6.0), child: Text( e, style: Theme.of(context).textTheme.bodyText2, ), ), Column( children: gameList .map((item) => Container( margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), decoration: card(), child: Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 80.0), child: Column( children: [ Text( item.mode == 0 ? "闯关模式" : item.mode == 1 ? "匹配模式" : "好友模式", style: Theme.of(context).textTheme.headline3, ), const SizedBox( height: 3.0, ), Text( "${DateFormat.formatCreateAtHHmm(item.createdAt)}", style: Theme.of(context).textTheme.bodyText2, ), ], ), ), Container( margin: const EdgeInsets.symmetric(horizontal: 16.0), height: 70.0, child: DashedRect(color: Color(0xff979797), strokeWidth: 0.5, gap: 3.0), ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "评分:${item.score}", style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor), ), const SizedBox( height: 3.0, ), Text( "时长:${(item.duration ?? 0) ~/ 60}分${(item.duration ?? 0) % 60}秒", style: Theme.of(context).textTheme.subtitle1, ), const SizedBox( height: 3.0, ), Text( "消耗:${item.consume} 卡", style: Theme.of(context).textTheme.subtitle1, ), ], ) ], ), )) .toList(), ) ], ), indicatorSize: 9.0, indicatorOffset: Offset(0, 3.5), indicator: dot); }).toList(), anchor: IndicatorPosition.top, ); }), ); } Widget _buildHistoryItemWidget(RecordsTodaySum item) { return Container( color: Colors.white, padding: const EdgeInsets.fromLTRB(12.0, 6, 12.0, 6), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.screen == 1 ? "投屏运动" : "手机运动", style: Theme.of(context).textTheme.subtitle1, ), Space( height: 4, ), Text( "日期:${item.createdAt}", style: Theme.of(context).textTheme.bodyText1, ) ], ), ), Text( "${item.score.toStringAsFixed(1)}", style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor), ), Text("分", style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 12)), ], ), ); } @override SportHistoryModel createModel() => SportHistoryModel(widget.details.id); } class DashedRect extends StatelessWidget { final Color color; final double strokeWidth; final double gap; DashedRect({this.color = Colors.black, this.strokeWidth = 1.0, this.gap = 5.0}); @override Widget build(BuildContext context) { return Container( child: Padding( padding: EdgeInsets.all(strokeWidth / 2), child: CustomPaint( painter: DashRectPainter(color: color, strokeWidth: strokeWidth, gap: gap), ), ), ); } } class DashRectPainter extends CustomPainter { double strokeWidth; Color color; double gap; DashRectPainter({this.strokeWidth = 5.0, this.color = Colors.red, this.gap = 5.0}); @override void paint(Canvas canvas, Size size) { Paint dashedPaint = Paint() ..color = color ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; double x = size.width; double y = size.height; Path _topPath = getDashedPath( a: math.Point(0, 0), b: math.Point(x, 0), gap: gap, ); Path _rightPath = getDashedPath( a: math.Point(x, 0), b: math.Point(x, y), gap: gap, ); Path _bottomPath = getDashedPath( a: math.Point(0, y), b: math.Point(x, y), gap: gap, ); Path _leftPath = getDashedPath( a: math.Point(0, 0), b: math.Point(0.001, y), gap: gap, ); canvas.drawPath(_topPath, dashedPaint); canvas.drawPath(_rightPath, dashedPaint); canvas.drawPath(_bottomPath, dashedPaint); canvas.drawPath(_leftPath, dashedPaint); } Path getDashedPath({ @required math.Point a, @required math.Point b, @required gap, }) { Size size = Size(b.x - a.x, b.y - a.y); Path path = Path(); path.moveTo(a.x, a.y); bool shouldDraw = true; math.Point currentPoint = math.Point(a.x, a.y); num radians = math.atan(size.height / size.width); num dx = math.cos(radians) * gap < 0 ? math.cos(radians) * gap * -1 : math.cos(radians) * gap; num dy = math.sin(radians) * gap < 0 ? math.sin(radians) * gap * -1 : math.sin(radians) * gap; while (currentPoint.x <= b.x && currentPoint.y <= b.y) { shouldDraw ? path.lineTo(currentPoint.x, currentPoint.y) : path.moveTo(currentPoint.x, currentPoint.y); shouldDraw = !shouldDraw; currentPoint = math.Point( currentPoint.x + dx, currentPoint.y + dy, ); } return path; } @override bool shouldRepaint(CustomPainter oldDelegate) { return false; } }