// Package flutter_swiper: // https://pub.dartlang.org/packages/flutter_swiper import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'flutter_page_indicator.dart'; import 'transformer_page_view.dart'; typedef SwiperOnTap = void Function(int index); typedef SwiperDataBuilder = Widget Function( BuildContext context, dynamic data, int index); /// default auto play delay const int kDefaultAutoplayDelayMs = 3000; /// Default auto play transition duration (in millisecond) const int kDefaultAutoplayTransactionDuration = 300; const int kMaxValue = 2000000000; const int kMiddleValue = 1000000000; // ignore_for_file: constant_identifier_names enum SwiperLayout { DEFAULT, STACK, TINDER, CUSTOM } class Swiper extends StatefulWidget { /// If set true , the pagination will display 'outer' of the 'content' container. final bool outer; /// Inner item height, this property is valid if layout=STACK or layout=TINDER or LAYOUT=CUSTOM, final double itemHeight; /// Inner item width, this property is valid if layout=STACK or layout=TINDER or LAYOUT=CUSTOM, final double itemWidth; // height of the inside container,this property is valid when outer=true,otherwise the inside container size is controlled by parent widget final double containerHeight; // width of the inside container,this property is valid when outer=true,otherwise the inside container size is controlled by parent widget final double containerWidth; /// Build item on index final IndexedWidgetBuilder itemBuilder; /// Support transform like Android PageView did /// `itemBuilder` and `transformItemBuilder` must have one not null final PageTransformer transformer; /// count of the display items final int itemCount; final ValueChanged onIndexChanged; ///auto play config final bool autoplay; ///Duration of the animation between transactions (in millisecond). final int autoplayDelay; ///disable auto play when interaction final bool autoplayDisableOnInteraction; ///auto play transition duration (in millisecond) final int duration; ///horizontal/vertical final Axis scrollDirection; ///transition curve final Curve curve; /// Set to false to disable continuous loop mode. final bool loop; ///Index number of initial slide. ///If not set , the `Swiper` is 'uncontrolled', which means manage index by itself ///If set , the `Swiper` is 'controlled', which means the index is fully managed by parent widget. final int index; ///Called when tap final SwiperOnTap onTap; ///The swiper pagination plugin final SwiperPlugin pagination; ///the swiper control button plugin final SwiperPlugin control; ///other plugins, you can custom your own plugin final List plugins; /// final SwiperController controller; final ScrollPhysics physics; /// final double viewportFraction; /// Build in layouts final SwiperLayout layout; /// this value is valid when layout == SwiperLayout.CUSTOM final CustomLayoutOption customLayoutOption; // This value is valid when viewportFraction is set and < 1.0 final double scale; // This value is valid when viewportFraction is set and < 1.0 final double fade; final PageIndicatorLayout indicatorLayout; const Swiper({ this.itemBuilder, this.indicatorLayout = PageIndicatorLayout.NONE, /// this.transformer, @required this.itemCount, this.autoplay = false, this.layout = SwiperLayout.DEFAULT, this.autoplayDelay = kDefaultAutoplayDelayMs, this.autoplayDisableOnInteraction = true, this.duration = kDefaultAutoplayTransactionDuration, this.onIndexChanged, this.index, this.onTap, this.control, this.loop = true, this.curve = Curves.ease, this.scrollDirection = Axis.horizontal, this.pagination, this.plugins, this.physics, Key key, this.controller, this.customLayoutOption, /// since v1.0.0 this.containerHeight, this.containerWidth, this.viewportFraction = 1.0, this.itemHeight, this.itemWidth, this.outer = false, this.scale, this.fade, }) : assert(itemBuilder != null || transformer != null, "itemBuilder and transformItemBuilder must not be both null"), assert( !loop || ((loop && layout == SwiperLayout.DEFAULT && (indicatorLayout == PageIndicatorLayout.SCALE || indicatorLayout == PageIndicatorLayout.COLOR || indicatorLayout == PageIndicatorLayout.NONE)) || (loop && layout != SwiperLayout.DEFAULT)), "Only support `PageIndicatorLayout.SCALE` and `PageIndicatorLayout.COLOR`when layout==SwiperLayout.DEFAULT in loop mode"), super(key: key); factory Swiper.children({ @required List children, bool autoplay = false, PageTransformer transformer, int autoplayDelay = kDefaultAutoplayDelayMs, bool reverse = false, bool autoplayDisableOnInteraction = true, int duration = kDefaultAutoplayTransactionDuration, ValueChanged onIndexChanged, int index, SwiperOnTap onTap, bool loop = true, Curve curve = Curves.ease, Axis scrollDirection = Axis.horizontal, SwiperPlugin pagination, SwiperPlugin control, List plugins, SwiperController controller, Key key, CustomLayoutOption customLayoutOption, ScrollPhysics physics, double containerHeight, double containerWidth, double viewportFraction = 1.0, double itemHeight, double itemWidth, bool outer = false, double scale = 1.0, }) { assert(children != null, "children must not be null"); return Swiper( transformer: transformer, customLayoutOption: customLayoutOption, containerHeight: containerHeight, containerWidth: containerWidth, viewportFraction: viewportFraction, itemHeight: itemHeight, itemWidth: itemWidth, outer: outer, scale: scale, autoplay: autoplay, autoplayDelay: autoplayDelay, autoplayDisableOnInteraction: autoplayDisableOnInteraction, duration: duration, onIndexChanged: onIndexChanged, index: index, onTap: onTap, curve: curve, scrollDirection: scrollDirection, pagination: pagination, control: control, controller: controller, loop: loop, plugins: plugins, physics: physics, key: key, itemBuilder: (context, index) { return children[index]; }, itemCount: children.length); } factory Swiper.list({ PageTransformer transformer, List list, CustomLayoutOption customLayoutOption, SwiperDataBuilder builder, bool autoplay = false, int autoplayDelay = kDefaultAutoplayDelayMs, bool reverse = false, bool autoplayDisableOnInteraction = true, int duration = kDefaultAutoplayTransactionDuration, ValueChanged onIndexChanged, int index, SwiperOnTap onTap, bool loop = true, Curve curve = Curves.ease, Axis scrollDirection = Axis.horizontal, SwiperPlugin pagination, SwiperPlugin control, List plugins, SwiperController controller, Key key, ScrollPhysics physics, double containerHeight, double containerWidth, double viewportFraction = 1.0, double itemHeight, double itemWidth, bool outer = false, double scale = 1.0, }) { return Swiper( transformer: transformer, customLayoutOption: customLayoutOption, containerHeight: containerHeight, containerWidth: containerWidth, viewportFraction: viewportFraction, itemHeight: itemHeight, itemWidth: itemWidth, outer: outer, scale: scale, autoplay: autoplay, autoplayDelay: autoplayDelay, autoplayDisableOnInteraction: autoplayDisableOnInteraction, duration: duration, onIndexChanged: onIndexChanged, index: index, onTap: onTap, curve: curve, key: key, scrollDirection: scrollDirection, pagination: pagination, control: control, controller: controller, loop: loop, plugins: plugins, physics: physics, itemBuilder: (context, index) { return builder(context, list[index], index); }, itemCount: list.length); } @override State createState() { return _SwiperState(); } } abstract class _SwiperTimerMixin extends State { Timer _timer; SwiperController _controller; @override void initState() { _controller = widget.controller; _controller ??= SwiperController(); _controller.addListener(_onController); _handleAutoplay(); super.initState(); } void _onController() { switch (_controller.event) { case SwiperController.START_AUTOPLAY: { if (_timer == null) { _startAutoplay(); } } break; case SwiperController.STOP_AUTOPLAY: { if (_timer != null) { _stopAutoplay(); } } break; } } @override void didUpdateWidget(Swiper oldWidget) { if (_controller != oldWidget.controller) { if (oldWidget.controller != null) { oldWidget.controller.removeListener(_onController); _controller = oldWidget.controller; _controller.addListener(_onController); } } _handleAutoplay(); super.didUpdateWidget(oldWidget); } @override void dispose() { if (_controller != null) { _controller.removeListener(_onController); // _controller.dispose(); } _stopAutoplay(); super.dispose(); } bool _autoplayEnabled() { return _controller.autoplay ?? widget.autoplay; } void _handleAutoplay() { if (_autoplayEnabled() && _timer != null) return; _stopAutoplay(); if (_autoplayEnabled()) { _startAutoplay(); } } void _startAutoplay() { assert(_timer == null, "Timer must be stopped before start!"); _timer = Timer.periodic(Duration(milliseconds: widget.autoplayDelay), _onTimer); } void _onTimer(Timer timer) { _controller.next(animation: true); } void _stopAutoplay() { if (_timer != null) { _timer.cancel(); _timer = null; } } } class _SwiperState extends _SwiperTimerMixin { int _activeIndex; TransformerPageController _pageController; Widget _wrapTap(BuildContext context, int index) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { widget.onTap(index); }, child: widget.itemBuilder(context, index), ); } @override void initState() { _activeIndex = widget.index ?? 0; if (_isPageViewLayout()) { _pageController = TransformerPageController( initialPage: widget.index, loop: widget.loop, itemCount: widget.itemCount, reverse: widget.transformer == null ? false : widget.transformer.reverse, viewportFraction: widget.viewportFraction); } super.initState(); } bool _isPageViewLayout() { return widget.layout == null || widget.layout == SwiperLayout.DEFAULT; } @override void didChangeDependencies() { super.didChangeDependencies(); } bool _getReverse(Swiper widget) => widget.transformer == null ? false : widget.transformer.reverse; @override void didUpdateWidget(Swiper oldWidget) { super.didUpdateWidget(oldWidget); if (_isPageViewLayout()) { if (_pageController == null || (widget.index != oldWidget.index || widget.loop != oldWidget.loop || widget.itemCount != oldWidget.itemCount || widget.viewportFraction != oldWidget.viewportFraction || _getReverse(widget) != _getReverse(oldWidget))) { _pageController = TransformerPageController( initialPage: widget.index, loop: widget.loop, itemCount: widget.itemCount, reverse: _getReverse(widget), viewportFraction: widget.viewportFraction); } } else { scheduleMicrotask(() { // So that we have a chance to do `removeListener` in child widgets. if (_pageController != null) { _pageController.dispose(); _pageController = null; } }); } if (widget.index != null && widget.index != _activeIndex) { _activeIndex = widget.index; } } void _onIndexChanged(int index) { setState(() { _activeIndex = index; }); if (widget.onIndexChanged != null) { widget.onIndexChanged(index); } } Widget _buildSwiper() { IndexedWidgetBuilder itemBuilder; if (widget.onTap != null) { itemBuilder = _wrapTap; } else { itemBuilder = widget.itemBuilder; } if (widget.layout == SwiperLayout.STACK) { return _StackSwiper( loop: widget.loop, itemWidth: widget.itemWidth, itemHeight: widget.itemHeight, itemCount: widget.itemCount, itemBuilder: itemBuilder, index: _activeIndex, curve: widget.curve, duration: widget.duration, onIndexChanged: _onIndexChanged, controller: _controller, scrollDirection: widget.scrollDirection, ); } else if (_isPageViewLayout()) { PageTransformer transformer = widget.transformer; if (widget.scale != null || widget.fade != null) { transformer = ScaleAndFadeTransformer(scale: widget.scale, fade: widget.fade); } Widget child = TransformerPageView( pageController: _pageController, loop: widget.loop, itemCount: widget.itemCount, itemBuilder: itemBuilder, transformer: transformer, viewportFraction: widget.viewportFraction, index: _activeIndex, duration: Duration(milliseconds: widget.duration), scrollDirection: widget.scrollDirection, onPageChanged: _onIndexChanged, curve: widget.curve, physics: widget.physics, controller: _controller, ); if (widget.autoplayDisableOnInteraction && widget.autoplay) { return NotificationListener( child: child, onNotification: (dynamic notification) { if (notification is ScrollStartNotification) { if (notification.dragDetails != null) { //by human if (_timer != null) _stopAutoplay(); } } else if (notification is ScrollEndNotification) { if (_timer == null) _startAutoplay(); } return false; }, ); } return child; } else if (widget.layout == SwiperLayout.TINDER) { return _TinderSwiper( loop: widget.loop, itemWidth: widget.itemWidth, itemHeight: widget.itemHeight, itemCount: widget.itemCount, itemBuilder: itemBuilder, index: _activeIndex, curve: widget.curve, duration: widget.duration, onIndexChanged: _onIndexChanged, controller: _controller, scrollDirection: widget.scrollDirection, ); } else if (widget.layout == SwiperLayout.CUSTOM) { return _CustomLayoutSwiper( loop: widget.loop, option: widget.customLayoutOption, itemWidth: widget.itemWidth, itemHeight: widget.itemHeight, itemCount: widget.itemCount, itemBuilder: itemBuilder, index: _activeIndex, curve: widget.curve, duration: widget.duration, onIndexChanged: _onIndexChanged, controller: _controller, scrollDirection: widget.scrollDirection, ); } else { return Container(); } } SwiperPluginConfig _ensureConfig(SwiperPluginConfig config) { config ??= SwiperPluginConfig( outer: widget.outer, itemCount: widget.itemCount, layout: widget.layout, indicatorLayout: widget.indicatorLayout, pageController: _pageController, activeIndex: _activeIndex, scrollDirection: widget.scrollDirection, controller: _controller, loop: widget.loop); return config; } List _ensureListForStack( Widget swiper, List listForStack, Widget widget) { if (listForStack == null) { listForStack = [swiper, widget]; } else { listForStack.add(widget); } return listForStack; } @override Widget build(BuildContext context) { Widget swiper = _buildSwiper(); List listForStack; SwiperPluginConfig config; if (widget.control != null) { //Stack config = _ensureConfig(config); listForStack = _ensureListForStack( swiper, listForStack, widget.control.build(context, config)); } if (widget.plugins != null) { config = _ensureConfig(config); for (SwiperPlugin plugin in widget.plugins) { listForStack = _ensureListForStack( swiper, listForStack, plugin.build(context, config)); } } if (widget.pagination != null) { config = _ensureConfig(config); if (widget.outer) { return _buildOuterPagination( widget.pagination as SwiperPagination, listForStack == null ? swiper : Stack(children: listForStack), config); } else { listForStack = _ensureListForStack( swiper, listForStack, widget.pagination.build(context, config)); } } if (listForStack != null) { return Stack( children: listForStack, ); } return swiper; } Widget _buildOuterPagination( SwiperPagination pagination, Widget swiper, SwiperPluginConfig config) { List list = []; //Only support bottom yet! if (widget.containerHeight != null || widget.containerWidth != null) { list.add(swiper); } else { list.add(Expanded(child: swiper)); } list.add(Align( alignment: Alignment.center, child: pagination.build(context, config), )); return Column( children: list, crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, ); } } abstract class _SubSwiper extends StatefulWidget { final IndexedWidgetBuilder itemBuilder; final int itemCount; final int index; final ValueChanged onIndexChanged; final SwiperController controller; final int duration; final Curve curve; final double itemWidth; final double itemHeight; final bool loop; final Axis scrollDirection; const _SubSwiper( {Key key, this.loop, this.itemHeight, this.itemWidth, this.duration, this.curve, this.itemBuilder, this.controller, this.index, this.itemCount, this.scrollDirection = Axis.horizontal, this.onIndexChanged}) : super(key: key); int getCorrectIndex(int indexNeedsFix) { if (itemCount == 0) return 0; int value = indexNeedsFix % itemCount; if (value < 0) { value += itemCount; } return value; } } class _TinderSwiper extends _SubSwiper { const _TinderSwiper({ Key key, Curve curve, int duration, SwiperController controller, ValueChanged onIndexChanged, @required double itemHeight, @required double itemWidth, IndexedWidgetBuilder itemBuilder, int index, bool loop, int itemCount, Axis scrollDirection, }) : assert(itemWidth != null && itemHeight != null), super( loop: loop, key: key, itemWidth: itemWidth, itemHeight: itemHeight, itemBuilder: itemBuilder, curve: curve, duration: duration, controller: controller, index: index, onIndexChanged: onIndexChanged, itemCount: itemCount, scrollDirection: scrollDirection); @override State createState() { return _TinderState(); } } class _StackSwiper extends _SubSwiper { const _StackSwiper({ Key key, Curve curve, int duration, SwiperController controller, ValueChanged onIndexChanged, double itemHeight, double itemWidth, IndexedWidgetBuilder itemBuilder, int index, bool loop, int itemCount, Axis scrollDirection, }) : super( loop: loop, key: key, itemWidth: itemWidth, itemHeight: itemHeight, itemBuilder: itemBuilder, curve: curve, duration: duration, controller: controller, index: index, onIndexChanged: onIndexChanged, itemCount: itemCount, scrollDirection: scrollDirection); @override State createState() { return _StackViewState(); } } class _TinderState extends _CustomLayoutStateBase<_TinderSwiper> { List scales; List offsetsX; List offsetsY; List opacity; List rotates; double getOffsetY(double scale) { return widget.itemHeight - widget.itemHeight * scale; } @override void didChangeDependencies() { super.didChangeDependencies(); } @override void didUpdateWidget(_TinderSwiper oldWidget) { _updateValues(); super.didUpdateWidget(oldWidget); } @override void afterRender() { super.afterRender(); _startIndex = -3; _animationCount = 5; opacity = [0.0, 0.9, 0.9, 1.0, 0.0, 0.0]; scales = [0.80, 0.80, 0.85, 0.90, 1.0, 1.0, 1.0]; rotates = [0.0, 0.0, 0.0, 0.0, 20.0, 25.0]; _updateValues(); } void _updateValues() { if (widget.scrollDirection == Axis.horizontal) { offsetsX = [0.0, 0.0, 0.0, 0.0, _swiperWidth, _swiperWidth]; offsetsY = [ 0.0, 0.0, -5.0, -10.0, -15.0, -20.0, ]; } else { offsetsX = [ 0.0, 0.0, 5.0, 10.0, 15.0, 20.0, ]; offsetsY = [0.0, 0.0, 0.0, 0.0, _swiperHeight, _swiperHeight]; } } @override Widget _buildItem(int i, int realIndex, double animationValue) { double s = _getValue(scales, animationValue, i); double f = _getValue(offsetsX, animationValue, i); double fy = _getValue(offsetsY, animationValue, i); double o = _getValue(opacity, animationValue, i); double a = _getValue(rotates, animationValue, i); Alignment alignment = widget.scrollDirection == Axis.horizontal ? Alignment.bottomCenter : Alignment.centerLeft; return Opacity( opacity: o, child: Transform.rotate( angle: a / 180.0, child: Transform.translate( key: ValueKey(_currentIndex + i), offset: Offset(f, fy), child: Transform.scale( scale: s, alignment: alignment, child: SizedBox( width: widget.itemWidth ?? double.infinity, height: widget.itemHeight ?? double.infinity, child: widget.itemBuilder(context, realIndex), ), ), ), ), ); } } class _StackViewState extends _CustomLayoutStateBase<_StackSwiper> { List scales; List offsets; List opacity; @override void didChangeDependencies() { super.didChangeDependencies(); } void _updateValues() { if (widget.scrollDirection == Axis.horizontal) { double space = (_swiperWidth - widget.itemWidth) / 2; offsets = [-space, -space / 3 * 2, -space / 3, 0.0, _swiperWidth]; } else { double space = (_swiperHeight - widget.itemHeight) / 2; offsets = [-space, -space / 3 * 2, -space / 3, 0.0, _swiperHeight]; } } @override void didUpdateWidget(_StackSwiper oldWidget) { _updateValues(); super.didUpdateWidget(oldWidget); } @override void afterRender() { super.afterRender(); //length of the values array below _animationCount = 5; //Array below this line, '0' index is 1.0 ,witch is the first item show in swiper. _startIndex = -3; scales = [0.7, 0.8, 0.9, 1.0, 1.0]; opacity = [0.0, 0.5, 1.0, 1.0, 1.0]; _updateValues(); } @override Widget _buildItem(int i, int realIndex, double animationValue) { double s = _getValue(scales, animationValue, i); double f = _getValue(offsets, animationValue, i); double o = _getValue(opacity, animationValue, i); Offset offset = widget.scrollDirection == Axis.horizontal ? Offset(f, 0.0) : Offset(0.0, f); Alignment alignment = widget.scrollDirection == Axis.horizontal ? Alignment.centerLeft : Alignment.topCenter; return Opacity( opacity: o, child: Transform.translate( key: ValueKey(_currentIndex + i), offset: offset, child: Transform.scale( scale: s, alignment: alignment, child: SizedBox( width: widget.itemWidth ?? double.infinity, height: widget.itemHeight ?? double.infinity, child: widget.itemBuilder(context, realIndex), ), ), ), ); } } class ScaleAndFadeTransformer extends PageTransformer { final double _scale; final double _fade; ScaleAndFadeTransformer({double fade = 0.3, double scale = 0.8}) : _fade = fade, _scale = scale; @override // ignore: avoid_renaming_method_parameters Widget transform(Widget item, TransformInfo info) { double position = info.position; Widget child = item; if (_scale != null) { double scaleFactor = (1 - position.abs()) * (1 - _scale); double scale = _scale + scaleFactor; child = Transform.scale( scale: scale, child: item, ); } if (_fade != null) { double fadeFactor = (1 - position.abs()) * (1 - _fade); double opacity = _fade + fadeFactor; child = Opacity( opacity: opacity, child: child, ); } return child; } } class SwiperControl extends SwiperPlugin { ///IconData for previous final IconData iconPrevious; ///iconData fopr next final IconData iconNext; ///icon size final double size; ///Icon normal color, The theme's [ThemeData.primaryColor] by default. final Color color; ///if set loop=false on Swiper, this color will be used when swiper goto the last slide. ///The theme's [ThemeData.disabledColor] by default. final Color disableColor; final EdgeInsetsGeometry padding; final Key key; const SwiperControl( {this.iconPrevious = Icons.arrow_back_ios, this.iconNext = Icons.arrow_forward_ios, this.color, this.disableColor, this.key, this.size = 30.0, this.padding = const EdgeInsets.all(5.0)}); Widget buildButton(SwiperPluginConfig config, Color color, IconData iconDaga, int quarterTurns, bool previous) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (previous) { config.controller.previous(animation: true); } else { config.controller.next(animation: true); } }, child: Padding( padding: padding, child: RotatedBox( quarterTurns: quarterTurns, child: Icon( iconDaga, semanticLabel: previous ? "Previous" : "Next", size: size, color: color, ))), ); } @override Widget build(BuildContext context, SwiperPluginConfig config) { ThemeData themeData = Theme.of(context); Color color = this.color ?? themeData.primaryColor; Color disableColor = this.disableColor ?? themeData.disabledColor; Color prevColor; Color nextColor; if (config.loop) { prevColor = nextColor = color; } else { bool next = config.activeIndex < config.itemCount - 1; bool prev = config.activeIndex > 0; prevColor = prev ? color : disableColor; nextColor = next ? color : disableColor; } Widget child; if (config.scrollDirection == Axis.horizontal) { child = Row( key: key, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ buildButton(config, prevColor, iconPrevious, 0, true), buildButton(config, nextColor, iconNext, 0, false) ], ); } else { child = Column( key: key, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ buildButton(config, prevColor, iconPrevious, -3, true), buildButton(config, nextColor, iconNext, -3, false) ], ); } return SizedBox( height: double.infinity, child: child, width: double.infinity, ); } } class SwiperController extends IndexController { // Autoplay is started static const int START_AUTOPLAY = 2; // Autoplay is stopped. static const int STOP_AUTOPLAY = 3; // Indicate that the user is swiping static const int SWIPE = 4; // Indicate that the `Swiper` has changed it's index and is building it's ui ,so that the // `SwiperPluginConfig` is available. static const int BUILD = 5; // available when `event` == SwiperController.BUILD SwiperPluginConfig config; // available when `event` == SwiperController.SWIPE // this value is PageViewController.pos double pos; // ignore: overridden_fields, annotate_overrides int index; // ignore: overridden_fields, annotate_overrides bool animation; bool autoplay; SwiperController(); void startAutoplay() { event = SwiperController.START_AUTOPLAY; autoplay = true; notifyListeners(); } void stopAutoplay() { event = SwiperController.STOP_AUTOPLAY; autoplay = false; notifyListeners(); } } class FractionPaginationBuilder extends SwiperPlugin { ///color ,if set null , will be Theme.of(context).scaffoldBackgroundColor final Color color; ///color when active,if set null , will be Theme.of(context).primaryColor final Color activeColor; ////font size final double fontSize; ///font size when active final double activeFontSize; final Key key; const FractionPaginationBuilder( {this.color, this.fontSize = 20.0, this.key, this.activeColor, this.activeFontSize = 35.0}); @override Widget build(BuildContext context, SwiperPluginConfig config) { ThemeData themeData = Theme.of(context); Color activeColor = this.activeColor ?? themeData.primaryColor; Color color = this.color ?? themeData.scaffoldBackgroundColor; if (Axis.vertical == config.scrollDirection) { return Column( key: key, mainAxisSize: MainAxisSize.min, children: [ Text( "${config.activeIndex + 1}", style: TextStyle(color: activeColor, fontSize: activeFontSize), ), Text( "/", style: TextStyle(color: color, fontSize: fontSize), ), Text( "${config.itemCount}", style: TextStyle(color: color, fontSize: fontSize), ) ], ); } else { return Row( key: key, mainAxisSize: MainAxisSize.min, children: [ Text( "${config.activeIndex + 1}", style: TextStyle(color: activeColor, fontSize: activeFontSize), ), Text( " / ${config.itemCount}", style: TextStyle(color: color, fontSize: fontSize), ) ], ); } } } class RectSwiperPaginationBuilder extends SwiperPlugin { ///color when current index,if set null , will be Theme.of(context).primaryColor final Color activeColor; ///,if set null , will be Theme.of(context).scaffoldBackgroundColor final Color color; ///Size of the rect when activate final Size activeSize; ///Size of the rect final Size size; /// Space between rects final double space; final Key key; const RectSwiperPaginationBuilder( {this.activeColor, this.color, this.key, this.size = const Size(10.0, 2.0), this.activeSize = const Size(10.0, 2.0), this.space = 3.0}); @override Widget build(BuildContext context, SwiperPluginConfig config) { ThemeData themeData = Theme.of(context); Color activeColor = this.activeColor ?? themeData.primaryColor; Color color = this.color ?? themeData.scaffoldBackgroundColor; List list = []; if (config.itemCount > 20) { // ignore: avoid_print print( "The itemCount is too big, we suggest use FractionPaginationBuilder instead of DotSwiperPaginationBuilder in this sitituation"); } int itemCount = config.itemCount; int activeIndex = config.activeIndex; for (int i = 0; i < itemCount; ++i) { bool active = i == activeIndex; Size size = active ? activeSize : this.size; list.add(SizedBox( width: size.width, height: size.height, child: Container( color: active ? activeColor : color, key: Key("pagination_$i"), margin: EdgeInsets.all(space), ), )); } if (config.scrollDirection == Axis.vertical) { return Column( key: key, mainAxisSize: MainAxisSize.min, children: list, ); } else { return Row( key: key, mainAxisSize: MainAxisSize.min, children: list, ); } } } class DotSwiperPaginationBuilder extends SwiperPlugin { ///color when current index,if set null , will be Theme.of(context).primaryColor final Color activeColor; ///,if set null , will be Theme.of(context).scaffoldBackgroundColor final Color color; ///Size of the dot when activate final double activeSize; ///Size of the dot final double size; /// Space between dots final double space; final Key key; const DotSwiperPaginationBuilder( {this.activeColor, this.color, this.key, this.size = 10.0, this.activeSize = 10.0, this.space = 3.0}); @override Widget build(BuildContext context, SwiperPluginConfig config) { if (config.itemCount > 20) { // ignore: avoid_print print( "The itemCount is too big, we suggest use FractionPaginationBuilder instead of DotSwiperPaginationBuilder in this sitituation"); } Color activeColor = this.activeColor; Color color = this.color; if (activeColor == null || color == null) { ThemeData themeData = Theme.of(context); activeColor = this.activeColor ?? themeData.primaryColor; color = this.color ?? themeData.scaffoldBackgroundColor; } if (config.indicatorLayout != PageIndicatorLayout.NONE && config.layout == SwiperLayout.DEFAULT) { return PageIndicator( count: config.itemCount, controller: config.pageController, layout: config.indicatorLayout, size: size, activeColor: activeColor, color: color, space: space, ); } List list = []; int itemCount = config.itemCount; int activeIndex = config.activeIndex; for (int i = 0; i < itemCount; ++i) { bool active = i == activeIndex; list.add(Container( key: Key("pagination_$i"), margin: EdgeInsets.all(space), child: ClipOval( child: Container( color: active ? activeColor : color, width: active ? activeSize : size, height: active ? activeSize : size, ), ), )); } if (config.scrollDirection == Axis.vertical) { return Column( key: key, mainAxisSize: MainAxisSize.min, children: list, ); } else { return Row( key: key, mainAxisSize: MainAxisSize.min, children: list, ); } } } typedef SwiperPaginationBuilder = Widget Function( BuildContext context, SwiperPluginConfig config); class SwiperCustomPagination extends SwiperPlugin { final SwiperPaginationBuilder builder; SwiperCustomPagination({@required this.builder}) : assert(builder != null); @override Widget build(BuildContext context, SwiperPluginConfig config) { return builder(context, config); } } class SwiperPagination extends SwiperPlugin { /// dot style pagination static const SwiperPlugin dots = DotSwiperPaginationBuilder(); /// fraction style pagination static const SwiperPlugin fraction = FractionPaginationBuilder(); static const SwiperPlugin rect = RectSwiperPaginationBuilder(); /// Alignment.bottomCenter by default when scrollDirection== Axis.horizontal /// Alignment.centerRight by default when scrollDirection== Axis.vertical final Alignment alignment; /// Distance between pagination and the container final EdgeInsetsGeometry margin; /// Build the widet final SwiperPlugin builder; final Key key; const SwiperPagination( {this.alignment, this.key, this.margin = const EdgeInsets.all(10.0), this.builder = SwiperPagination.dots}); @override Widget build(BuildContext context, SwiperPluginConfig config) { Alignment alignment = this.alignment ?? (config.scrollDirection == Axis.horizontal ? Alignment.bottomCenter : Alignment.centerRight); Widget child = Container( margin: margin, child: builder.build(context, config), ); if (!config.outer) { child = Align( key: key, alignment: alignment, child: child, ); } return child; } } /// plugin to display swiper components /// abstract class SwiperPlugin { const SwiperPlugin(); Widget build(BuildContext context, SwiperPluginConfig config); } class SwiperPluginConfig { final int activeIndex; final int itemCount; final PageIndicatorLayout indicatorLayout; final Axis scrollDirection; final bool loop; final bool outer; final PageController pageController; final SwiperController controller; final SwiperLayout layout; const SwiperPluginConfig( {this.activeIndex, this.itemCount, this.indicatorLayout, this.outer, @required this.scrollDirection, @required this.controller, this.pageController, this.layout, this.loop}) : assert(scrollDirection != null), assert(controller != null); } class SwiperPluginView extends StatelessWidget { final SwiperPlugin plugin; final SwiperPluginConfig config; const SwiperPluginView(this.plugin, this.config); @override Widget build(BuildContext context) { return plugin.build(context, config); } } abstract class _CustomLayoutStateBase extends State with SingleTickerProviderStateMixin { double _swiperWidth; double _swiperHeight; Animation _animation; AnimationController _animationController; int _startIndex; int _animationCount; @override void initState() { if (widget.itemWidth == null) { throw Exception( "==============\n\nwidget.itemWith must not be null when use stack layout.\n========\n"); } _createAnimationController(); widget.controller.addListener(_onController); super.initState(); } void _createAnimationController() { _animationController = AnimationController(vsync: this, value: 0.5); Tween tween = Tween(begin: 0.0, end: 1.0); _animation = tween.animate(_animationController); } @override void didChangeDependencies() { WidgetsBinding.instance.addPostFrameCallback(_getSize); super.didChangeDependencies(); } void _getSize(dynamic _) { afterRender(); } @mustCallSuper void afterRender() { RenderObject renderObject = context.findRenderObject(); Size size = renderObject.paintBounds.size; _swiperWidth = size.width; _swiperHeight = size.height; setState(() {}); } @override void didUpdateWidget(T oldWidget) { if (widget.controller != oldWidget.controller) { oldWidget.controller.removeListener(_onController); widget.controller.addListener(_onController); } if (widget.loop != oldWidget.loop) { if (!widget.loop) { _currentIndex = _ensureIndex(_currentIndex); } } super.didUpdateWidget(oldWidget); } int _ensureIndex(int index) { index = index % widget.itemCount; if (index < 0) { index += widget.itemCount; } return index; } @override void dispose() { widget.controller.removeListener(_onController); _animationController?.dispose(); super.dispose(); } Widget _buildItem(int i, int realIndex, double animationValue); Widget _buildContainer(List list) { return Stack( children: list, ); } Widget _buildAnimation(BuildContext context, Widget w) { List list = []; double animationValue = _animation.value; for (int i = 0; i < _animationCount; ++i) { int realIndex = _currentIndex + i + _startIndex; realIndex = realIndex % widget.itemCount; if (realIndex < 0) { realIndex += widget.itemCount; } list.add(_buildItem(i, realIndex, animationValue)); } return GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: _onPanStart, onPanEnd: _onPanEnd, onPanUpdate: _onPanUpdate, child: ClipRect( child: Center( child: _buildContainer(list), ), ), ); } @override Widget build(BuildContext context) { if (_animationCount == null) { return Container(); } return AnimatedBuilder( animation: _animationController, builder: _buildAnimation); } double _currentValue; double _currentPos; bool _lockScroll = false; Future _move(double position, {int nextIndex}) async { if (_lockScroll) return; try { _lockScroll = true; await _animationController.animateTo(position, duration: Duration(milliseconds: widget.duration), curve: widget.curve); if (nextIndex != null) { widget.onIndexChanged(widget.getCorrectIndex(nextIndex)); } } catch (e) { // ignore: avoid_print print(e); } finally { if (nextIndex != null) { try { _animationController.value = 0.5; } catch (e) { // ignore: avoid_print print(e); } _currentIndex = nextIndex; } _lockScroll = false; } } int _nextIndex() { int index = _currentIndex + 1; if (!widget.loop && index >= widget.itemCount - 1) { return widget.itemCount - 1; } return index; } int _prevIndex() { int index = _currentIndex - 1; if (!widget.loop && index < 0) { return 0; } return index; } void _onController() { switch (widget.controller.event) { case IndexController.PREVIOUS: int prevIndex = _prevIndex(); if (prevIndex == _currentIndex) return; _move(1.0, nextIndex: prevIndex); break; case IndexController.NEXT: int nextIndex = _nextIndex(); if (nextIndex == _currentIndex) return; _move(0.0, nextIndex: nextIndex); break; case IndexController.MOVE: throw Exception( "Custom layout does not support SwiperControllerEvent.MOVE_INDEX yet!"); case SwiperController.STOP_AUTOPLAY: case SwiperController.START_AUTOPLAY: break; } } void _onPanEnd(DragEndDetails details) { if (_lockScroll) return; double velocity = widget.scrollDirection == Axis.horizontal ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy; if (_animationController.value >= 0.75 || velocity > 500.0) { if (_currentIndex <= 0 && !widget.loop) { return; } _move(1.0, nextIndex: _currentIndex - 1); } else if (_animationController.value < 0.25 || velocity < -500.0) { if (_currentIndex >= widget.itemCount - 1 && !widget.loop) { return; } _move(0.0, nextIndex: _currentIndex + 1); } else { _move(0.5); } } void _onPanStart(DragStartDetails details) { if (_lockScroll) return; _currentValue = _animationController.value; _currentPos = widget.scrollDirection == Axis.horizontal ? details.globalPosition.dx : details.globalPosition.dy; } void _onPanUpdate(DragUpdateDetails details) { if (_lockScroll) return; double value = _currentValue + ((widget.scrollDirection == Axis.horizontal ? details.globalPosition.dx : details.globalPosition.dy) - _currentPos) / _swiperWidth / 2; // no loop ? if (!widget.loop) { if (_currentIndex >= widget.itemCount - 1) { if (value < 0.5) { value = 0.5; } } else if (_currentIndex <= 0) { if (value > 0.5) { value = 0.5; } } } _animationController.value = value; } int _currentIndex = 0; } double _getValue(List values, double animationValue, int index) { double s = values[index]; if (animationValue >= 0.5) { if (index < values.length - 1) { s = s + (values[index + 1] - s) * (animationValue - 0.5) * 2.0; } } else { if (index != 0) { s = s - (s - values[index - 1]) * (0.5 - animationValue) * 2.0; } } return s; } Offset _getOffsetValue(List values, double animationValue, int index) { Offset s = values[index]; double dx = s.dx; double dy = s.dy; if (animationValue >= 0.5) { if (index < values.length - 1) { dx = dx + (values[index + 1].dx - dx) * (animationValue - 0.5) * 2.0; dy = dy + (values[index + 1].dy - dy) * (animationValue - 0.5) * 2.0; } } else { if (index != 0) { dx = dx - (dx - values[index - 1].dx) * (0.5 - animationValue) * 2.0; dy = dy - (dy - values[index - 1].dy) * (0.5 - animationValue) * 2.0; } } return Offset(dx, dy); } abstract class TransformBuilder { List values; TransformBuilder({this.values}); Widget build(int i, double animationValue, Widget widget); } class ScaleTransformBuilder extends TransformBuilder { final Alignment alignment; ScaleTransformBuilder( {List values, this.alignment = Alignment.center}) : super(values: values); @override Widget build(int i, double animationValue, Widget widget) { double s = _getValue(values, animationValue, i); return Transform.scale(scale: s, child: widget); } } class OpacityTransformBuilder extends TransformBuilder { OpacityTransformBuilder({List values}) : super(values: values); @override Widget build(int i, double animationValue, Widget widget) { double v = _getValue(values, animationValue, i); return Opacity( opacity: v, child: widget, ); } } class RotateTransformBuilder extends TransformBuilder { RotateTransformBuilder({List values}) : super(values: values); @override Widget build(int i, double animationValue, Widget widget) { double v = _getValue(values, animationValue, i); return Transform.rotate( angle: v, child: widget, ); } } class TranslateTransformBuilder extends TransformBuilder { TranslateTransformBuilder({List values}) : super(values: values); @override Widget build(int i, double animationValue, Widget widget) { Offset s = _getOffsetValue(values, animationValue, i); return Transform.translate( offset: s, child: widget, ); } } class CustomLayoutOption { final List builders = []; final int startIndex; final int stateCount; CustomLayoutOption({this.stateCount, @required this.startIndex}) : assert(startIndex != null, stateCount != null); CustomLayoutOption addOpacity(List values) { builders.add(OpacityTransformBuilder(values: values)); return this; } CustomLayoutOption addTranslate(List values) { builders.add(TranslateTransformBuilder(values: values)); return this; } CustomLayoutOption addScale(List values, Alignment alignment) { builders.add(ScaleTransformBuilder(values: values, alignment: alignment)); return this; } CustomLayoutOption addRotate(List values) { builders.add(RotateTransformBuilder(values: values)); return this; } } class _CustomLayoutSwiper extends _SubSwiper { final CustomLayoutOption option; const _CustomLayoutSwiper( {@required this.option, double itemWidth, bool loop, double itemHeight, ValueChanged onIndexChanged, Key key, IndexedWidgetBuilder itemBuilder, Curve curve, int duration, int index, int itemCount, Axis scrollDirection, SwiperController controller}) : assert(option != null), super( loop: loop, onIndexChanged: onIndexChanged, itemWidth: itemWidth, itemHeight: itemHeight, key: key, itemBuilder: itemBuilder, curve: curve, duration: duration, index: index, itemCount: itemCount, controller: controller, scrollDirection: scrollDirection); @override State createState() { return _CustomLayoutState(); } } class _CustomLayoutState extends _CustomLayoutStateBase<_CustomLayoutSwiper> { @override void didChangeDependencies() { super.didChangeDependencies(); _startIndex = widget.option.startIndex; _animationCount = widget.option.stateCount; } @override void didUpdateWidget(_CustomLayoutSwiper oldWidget) { _startIndex = widget.option.startIndex; _animationCount = widget.option.stateCount; super.didUpdateWidget(oldWidget); } @override Widget _buildItem(int index, int realIndex, double animationValue) { List builders = widget.option.builders; Widget child = SizedBox( width: widget.itemWidth ?? double.infinity, height: widget.itemHeight ?? double.infinity, child: widget.itemBuilder(context, realIndex)); for (int i = builders.length - 1; i >= 0; --i) { TransformBuilder builder = builders[i]; child = builder.build(index, animationValue, child); } return child; } }