import 'dart:math' as math; import 'package:fikoyt_hmi_pano/apps/pages/fitness_app/fitness_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:vector_math/vector_math.dart' as vector; class WaveView extends StatefulWidget { final double percentageValue; const WaveView({super.key, this.percentageValue = 100.0}); @override _WaveViewState createState() => _WaveViewState(); } class _WaveViewState extends State with TickerProviderStateMixin { AnimationController? animationController; AnimationController? waveAnimationController; Offset bottleOffset1 = Offset(0, 0); List animList1 = []; Offset bottleOffset2 = Offset(60, 0); List animList2 = []; @override void initState() { animationController = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); waveAnimationController = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); animationController!.addStatusListener((status) { if (status == AnimationStatus.completed) { animationController?.reverse(); } else if (status == AnimationStatus.dismissed) { animationController?.forward(); } }); waveAnimationController!.addListener(() { animList1.clear(); for (int i = -2 - bottleOffset1.dx.toInt(); i <= 60 + 2; i++) { animList1.add( Offset( i.toDouble() + bottleOffset1.dx.toInt(), math.sin((waveAnimationController!.value * 360 - i) % 360 * vector.degrees2Radians) * 2 + (((100 - widget.percentageValue) * 160 / 100)), ), ); } animList2.clear(); for (int i = -2 - bottleOffset2.dx.toInt(); i <= 60 + 2; i++) { animList2.add( Offset( i.toDouble() + bottleOffset2.dx.toInt(), math.sin((waveAnimationController!.value * 360 - i) % 360 * vector.degrees2Radians) * 2 + (((100 - widget.percentageValue) * 160 / 100)), ), ); } }); waveAnimationController?.repeat(); animationController?.forward(); super.initState(); } @override void dispose() { animationController?.dispose(); waveAnimationController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: AnimatedBuilder( animation: CurvedAnimation( parent: animationController!, curve: Curves.easeInOut, ), builder: (context, child) => Stack( children: [ ClipPath( clipper: WaveClipper(animationController!.value, animList1), child: Container( decoration: BoxDecoration( color: FitnessAppTheme.nearlyDarkBlue.withOpacity(0.5), borderRadius: BorderRadius.only(topLeft: Radius.circular(80.0), bottomLeft: Radius.circular(80.0), bottomRight: Radius.circular(80.0), topRight: Radius.circular(80.0)), gradient: LinearGradient( colors: [FitnessAppTheme.nearlyDarkBlue.withOpacity(0.2), FitnessAppTheme.nearlyDarkBlue.withOpacity(0.5)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), ), ), ClipPath( clipper: WaveClipper(animationController!.value, animList2), child: Container( decoration: BoxDecoration( color: FitnessAppTheme.nearlyDarkBlue, gradient: LinearGradient( colors: [FitnessAppTheme.nearlyDarkBlue.withOpacity(0.4), FitnessAppTheme.nearlyDarkBlue], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.only(topLeft: Radius.circular(80.0), bottomLeft: Radius.circular(80.0), bottomRight: Radius.circular(80.0), topRight: Radius.circular(80.0)), ), ), ), Padding( padding: const EdgeInsets.only(top: 48), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.percentageValue.round().toString(), textAlign: TextAlign.center, style: TextStyle( fontFamily: FitnessAppTheme.fontName, fontWeight: FontWeight.w500, fontSize: 24, letterSpacing: 0.0, color: FitnessAppTheme.white, ), ), Padding( padding: const EdgeInsets.only(top: 3.0), child: Text( '%', textAlign: TextAlign.center, style: TextStyle( fontFamily: FitnessAppTheme.fontName, fontWeight: FontWeight.w500, fontSize: 14, letterSpacing: 0.0, color: FitnessAppTheme.white, ), ), ), ], ), ), ), Positioned( top: 0, left: 6, bottom: 8, child: ScaleTransition( alignment: Alignment.center, scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: animationController!, curve: Interval(0.0, 1.0, curve: Curves.fastOutSlowIn))), child: Container( width: 2, height: 2, decoration: BoxDecoration( color: FitnessAppTheme.white.withOpacity(0.4), shape: BoxShape.circle, ), ), ), ), Positioned( left: 24, right: 0, bottom: 16, child: ScaleTransition( alignment: Alignment.center, scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: animationController!, curve: Interval(0.4, 1.0, curve: Curves.fastOutSlowIn))), child: Container( width: 4, height: 4, decoration: BoxDecoration( color: FitnessAppTheme.white.withOpacity(0.4), shape: BoxShape.circle, ), ), ), ), Positioned( left: 0, right: 24, bottom: 32, child: ScaleTransition( alignment: Alignment.center, scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: animationController!, curve: Interval(0.6, 0.8, curve: Curves.fastOutSlowIn))), child: Container( width: 3, height: 3, decoration: BoxDecoration( color: FitnessAppTheme.white.withOpacity(0.4), shape: BoxShape.circle, ), ), ), ), Positioned( top: 0, right: 20, bottom: 0, child: Transform( transform: Matrix4.translationValues(0.0, 16 * (1.0 - animationController!.value), 0.0), child: Container( width: 4, height: 4, decoration: BoxDecoration( color: FitnessAppTheme.white.withOpacity(animationController!.status == AnimationStatus.reverse ? 0.0 : 0.4), shape: BoxShape.circle, ), ), ), ), Column( children: [ AspectRatio( aspectRatio: 1, child: Image.asset("assets/fitness_app/bottle.png"), ), ], ) ], ), ), ); } } class WaveClipper extends CustomClipper { final double animation; List waveList1 = []; WaveClipper(this.animation, this.waveList1); @override Path getClip(Size size) { Path path = Path(); path.addPolygon(waveList1, false); path.lineTo(size.width, size.height); path.lineTo(0.0, size.height); path.close(); return path; } @override bool shouldReclip(WaveClipper oldClipper) => animation != oldClipper.animation; }