import 'dart:math'; import 'package:mobdr/config/constant.dart'; import 'package:mobdr/library/flutter_overboard/circular_clipper.dart'; import 'package:mobdr/library/flutter_overboard/overboard_animator.dart'; import 'package:mobdr/library/flutter_overboard/page_model.dart'; import 'package:mobdr/ui/reusable/cache_image_network.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; enum SwipeDirection { LEFT_TO_RIGHT, RIGHT_TO_LEFT, SKIP_TO_LAST } class OverBoard extends StatefulWidget { final List pages; final Offset? center; final bool? showBullets; final VoidCallback finishCallback; final VoidCallback? skipCallback; final OverBoardAnimator? animator; final String? skipText, nextText, finishText; OverBoard( {Key? key, required this.pages, this.center, this.showBullets, this.skipText, this.nextText, this.finishText, required this.finishCallback, this.animator, this.skipCallback}) : super(key: key); @override _OverBoardState createState() => _OverBoardState(); } class _OverBoardState extends State with TickerProviderStateMixin { late OverBoardAnimator _animator; ScrollController _scrollController = new ScrollController(); double _bulletPadding = 5.0, _bulletSize = 10.0, _bulletContainerWidth = 0; int _counter = 0, _last = 0; int _total = 0; double initial = 0, distance = 0; Random random = new Random(); SwipeDirection _swipeDirection = SwipeDirection.RIGHT_TO_LEFT; @override void initState() { super.initState(); _animator = new OverBoardAnimator(this); _total = widget.pages.length; _animate(); } @override Widget build(BuildContext context) { return Container( child: _getStack(), ); } _getStack() { return GestureDetector( onPanStart: (DragStartDetails details) { initial = details.globalPosition.dx; }, onPanUpdate: (DragUpdateDetails details) { distance = details.globalPosition.dx - initial; }, onPanEnd: (DragEndDetails details) { initial = 0.0; setState(() { _last = _counter; }); if (distance > 1 && _counter > 0) { setState(() { _counter--; _swipeDirection = SwipeDirection.LEFT_TO_RIGHT; }); _animate(); } else if (distance < 0 && _counter < _total - 1) { setState(() { _counter++; _swipeDirection = SwipeDirection.RIGHT_TO_LEFT; }); _animate(); } }, child: Stack( children: [ _getPage(_last), AnimatedBuilder( animation: _animator.getAnimator(), builder: (context, child) { return ClipOval( clipper: CircularClipper( _animator.getAnimator().value, widget.center), child: _getPage(_counter)); }, child: Container(), ), Positioned( left: 0, right: 0, bottom: 0, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Opacity( child: TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( (Set states) => PRIMARY_COLOR, ), ), onPressed: (widget.skipCallback != null ? widget.skipCallback : _skip), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 18), child: Text(widget.skipText ?? "SKIP"), )), opacity: (_counter < _total - 1) ? 1.0 : 0.0, ), Expanded( child: Center(child: LayoutBuilder( builder: (context, constraints) { _bulletContainerWidth = constraints.maxWidth - 40.0; return Container( padding: const EdgeInsets.all(20.0), child: ((widget.showBullets ?? true) ? SingleChildScrollView( physics: NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal, controller: _scrollController, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (int i = 0; i < _total; i++) Padding( padding: EdgeInsets.all(_bulletPadding), child: AnimatedContainer( duration: Duration(milliseconds: 150), height: _bulletSize, width: (i == _counter) ? _bulletSize * 2 : _bulletSize, decoration: BoxDecoration( color: (i == _counter) ? PRIMARY_COLOR : Colors.grey[300], borderRadius: BorderRadius.circular(10))), ) ], ), ) : Container()), ); }, )), ), (_counter < _total - 1 ? TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( (Set states) => PRIMARY_COLOR, ), ), onPressed: _next, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text(widget.nextText ?? "NEXT"), )) : TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( (Set states) => PRIMARY_COLOR, ), ), onPressed: widget.finishCallback, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text(widget.finishText ?? "FINISH"), ))), ], ), ), ], ), ); } _getPage(index) { PageModel page = widget.pages[index]; return Container( width: double.infinity, height: double.infinity, color: page.color, child: page.child != null ? Center( child: page.doAnimateChild! ? AnimatedBoard( animator: _animator, child: page.child, ) : page.child, ) : (kIsWeb) ? SingleChildScrollView( child: Padding( padding: EdgeInsets.only(top: 64), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ page.doAnimateImage! ? AnimatedBoard( animator: _animator, child: new Padding( padding: new EdgeInsets.only(bottom: 25.0), child: (page.imageAssetPath != null) ? new Image.asset(page.imageAssetPath!, width: 300.0, height: 300.0) : buildCacheNetworkImage( width: 300, height: 300, url: page.imageFromUrl, plColor: Colors.transparent), ), ) : new Padding( padding: new EdgeInsets.only(bottom: 25.0), child: (page.imageAssetPath != null) ? new Image.asset(page.imageAssetPath!, width: 300.0, height: 300.0) : buildCacheNetworkImage( width: 300, height: 300, url: page.imageFromUrl, plColor: Colors.transparent), ), Padding( padding: new EdgeInsets.only( top: 10.0, bottom: 30.0, left: 30.0, right: 30.0), child: new Text( page.title!, textAlign: TextAlign.center, style: new TextStyle( color: Colors.black, fontSize: 24.0, fontWeight: FontWeight.bold), ), ), Padding( padding: new EdgeInsets.only( bottom: 75.0, left: 30.0, right: 30.0), child: new Text( page.body!, textAlign: TextAlign.center, style: new TextStyle( color: SOFT_GREY, fontSize: 15, ), ), ), ], ), ), ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ page.doAnimateImage! ? AnimatedBoard( animator: _animator, child: new Padding( padding: new EdgeInsets.only(bottom: 25.0), child: (page.imageAssetPath != null) ? new Image.asset(page.imageAssetPath!, width: 300.0, height: 300.0) : buildCacheNetworkImage( width: 300, height: 300, url: page.imageFromUrl, plColor: Colors.transparent), ), ) : new Padding( padding: new EdgeInsets.only(bottom: 25.0), child: (page.imageAssetPath != null) ? new Image.asset(page.imageAssetPath!, width: 300.0, height: 300.0) : buildCacheNetworkImage( width: 300, height: 300, url: page.imageFromUrl, plColor: Colors.transparent), ), Padding( padding: new EdgeInsets.only( top: 10.0, bottom: 30.0, left: 30.0, right: 30.0), child: new Text( page.title!, textAlign: TextAlign.center, style: new TextStyle( color: Colors.black, fontSize: 24.0, fontWeight: FontWeight.bold), ), ), Padding( padding: new EdgeInsets.only( bottom: 75.0, left: 30.0, right: 30.0), child: new Text( page.body!, textAlign: TextAlign.center, style: new TextStyle( color: SOFT_GREY, fontSize: 15, ), ), ), ], ), ); } _next() { setState(() { _swipeDirection = SwipeDirection.RIGHT_TO_LEFT; _last = _counter; _counter++; }); _animate(); } _skip() { setState(() { _swipeDirection = SwipeDirection.SKIP_TO_LAST; _last = _counter; _counter = _total - 1; }); _animate(); } _animate() { _animator.getController().forward(from: 0.0); double _bulletDimension = (_bulletPadding * 2) + (_bulletSize); double _scroll = _bulletDimension * _counter; double _maxScroll = _bulletDimension * _total - 1; if (_scroll > _bulletContainerWidth && _swipeDirection == SwipeDirection.RIGHT_TO_LEFT) { double _scrollDistance = (((_scroll - _bulletContainerWidth) ~/ _bulletDimension) + 1) * _bulletDimension; _scrollController.animateTo(_scrollDistance, curve: Curves.easeIn, duration: Duration(milliseconds: 100)); } else if (_scroll < (_maxScroll - _bulletContainerWidth) && _swipeDirection == SwipeDirection.LEFT_TO_RIGHT) { _scrollController.animateTo(_scroll, curve: Curves.easeIn, duration: Duration(milliseconds: 100)); } else if (_swipeDirection == SwipeDirection.SKIP_TO_LAST) { _scrollController.animateTo(_maxScroll, curve: Curves.easeIn, duration: Duration(milliseconds: 100)); } } } class AnimatedBoard extends StatelessWidget { final Widget? child; final OverBoardAnimator? animator; const AnimatedBoard({Key? key, this.animator, this.child}) : super(key: key); @override Widget build(BuildContext context) { return Transform( transform: new Matrix4.translationValues( 0.0, 50.0 * (1.0 - animator!.getAnimator().value), 0.0), child: new Padding( padding: new EdgeInsets.only(bottom: 25.0), child: child, ), ); } }