mirror of https://github.com/flutter/samples.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1871 lines
50 KiB
1871 lines
50 KiB
// Package flutter_swiper:
|
|
// https://pub.dartlang.org/packages/flutter_swiper
|
|
|
|
import 'package:flutter_web/material.dart';
|
|
import 'package:flutter_web/foundation.dart';
|
|
|
|
import 'dart:async';
|
|
|
|
import 'flutter_page_indicator.dart';
|
|
import 'transformer_page_view.dart';
|
|
|
|
typedef void SwiperOnTap(int index);
|
|
|
|
typedef Widget SwiperDataBuilder(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;
|
|
|
|
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<int> 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<SwiperPlugin> 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;
|
|
|
|
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({
|
|
List<Widget> children,
|
|
bool autoplay: false,
|
|
PageTransformer transformer,
|
|
int autoplayDelay: kDefaultAutoplayDelayMs,
|
|
bool reverse: false,
|
|
bool autoplayDisableOnInteraction: true,
|
|
int duration: kDefaultAutoplayTransactionDuration,
|
|
ValueChanged<int> onIndexChanged,
|
|
int index,
|
|
SwiperOnTap onTap,
|
|
bool loop: true,
|
|
Curve curve: Curves.ease,
|
|
Axis scrollDirection: Axis.horizontal,
|
|
SwiperPlugin pagination,
|
|
SwiperPlugin control,
|
|
List<SwiperPlugin> 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 new 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: (BuildContext context, int 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<int> onIndexChanged,
|
|
int index,
|
|
SwiperOnTap onTap,
|
|
bool loop: true,
|
|
Curve curve: Curves.ease,
|
|
Axis scrollDirection: Axis.horizontal,
|
|
SwiperPlugin pagination,
|
|
SwiperPlugin control,
|
|
List<SwiperPlugin> 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 new 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: (BuildContext context, int index) {
|
|
return builder(context, list[index], index);
|
|
},
|
|
itemCount: list.length);
|
|
}
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return new _SwiperState();
|
|
}
|
|
}
|
|
|
|
abstract class _SwiperTimerMixin extends State<Swiper> {
|
|
Timer _timer;
|
|
|
|
SwiperController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
_controller = widget.controller;
|
|
if (_controller == null) {
|
|
_controller = new 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 new GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
this.widget.onTap(index);
|
|
},
|
|
child: widget.itemBuilder(context, index),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_activeIndex = widget.index ?? 0;
|
|
if (_isPageViewLayout()) {
|
|
_pageController = new 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 = new 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 new _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 =
|
|
new ScaleAndFadeTransformer(scale: widget.scale, fade: widget.fade);
|
|
}
|
|
|
|
Widget child = new TransformerPageView(
|
|
pageController: _pageController,
|
|
loop: widget.loop,
|
|
itemCount: widget.itemCount,
|
|
itemBuilder: itemBuilder,
|
|
transformer: transformer,
|
|
viewportFraction: widget.viewportFraction,
|
|
index: _activeIndex,
|
|
duration: new Duration(milliseconds: widget.duration),
|
|
scrollDirection: widget.scrollDirection,
|
|
onPageChanged: _onIndexChanged,
|
|
curve: widget.curve,
|
|
physics: widget.physics,
|
|
controller: _controller,
|
|
);
|
|
if (widget.autoplayDisableOnInteraction && widget.autoplay) {
|
|
return new NotificationListener(
|
|
child: child,
|
|
onNotification: (ScrollNotification 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 new _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 new _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 new Container();
|
|
}
|
|
}
|
|
|
|
SwiperPluginConfig _ensureConfig(SwiperPluginConfig config) {
|
|
if (config == null) {
|
|
config = new 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<Widget> _ensureListForStack(
|
|
Widget swiper, List<Widget> listForStack, Widget widget) {
|
|
if (listForStack == null) {
|
|
listForStack = [swiper, widget];
|
|
} else {
|
|
listForStack.add(widget);
|
|
}
|
|
return listForStack;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget swiper = _buildSwiper();
|
|
List<Widget> 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,
|
|
listForStack == null ? swiper : new Stack(children: listForStack),
|
|
config);
|
|
} else {
|
|
listForStack = _ensureListForStack(
|
|
swiper, listForStack, widget.pagination.build(context, config));
|
|
}
|
|
}
|
|
|
|
if (listForStack != null) {
|
|
return new Stack(
|
|
children: listForStack,
|
|
);
|
|
}
|
|
|
|
return swiper;
|
|
}
|
|
|
|
Widget _buildOuterPagination(
|
|
SwiperPagination pagination, Widget swiper, SwiperPluginConfig config) {
|
|
List<Widget> list = [];
|
|
//Only support bottom yet!
|
|
if (widget.containerHeight != null || widget.containerWidth != null) {
|
|
list.add(swiper);
|
|
} else {
|
|
list.add(new Expanded(child: swiper));
|
|
}
|
|
|
|
list.add(new Align(
|
|
alignment: Alignment.center,
|
|
child: pagination.build(context, config),
|
|
));
|
|
|
|
return new 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<int> onIndexChanged;
|
|
final SwiperController controller;
|
|
final int duration;
|
|
final Curve curve;
|
|
final double itemWidth;
|
|
final double itemHeight;
|
|
final bool loop;
|
|
final Axis scrollDirection;
|
|
|
|
_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);
|
|
|
|
@override
|
|
State<StatefulWidget> createState();
|
|
|
|
int getCorrectIndex(int indexNeedsFix) {
|
|
if (itemCount == 0) return 0;
|
|
int value = indexNeedsFix % itemCount;
|
|
if (value < 0) {
|
|
value += itemCount;
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
class _TinderSwiper extends _SubSwiper {
|
|
_TinderSwiper({
|
|
Key key,
|
|
Curve curve,
|
|
int duration,
|
|
SwiperController controller,
|
|
ValueChanged<int> onIndexChanged,
|
|
double itemHeight,
|
|
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<StatefulWidget> createState() {
|
|
return new _TinderState();
|
|
}
|
|
}
|
|
|
|
class _StackSwiper extends _SubSwiper {
|
|
_StackSwiper({
|
|
Key key,
|
|
Curve curve,
|
|
int duration,
|
|
SwiperController controller,
|
|
ValueChanged<int> 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<StatefulWidget> createState() {
|
|
return new _StackViewState();
|
|
}
|
|
}
|
|
|
|
class _TinderState extends _CustomLayoutStateBase<_TinderSwiper> {
|
|
List<double> scales;
|
|
List<double> offsetsX;
|
|
List<double> offsetsY;
|
|
List<double> opacity;
|
|
List<double> 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 new Opacity(
|
|
opacity: o,
|
|
child: new Transform.rotate(
|
|
angle: a / 180.0,
|
|
child: new Transform.translate(
|
|
key: new ValueKey<int>(_currentIndex + i),
|
|
offset: new Offset(f, fy),
|
|
child: new Transform.scale(
|
|
scale: s,
|
|
alignment: alignment,
|
|
child: new SizedBox(
|
|
width: widget.itemWidth ?? double.infinity,
|
|
height: widget.itemHeight ?? double.infinity,
|
|
child: widget.itemBuilder(context, realIndex),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _StackViewState extends _CustomLayoutStateBase<_StackSwiper> {
|
|
List<double> scales;
|
|
List<double> offsets;
|
|
List<double> 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
|
|
? new Offset(f, 0.0)
|
|
: new Offset(0.0, f);
|
|
|
|
Alignment alignment = widget.scrollDirection == Axis.horizontal
|
|
? Alignment.centerLeft
|
|
: Alignment.topCenter;
|
|
|
|
return new Opacity(
|
|
opacity: o,
|
|
child: new Transform.translate(
|
|
key: new ValueKey<int>(_currentIndex + i),
|
|
offset: offset,
|
|
child: new Transform.scale(
|
|
scale: s,
|
|
alignment: alignment,
|
|
child: new 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
|
|
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 = new Transform.scale(
|
|
scale: scale,
|
|
child: item,
|
|
);
|
|
}
|
|
|
|
if (_fade != null) {
|
|
double fadeFactor = (1 - position.abs()) * (1 - _fade);
|
|
double opacity = _fade + fadeFactor;
|
|
child = new 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 new 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: <Widget>[
|
|
buildButton(config, prevColor, iconPrevious, 0, true),
|
|
buildButton(config, nextColor, iconNext, 0, false)
|
|
],
|
|
);
|
|
} else {
|
|
child = Column(
|
|
key: key,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
buildButton(config, prevColor, iconPrevious, -3, true),
|
|
buildButton(config, nextColor, iconNext, -3, false)
|
|
],
|
|
);
|
|
}
|
|
|
|
return new Container(
|
|
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;
|
|
|
|
int index;
|
|
bool animation;
|
|
bool autoplay;
|
|
|
|
SwiperController();
|
|
|
|
void startAutoplay() {
|
|
event = SwiperController.START_AUTOPLAY;
|
|
this.autoplay = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
void stopAutoplay() {
|
|
event = SwiperController.STOP_AUTOPLAY;
|
|
this.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 new Column(
|
|
key: key,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
new Text(
|
|
"${config.activeIndex + 1}",
|
|
style: TextStyle(color: activeColor, fontSize: activeFontSize),
|
|
),
|
|
new Text(
|
|
"/",
|
|
style: TextStyle(color: color, fontSize: fontSize),
|
|
),
|
|
new Text(
|
|
"${config.itemCount}",
|
|
style: TextStyle(color: color, fontSize: fontSize),
|
|
)
|
|
],
|
|
);
|
|
} else {
|
|
return new Row(
|
|
key: key,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
new Text(
|
|
"${config.activeIndex + 1}",
|
|
style: TextStyle(color: activeColor, fontSize: activeFontSize),
|
|
),
|
|
new 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<Widget> list = [];
|
|
|
|
if (config.itemCount > 20) {
|
|
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 ? this.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 new Column(
|
|
key: key,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: list,
|
|
);
|
|
} else {
|
|
return new 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) {
|
|
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 new PageIndicator(
|
|
count: config.itemCount,
|
|
controller: config.pageController,
|
|
layout: config.indicatorLayout,
|
|
size: size,
|
|
activeColor: activeColor,
|
|
color: color,
|
|
space: space,
|
|
);
|
|
}
|
|
|
|
List<Widget> 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 new Column(
|
|
key: key,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: list,
|
|
);
|
|
} else {
|
|
return new Row(
|
|
key: key,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: list,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
typedef Widget SwiperPaginationBuilder(
|
|
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 = const DotSwiperPaginationBuilder();
|
|
|
|
/// fraction style pagination
|
|
static const SwiperPlugin fraction = const FractionPaginationBuilder();
|
|
|
|
static const SwiperPlugin rect = const 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});
|
|
|
|
Widget build(BuildContext context, SwiperPluginConfig config) {
|
|
Alignment alignment = this.alignment ??
|
|
(config.scrollDirection == Axis.horizontal
|
|
? Alignment.bottomCenter
|
|
: Alignment.centerRight);
|
|
Widget child = Container(
|
|
margin: margin,
|
|
child: this.builder.build(context, config),
|
|
);
|
|
if (!config.outer) {
|
|
child = new 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,
|
|
this.scrollDirection,
|
|
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<T extends _SubSwiper> extends State<T>
|
|
with SingleTickerProviderStateMixin {
|
|
double _swiperWidth;
|
|
double _swiperHeight;
|
|
Animation<double> _animation;
|
|
AnimationController _animationController;
|
|
int _startIndex;
|
|
int _animationCount;
|
|
|
|
@override
|
|
void initState() {
|
|
if (widget.itemWidth == null) {
|
|
throw new Exception(
|
|
"==============\n\nwidget.itemWith must not be null when use stack layout.\n========\n");
|
|
}
|
|
|
|
_createAnimationController();
|
|
widget.controller.addListener(_onController);
|
|
super.initState();
|
|
}
|
|
|
|
void _createAnimationController() {
|
|
_animationController = new AnimationController(vsync: this, value: 0.5);
|
|
Tween<double> tween = new Tween(begin: 0.0, end: 1.0);
|
|
_animation = tween.animate(_animationController);
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
WidgetsBinding.instance.addPostFrameCallback(_getSize);
|
|
super.didChangeDependencies();
|
|
}
|
|
|
|
void _getSize(_) {
|
|
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<Widget> list) {
|
|
return new Stack(
|
|
children: list,
|
|
);
|
|
}
|
|
|
|
Widget _buildAnimation(BuildContext context, Widget w) {
|
|
List<Widget> 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 new GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onPanStart: _onPanStart,
|
|
onPanEnd: _onPanEnd,
|
|
onPanUpdate: _onPanUpdate,
|
|
child: new ClipRect(
|
|
child: new Center(
|
|
child: _buildContainer(list),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_animationCount == null) {
|
|
return new Container();
|
|
}
|
|
return new AnimatedBuilder(
|
|
animation: _animationController, builder: _buildAnimation);
|
|
}
|
|
|
|
double _currentValue;
|
|
double _currentPos;
|
|
|
|
bool _lockScroll = false;
|
|
|
|
void _move(double position, {int nextIndex}) async {
|
|
if (_lockScroll) return;
|
|
try {
|
|
_lockScroll = true;
|
|
await _animationController.animateTo(position,
|
|
duration: new Duration(milliseconds: widget.duration),
|
|
curve: widget.curve);
|
|
if (nextIndex != null) {
|
|
widget.onIndexChanged(widget.getCorrectIndex(nextIndex));
|
|
}
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
if (nextIndex != null) {
|
|
try {
|
|
_animationController.value = 0.5;
|
|
} catch (e) {
|
|
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 new 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<double> 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<Offset> 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 new Offset(dx, dy);
|
|
}
|
|
|
|
abstract class TransformBuilder<T> {
|
|
List<T> values;
|
|
TransformBuilder({this.values});
|
|
Widget build(int i, double animationValue, Widget widget);
|
|
}
|
|
|
|
class ScaleTransformBuilder extends TransformBuilder<double> {
|
|
final Alignment alignment;
|
|
ScaleTransformBuilder({List<double> values, this.alignment: Alignment.center})
|
|
: super(values: values);
|
|
|
|
Widget build(int i, double animationValue, Widget widget) {
|
|
double s = _getValue(values, animationValue, i);
|
|
return new Transform.scale(scale: s, child: widget);
|
|
}
|
|
}
|
|
|
|
class OpacityTransformBuilder extends TransformBuilder<double> {
|
|
OpacityTransformBuilder({List<double> values}) : super(values: values);
|
|
|
|
Widget build(int i, double animationValue, Widget widget) {
|
|
double v = _getValue(values, animationValue, i);
|
|
return new Opacity(
|
|
opacity: v,
|
|
child: widget,
|
|
);
|
|
}
|
|
}
|
|
|
|
class RotateTransformBuilder extends TransformBuilder<double> {
|
|
RotateTransformBuilder({List<double> values}) : super(values: values);
|
|
|
|
Widget build(int i, double animationValue, Widget widget) {
|
|
double v = _getValue(values, animationValue, i);
|
|
return new Transform.rotate(
|
|
angle: v,
|
|
child: widget,
|
|
);
|
|
}
|
|
}
|
|
|
|
class TranslateTransformBuilder extends TransformBuilder<Offset> {
|
|
TranslateTransformBuilder({List<Offset> values}) : super(values: values);
|
|
|
|
@override
|
|
Widget build(int i, double animationValue, Widget widget) {
|
|
Offset s = _getOffsetValue(values, animationValue, i);
|
|
return new Transform.translate(
|
|
offset: s,
|
|
child: widget,
|
|
);
|
|
}
|
|
}
|
|
|
|
class CustomLayoutOption {
|
|
final List<TransformBuilder> builders = [];
|
|
final int startIndex;
|
|
final int stateCount;
|
|
|
|
CustomLayoutOption({this.stateCount, this.startIndex})
|
|
: assert(startIndex != null, stateCount != null);
|
|
|
|
CustomLayoutOption addOpacity(List<double> values) {
|
|
builders.add(new OpacityTransformBuilder(values: values));
|
|
return this;
|
|
}
|
|
|
|
CustomLayoutOption addTranslate(List<Offset> values) {
|
|
builders.add(new TranslateTransformBuilder(values: values));
|
|
return this;
|
|
}
|
|
|
|
CustomLayoutOption addScale(List<double> values, Alignment alignment) {
|
|
builders
|
|
.add(new ScaleTransformBuilder(values: values, alignment: alignment));
|
|
return this;
|
|
}
|
|
|
|
CustomLayoutOption addRotate(List<double> values) {
|
|
builders.add(new RotateTransformBuilder(values: values));
|
|
return this;
|
|
}
|
|
}
|
|
|
|
class _CustomLayoutSwiper extends _SubSwiper {
|
|
final CustomLayoutOption option;
|
|
|
|
_CustomLayoutSwiper(
|
|
{this.option,
|
|
double itemWidth,
|
|
bool loop,
|
|
double itemHeight,
|
|
ValueChanged<int> 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<StatefulWidget> createState() {
|
|
return new _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<TransformBuilder> builders = widget.option.builders;
|
|
|
|
Widget child = new 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;
|
|
}
|
|
}
|