初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -0,0 +1,456 @@
part of 'swiper.dart';
abstract class _CustomLayoutStateBase<T extends _SubSwiper> extends State<T> with SingleTickerProviderStateMixin {
late double _swiperWidth;
late double _swiperHeight;
late Animation<double> _animation;
late AnimationController _animationController;
SwiperController get _controller => widget.controller;
late int _startIndex;
int? _animationCount;
int _currentIndex = 0;
@override
void initState() {
_currentIndex = widget.index ?? 0;
if (widget.itemWidth == null) {
throw Exception(
'==============\n\nwidget.itemWidth must not be null when use stack layout.\n========\n',
);
}
_createAnimationController();
_controller.addListener(_onController);
super.initState();
}
void _createAnimationController() {
_animationController = AnimationController(vsync: this, value: 0.5);
final tween = Tween(begin: 0.0, end: 1.0);
_animation = tween.animate(_animationController);
}
@override
void didChangeDependencies() {
_ambiguate(WidgetsBinding.instance)!.addPostFrameCallback(_getSize);
super.didChangeDependencies();
}
void _getSize(Duration _) {
if (!mounted) return;
afterRender();
}
@mustCallSuper
void afterRender() {
final renderObject = context.findRenderObject()!;
final 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);
}
}
if (widget.axisDirection != oldWidget.axisDirection) {
afterRender();
}
super.didUpdateWidget(oldWidget);
}
int _ensureIndex(int index) {
var res = index;
res = index % widget.itemCount;
if (res < 0) {
res += widget.itemCount;
}
return res;
}
@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 Stack(
children: list,
);
}
Widget _buildAnimation(BuildContext context, Widget? w) {
final list = <Widget>[];
final animationValue = _animation.value;
for (var i = 0; i < _animationCount! && widget.itemCount > 0; ++i) {
final itemIndex = _currentIndex + i + _startIndex;
if (!widget.loop && (itemIndex >= widget.itemCount || itemIndex < 0)) {
continue;
}
var realIndex = itemIndex % widget.itemCount;
if (realIndex < 0) {
realIndex += widget.itemCount;
}
if (widget.axisDirection == AxisDirection.right) {
list.insert(0, _buildItem(i, realIndex, animationValue));
} else {
list.add(_buildItem(i, realIndex, animationValue));
}
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragStart: _onPanStart,
onHorizontalDragEnd: _onPanEnd,
onHorizontalDragUpdate: _onPanUpdate,
child: ClipRect(
child: Center(
child: _buildContainer(list),
),
),
);
}
@override
Widget build(BuildContext context) {
if (_animationCount == null) {
return Container();
}
return AnimatedBuilder(
animation: _animationController,
builder: _buildAnimation,
);
}
late double _currentValue;
late double _currentPos;
bool _lockScroll = false;
Future<void> _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, st) {
log('error animating _animationController', error: e, stackTrace: st);
} finally {
if (nextIndex != null) {
try {
_animationController.value = 0.5;
} catch (e, st) {
log(
'error setting _animationController.value',
error: e,
stackTrace: st,
);
}
_currentIndex = nextIndex;
}
_lockScroll = false;
}
}
int _getProperNewIndex(int newIndex) {
var res = newIndex;
if (!widget.loop && newIndex >= widget.itemCount - 1) {
res = widget.itemCount - 1;
} else if (!widget.loop && newIndex < 0) {
res = 0;
}
return res;
}
Future<void> _onController() async {
final controller = widget.controller;
final event = controller.event;
if (event is StepBasedIndexControllerEvent) {
final newIndex = event.calcNextIndex(
currentIndex: _currentIndex,
itemCount: widget.itemCount,
loop: widget.loop,
reverse: false,
);
if (_currentIndex == newIndex) return;
return _move(event.targetPosition, nextIndex: newIndex);
} else if (event is MoveIndexControllerEvent) {
final newIndex = _getProperNewIndex(event.newIndex);
if (_currentIndex == newIndex) return;
return _move(event.targetPosition, nextIndex: newIndex);
}
}
Future<void> _onPanEnd(DragEndDetails details) async {
if (_lockScroll) return;
final 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(0.5);
}
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.5);
}
return _move(0.0, nextIndex: _currentIndex + 1);
} else {
return _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;
var value = _currentValue +
((widget.scrollDirection == Axis.horizontal ? details.globalPosition.dx : details.globalPosition.dy) - _currentPos) /
_swiperWidth /
2;
// no loop ?
if (!widget.loop) {
if (widget.itemCount == 1) {
value = 0.5;
}
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;
}
}
double _getValue(List<double> values, double animationValue, int index) {
var 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) {
final s = values[index];
var dx = s.dx;
var 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<T> {
TransformBuilder({required this.values});
final List<T> values;
Widget build(int i, double animationValue, Widget widget);
}
class ScaleTransformBuilder extends TransformBuilder<double> {
ScaleTransformBuilder({
required List<double> values,
this.alignment = Alignment.center,
}) : super(values: values);
final Alignment alignment;
@override
Widget build(int i, double animationValue, Widget widget) {
final s = _getValue(values, animationValue, i);
return Transform.scale(scale: s, child: widget);
}
}
class OpacityTransformBuilder extends TransformBuilder<double> {
OpacityTransformBuilder({required List<double> values}) : super(values: values);
@override
Widget build(int i, double animationValue, Widget widget) {
final v = _getValue(values, animationValue, i);
return Opacity(
opacity: v,
child: widget,
);
}
}
class RotateTransformBuilder extends TransformBuilder<double> {
RotateTransformBuilder({required List<double> values}) : super(values: values);
@override
Widget build(int i, double animationValue, Widget widget) {
final v = _getValue(values, animationValue, i);
return Transform.rotate(
angle: v,
child: widget,
);
}
}
class TranslateTransformBuilder extends TransformBuilder<Offset> {
TranslateTransformBuilder({required List<Offset> values}) : super(values: values);
@override
Widget build(int i, double animationValue, Widget widget) {
final s = _getOffsetValue(values, animationValue, i);
return Transform.translate(
offset: s,
child: widget,
);
}
}
class CustomLayoutOption {
CustomLayoutOption({this.stateCount, required this.startIndex});
final List<TransformBuilder<dynamic>> builders = [];
final int startIndex;
final int? stateCount;
void addOpacity(List<double> values) {
builders.add(OpacityTransformBuilder(values: values));
}
void addTranslate(List<Offset> values) {
builders.add(TranslateTransformBuilder(values: values));
}
void addScale(List<double> values, Alignment alignment) {
builders.add(ScaleTransformBuilder(values: values, alignment: alignment));
}
void addRotate(List<double> values) {
builders.add(RotateTransformBuilder(values: values));
}
}
class _CustomLayoutSwiper extends _SubSwiper {
const _CustomLayoutSwiper({
required this.option,
double? itemWidth,
required bool loop,
double? itemHeight,
ValueChanged<int>? onIndexChanged,
Key? key,
IndexedWidgetBuilder? itemBuilder,
required Curve curve,
int? duration,
int? index,
required int itemCount,
Axis? scrollDirection,
required SwiperController controller,
}) : 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);
final CustomLayoutOption option;
@override
State<StatefulWidget> 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) {
final builders = widget.option.builders;
Widget child = SizedBox(
width: widget.itemWidth ?? double.infinity,
height: widget.itemHeight ?? double.infinity,
child: widget.itemBuilder!(context, realIndex));
for (var i = builders.length - 1; i >= 0; --i) {
final builder = builders[i];
child = builder.build(index, animationValue, child);
}
return child;
}
}
/// Ref: https://docs.flutter.dev/development/tools_base/sdk/release-notes/release-notes-3.0.0#your-code
/// This allows a value of type T or T?
/// to be treated as a value of type T?.
///
/// We use this so that APIs that have become
/// non-nullable can still be used with `!` and `?`
/// to support older versions of the API as well.
T? _ambiguate<T>(T? value) => value;
@@ -0,0 +1,356 @@
// ignore_for_file: constant_identifier_names
/// page indicator library
library flutter_page_indicator;
import 'package:flutter/material.dart';
import '../transformer_page_view/transformer_page_view.dart';
class WarmPainter extends BasePainter {
WarmPainter(PageIndicator widget, double page, int index, Paint paint)
: super(widget, page, index, paint);
@override
void draw(Canvas canvas, double space, double size, double radius) {
final progress = page - index;
final distance = size + space;
final start = index * (size + space);
if (progress > 0.5) {
final right = start + size + distance;
//progress=>0.5-1.0
//left:0.0=>distance
final left = index * distance + distance * (progress - 0.5) * 2;
canvas.drawRRect(
RRect.fromLTRBR(left, 0.0, right, size, Radius.circular(radius)),
_paint);
} else {
final right = start + size + distance * progress * 2;
canvas.drawRRect(
RRect.fromLTRBR(start, 0.0, right, size, Radius.circular(radius)),
_paint);
}
}
}
class DropPainter extends BasePainter {
DropPainter(PageIndicator widget, double page, int index, Paint paint)
: super(widget, page, index, paint);
@override
void draw(Canvas canvas, double space, double size, double radius) {
final progress = page - index;
final dropHeight = widget.dropHeight;
final rate = (0.5 - progress).abs() * 2;
final scale = widget.scale;
//lerp(begin, end, progress)
canvas.drawCircle(
Offset(radius + ((page) * (size + space)),
radius - dropHeight * (1 - rate)),
radius * (scale + rate * (1.0 - scale)),
_paint);
}
}
class NonePainter extends BasePainter {
NonePainter(PageIndicator widget, double page, int index, Paint paint)
: super(widget, page, index, paint);
@override
void draw(Canvas canvas, double space, double size, double radius) {
final progress = page - index;
final secondOffset = index == widget.count - 1
? radius
: radius + ((index + 1) * (size + space));
if (progress > 0.5) {
canvas.drawCircle(Offset(secondOffset, radius), radius, _paint);
} else {
canvas.drawCircle(
Offset(radius + (index * (size + space)), radius), radius, _paint);
}
}
}
class SlidePainter extends BasePainter {
SlidePainter(PageIndicator widget, double page, int index, Paint paint)
: super(widget, page, index, paint);
@override
void draw(Canvas canvas, double space, double size, double radius) {
canvas.drawCircle(
Offset(radius + (page * (size + space)), radius), radius, _paint);
}
}
class ScalePainter extends BasePainter {
ScalePainter(PageIndicator widget, double page, int index, Paint paint)
: super(widget, page, index, paint);
@override
bool _shouldSkip(int index) {
if (this.index == widget.count - 1) {
return index == 0 || index == this.index;
}
return (index == this.index || index == this.index + 1);
}
@override
void paint(Canvas canvas, Size size) {
_paint.color = widget.color;
final space = widget.space;
final size = widget.size;
final radius = size / 2;
final c = widget.count;
for (var i = 0; i < c; ++i) {
if (_shouldSkip(i)) {
continue;
}
canvas.drawCircle(Offset(i * (size + space) + radius, radius),
radius * widget.scale, _paint);
}
_paint.color = widget.activeColor;
draw(canvas, space, size, radius);
}
@override
void draw(Canvas canvas, double space, double size, double radius) {
final secondOffset = index == widget.count - 1
? radius
: radius + ((index + 1) * (size + space));
final progress = page - index;
_paint.color = Color.lerp(widget.activeColor, widget.color, progress)!;
//last
canvas.drawCircle(Offset(radius + (index * (size + space)), radius),
lerp(radius, radius * widget.scale, progress), _paint);
//first
_paint.color = Color.lerp(widget.color, widget.activeColor, progress)!;
canvas.drawCircle(Offset(secondOffset, radius),
lerp(radius * widget.scale, radius, progress), _paint);
}
}
class ColorPainter extends BasePainter {
ColorPainter(PageIndicator widget, double page, int index, Paint paint)
: super(widget, page, index, paint);
@override
bool _shouldSkip(int index) {
if (this.index == widget.count - 1) {
return index == 0 || index == this.index;
}
return (index == this.index || index == this.index + 1);
}
@override
void draw(Canvas canvas, double space, double size, double radius) {
final progress = page - index;
final secondOffset = index == widget.count - 1
? radius
: radius + ((index + 1) * (size + space));
_paint.color = Color.lerp(widget.activeColor, widget.color, progress)!;
//left
canvas.drawCircle(
Offset(radius + (index * (size + space)), radius), radius, _paint);
//right
_paint.color = Color.lerp(widget.color, widget.activeColor, progress)!;
canvas.drawCircle(Offset(secondOffset, radius), radius, _paint);
}
}
abstract class BasePainter extends CustomPainter {
BasePainter(this.widget, this.page, this.index, this._paint);
final PageIndicator widget;
final double page;
final int index;
final Paint _paint;
double lerp(double begin, double end, double progress) {
return begin + (end - begin) * progress;
}
void draw(Canvas canvas, double space, double size, double radius);
bool _shouldSkip(int index) {
return false;
}
//double secondOffset = index == widget.count-1 ? radius : radius + ((index + 1) * (size + space));
@override
void paint(Canvas canvas, Size size) {
_paint.color = widget.color;
final space = widget.space;
final size = widget.size;
final radius = size / 2;
final c = widget.count;
for (var i = 0; i < c; ++i) {
if (_shouldSkip(i)) {
continue;
}
canvas.drawCircle(
Offset(i * (size + space) + radius, radius), radius, _paint);
}
var page = this.page;
if (page < index) {
page = 0.0;
}
_paint.color = widget.activeColor;
draw(canvas, space, size, radius);
}
@override
bool shouldRepaint(BasePainter oldDelegate) {
return oldDelegate.page != page;
}
}
class _PageIndicatorState extends State<PageIndicator> {
int index = 0;
double page = 0;
final _paint = Paint();
BasePainter _createPainter() {
switch (widget.layout) {
case PageIndicatorLayout.NONE:
return NonePainter(widget, page, index, _paint);
case PageIndicatorLayout.SLIDE:
return SlidePainter(widget, page, index, _paint);
case PageIndicatorLayout.WARM:
return WarmPainter(widget, page, index, _paint);
case PageIndicatorLayout.COLOR:
return ColorPainter(widget, page, index, _paint);
case PageIndicatorLayout.SCALE:
return ScalePainter(widget, page, index, _paint);
case PageIndicatorLayout.DROP:
return DropPainter(widget, page, index, _paint);
default:
throw Exception('Not a valid layout');
}
}
@override
Widget build(BuildContext context) {
Widget child = SizedBox(
width: widget.count * widget.size + (widget.count - 1) * widget.space,
height: widget.size,
child: CustomPaint(
painter: _createPainter(),
),
);
if (widget.layout == PageIndicatorLayout.SCALE ||
widget.layout == PageIndicatorLayout.COLOR) {
child = ClipRect(
child: child,
);
}
return IgnorePointer(
child: child,
);
}
void _setInitialPage() {
// use the initial page index but cut off
// the offset specified when looping (kMiddleValue)
index = widget.controller.initialPage % kMiddleValue;
page = index.toDouble();
}
void _onController() {
if (!widget.controller.hasClients) return;
page = widget.controller.page ?? 0.0;
index = page.floor();
setState(() {});
}
@override
void initState() {
super.initState();
widget.controller.addListener(_onController);
_setInitialPage();
}
@override
void didUpdateWidget(PageIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller.removeListener(_onController);
widget.controller.addListener(_onController);
}
}
@override
void dispose() {
widget.controller.removeListener(_onController);
super.dispose();
}
}
enum PageIndicatorLayout {
NONE,
SLIDE,
WARM,
COLOR,
SCALE,
DROP,
}
class PageIndicator extends StatefulWidget {
const PageIndicator({
Key? key,
this.size = 20.0,
this.space = 5.0,
required this.count,
this.activeSize = 20.0,
required this.controller,
this.color = Colors.white30,
this.layout = PageIndicatorLayout.SLIDE,
this.activeColor = Colors.white,
this.scale = 0.6,
this.dropHeight = 20.0,
}) : super(key: key);
/// size of the dots
final double size;
/// space between dots.
final double space;
/// count of dots
final int count;
/// active color
final Color activeColor;
/// normal color
final Color color;
/// layout of the dots,default is [PageIndicatorLayout.SLIDE]
final PageIndicatorLayout? layout;
// Only valid when layout==PageIndicatorLayout.scale
final double scale;
// Only valid when layout==PageIndicatorLayout.drop
final double dropHeight;
final PageController controller;
final double activeSize;
@override
State<StatefulWidget> createState() {
return _PageIndicatorState();
}
}
@@ -0,0 +1,996 @@
// ignore_for_file: constant_identifier_names
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import '../card_swiper.dart';
import 'transformer_page_view/transformer_page_view.dart';
part 'custom_layout.dart';
typedef SwiperOnTap = void Function(int index);
typedef SwiperDataBuilder<T> = Widget Function(
BuildContext context,
T 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 {
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.axisDirection = AxisDirection.left,
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,
this.allowImplicitScrolling = false,
}) : 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<Widget> children,
bool autoplay = false,
PageTransformer? transformer,
int autoplayDelay = kDefaultAutoplayDelayMs,
bool autoplayDisableOnInteraction = true,
int duration = kDefaultAutoplayTransactionDuration,
ValueChanged<int>? onIndexChanged,
int? index,
SwiperOnTap? onTap,
bool loop = true,
Curve curve = Curves.ease,
Axis scrollDirection = Axis.horizontal,
AxisDirection axisDirection = AxisDirection.left,
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,
double? fade,
PageIndicatorLayout indicatorLayout = PageIndicatorLayout.NONE,
SwiperLayout layout = SwiperLayout.DEFAULT,
}) =>
Swiper(
fade: fade,
indicatorLayout: indicatorLayout,
layout: layout,
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,
axisDirection: axisDirection,
pagination: pagination,
control: control,
controller: controller,
loop: loop,
plugins: plugins,
physics: physics,
key: key,
itemBuilder: (context, index) {
return children[index];
},
itemCount: children.length,
);
/// 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;
///left/right for Stack Layout
final AxisDirection axisDirection;
///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;
final bool allowImplicitScrolling;
static Swiper list<T>({
PageTransformer? transformer,
required List<T> list,
CustomLayoutOption? customLayoutOption,
required SwiperDataBuilder<T> 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,
AxisDirection axisDirection = AxisDirection.left,
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,
double? fade,
PageIndicatorLayout indicatorLayout = PageIndicatorLayout.NONE,
SwiperLayout layout = SwiperLayout.DEFAULT,
}) =>
Swiper(
fade: fade,
indicatorLayout: indicatorLayout,
layout: layout,
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,
axisDirection: axisDirection,
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<StatefulWidget> createState() => _SwiperState();
}
abstract class _SwiperTimerMixin extends State<Swiper> {
Timer? _timer;
late SwiperController _controller;
@override
void initState() {
super.initState();
_controller = widget.controller ?? SwiperController();
_controller.addListener(_onController);
if (widget.autoplay) {
_controller.startAutoplay();
} else {
_controller.stopAutoplay();
}
}
void _onController() {
final event = _controller.event;
if (event is AutoPlaySwiperControllerEvent) {
if (event.autoplay) {
if (_timer == null) {
_startAutoplay();
}
} else {
_stopAutoplay();
}
}
}
@override
void didUpdateWidget(Swiper oldWidget) {
super.didUpdateWidget(oldWidget);
if (_controller != oldWidget.controller) {
final oldController = oldWidget.controller;
if (oldController != null) {
oldController.removeListener(_onController);
_controller = oldController;
_controller.addListener(_onController);
}
}
if (widget.autoplay != oldWidget.autoplay) {
if (widget.autoplay) {
_controller.startAutoplay();
} else {
_controller.stopAutoplay();
}
}
}
@override
void dispose() {
_controller.removeListener(_onController);
_stopAutoplay();
super.dispose();
}
void _startAutoplay() {
_stopAutoplay();
_timer = Timer.periodic(
Duration(
milliseconds: widget.autoplayDelay,
),
_onTimer,
);
}
Future<void> _onTimer(Timer timer) async {
return _controller.next(animation: true);
}
void _stopAutoplay() {
_timer?.cancel();
_timer = null;
}
}
class _SwiperState extends _SwiperTimerMixin {
late 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() {
super.initState();
_activeIndex = widget.index ?? widget.controller?.index ?? 0;
if (_isPageViewLayout()) {
_pageController = TransformerPageController(
initialPage: widget.index ?? widget.controller?.index ?? 0,
loop: widget.loop,
itemCount: widget.itemCount,
reverse: widget.transformer?.reverse ?? false,
viewportFraction: widget.viewportFraction,
);
}
}
bool _isPageViewLayout() {
return widget.layout == SwiperLayout.DEFAULT;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
bool _getReverse(Swiper widget) => widget.transformer?.reverse ?? false;
@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 ?? widget.controller?.index ?? 0,
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;
});
final event = _controller.event;
if ((event is MoveIndexControllerEvent) && (event.newIndex != index)) {
return;
}
widget.onIndexChanged?.call(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,
axisDirection: widget.axisDirection,
);
} else if (_isPageViewLayout()) {
//default
var transformer = widget.transformer;
if (widget.scale != null || widget.fade != null) {
transformer = ScaleAndFadeTransformer(scale: widget.scale, fade: widget.fade);
}
final 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,
allowImplicitScrolling: widget.allowImplicitScrolling,
);
if (widget.autoplayDisableOnInteraction && widget.autoplay) {
return NotificationListener(
onNotification: (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;
},
child: child,
);
}
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 const SizedBox.shrink();
}
}
SwiperPluginConfig _ensureConfig(SwiperPluginConfig? config) {
final con = config ??
SwiperPluginConfig(
outer: widget.outer,
itemCount: widget.itemCount,
layout: widget.layout,
indicatorLayout: widget.indicatorLayout,
pageController: _pageController,
activeIndex: _activeIndex,
scrollDirection: widget.scrollDirection,
axisDirection: widget.axisDirection,
controller: _controller,
loop: widget.loop,
);
return con;
}
List<Widget>? _ensureListForStack({
required Widget swiper,
required List<Widget>? listForStack,
required Widget widget,
}) {
final resList = <Widget>[];
if (listForStack == null) {
resList.addAll([swiper, widget]);
} else {
resList.addAll([...listForStack, widget]);
}
return resList;
}
@override
Widget build(BuildContext context) {
final swiper = _buildSwiper();
List<Widget>? listForStack;
SwiperPluginConfig? config;
if (widget.control != null) {
//Stack
config = _ensureConfig(config);
listForStack = _ensureListForStack(
swiper: swiper,
listForStack: listForStack,
widget: widget.control!.build(context, config),
);
}
if (widget.plugins != null) {
config = _ensureConfig(config);
for (final plugin in widget.plugins!) {
listForStack = _ensureListForStack(
swiper: swiper,
listForStack: listForStack,
widget: 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: swiper,
listForStack: listForStack,
widget: widget.pagination!.build(context, config),
);
}
}
if (listForStack != null) {
return Stack(
children: listForStack,
);
}
return swiper;
}
Widget _buildOuterPagination(
SwiperPagination pagination,
Widget swiper,
SwiperPluginConfig config,
) {
final list = <Widget>[];
//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(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: list,
);
}
}
abstract class _SubSwiper extends StatefulWidget {
const _SubSwiper({
Key? key,
required this.loop,
this.itemHeight,
this.itemWidth,
this.duration,
required this.curve,
this.itemBuilder,
required this.controller,
this.index,
required this.itemCount,
this.scrollDirection = Axis.horizontal,
this.axisDirection = AxisDirection.left,
this.onIndexChanged,
}) : super(key: key);
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;
final AxisDirection? axisDirection;
@override
State<StatefulWidget> createState();
int getCorrectIndex(int indexNeedsFix) {
if (itemCount == 0) return 0;
var value = indexNeedsFix % itemCount;
if (value < 0) {
value += itemCount;
}
return value;
}
}
class _TinderSwiper extends _SubSwiper {
const _TinderSwiper({
Key? key,
required Curve curve,
int? duration,
required SwiperController controller,
ValueChanged<int>? onIndexChanged,
double? itemHeight,
double? itemWidth,
IndexedWidgetBuilder? itemBuilder,
int? index,
required bool loop,
required 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 _TinderState();
}
}
class _StackSwiper extends _SubSwiper {
const _StackSwiper({
Key? key,
required Curve curve,
int? duration,
required SwiperController controller,
ValueChanged<int>? onIndexChanged,
double? itemHeight,
double? itemWidth,
IndexedWidgetBuilder? itemBuilder,
int? index,
required bool loop,
required int itemCount,
Axis? scrollDirection,
AxisDirection? axisDirection,
}) : 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,
axisDirection: axisDirection,
);
@override
State<StatefulWidget> createState() => _StackViewState();
}
class _TinderState extends _CustomLayoutStateBase<_TinderSwiper> {
late List<double> scales;
late List<double> offsetsX;
late List<double> offsetsY;
late List<double> opacity;
late List<double> rotates;
double getOffsetY(double scale) {
return widget.itemHeight! - widget.itemHeight! * scale;
}
@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) {
final s = _getValue(scales, animationValue, i);
final f = _getValue(offsetsX, animationValue, i);
final fy = _getValue(offsetsY, animationValue, i);
final o = _getValue(opacity, animationValue, i);
final a = _getValue(rotates, animationValue, i);
final 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<int>(_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> {
late List<double> scales;
late List<double> offsets;
late List<double> opacity;
void _updateValues() {
if (widget.scrollDirection == Axis.horizontal) {
final space = (_swiperWidth - widget.itemWidth!) / 2;
offsets = widget.axisDirection == AxisDirection.left
? [-space, -space / 3 * 2, -space / 3, 0.0, _swiperWidth]
: [_swiperWidth, 0.0, -space / 3, -space / 3 * 2, -space];
} else {
final 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();
final isRightSide = widget.axisDirection == AxisDirection.right;
//length of the values array below
_animationCount = 5;
//Array below this line, '0' index is 1.0, which is the first item show in swiper.
_startIndex = isRightSide ? -1 : -3;
scales = isRightSide ? [1.0, 1.0, 0.9, 0.8, 0.7] : [0.7, 0.8, 0.9, 1.0, 1.0];
opacity = isRightSide ? [1.0, 1.0, 1.0, 0.5, 0.0] : [0.0, 0.5, 1.0, 1.0, 1.0];
_updateValues();
}
@override
Widget _buildItem(int i, int realIndex, double animationValue) {
final s = _getValue(scales, animationValue, i);
final f = _getValue(offsets, animationValue, i);
final o = _getValue(opacity, animationValue, i);
final offset = widget.scrollDirection == Axis.horizontal
? widget.axisDirection == AxisDirection.left
? Offset(f, 0.0)
: Offset(-f, 0.0)
: Offset(0.0, f);
final alignment = widget.scrollDirection == Axis.horizontal
? widget.axisDirection == AxisDirection.left
? Alignment.centerLeft
: Alignment.centerRight
: Alignment.topCenter;
return Opacity(
opacity: o,
child: Transform.translate(
key: ValueKey<int>(_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 {
ScaleAndFadeTransformer({double? fade = 0.3, double? scale = 0.8})
: _fade = fade,
_scale = scale;
final double? _scale;
final double? _fade;
@override
Widget transform(Widget child, TransformInfo info) {
final position = info.position;
var c = child;
if (_scale != null) {
final scaleFactor = (1 - position!.abs()) * (1 - _scale!);
final scale = _scale! + scaleFactor;
c = Transform.scale(
scale: scale,
child: c,
);
}
if (_fade != null) {
final fadeFactor = (1 - position!.abs()) * (1 - _fade!);
final opacity = _fade! + fadeFactor;
c = Opacity(
opacity: opacity,
child: c,
);
}
return c;
}
}
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:hgdj/tools_base/widget/card_swiper/src/swiper_plugin.dart';
class SwiperControl extends SwiperPlugin {
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),
});
///IconData for previous
final IconData iconPrevious;
///iconData for 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;
Widget buildButton({
required SwiperPluginConfig? config,
required Color color,
required IconData iconData,
required int quarterTurns,
required bool previous,
}) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
if (previous) {
await config!.controller.previous(animation: true);
} else {
await config!.controller.next(animation: true);
}
},
child: Padding(
padding: padding,
child: RotatedBox(
quarterTurns: quarterTurns,
child: Icon(
iconData,
semanticLabel: previous ? 'Previous' : 'Next',
size: size,
color: color,
))),
);
}
@override
Widget build(BuildContext context, SwiperPluginConfig config) {
final themeData = Theme.of(context);
final color = this.color ?? themeData.primaryColor;
final disableColor = this.disableColor ?? themeData.disabledColor;
Color prevColor;
Color nextColor;
if (config.loop) {
prevColor = nextColor = color;
} else {
final next = config.activeIndex < config.itemCount - 1;
final 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: config,
color: prevColor,
iconData: iconPrevious,
quarterTurns: 0,
previous: true,
),
buildButton(
config: config,
color: nextColor,
iconData: iconNext,
quarterTurns: 0,
previous: false,
)
],
);
} else {
child = Column(
key: key,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
buildButton(
config: config,
color: prevColor,
iconData: iconPrevious,
quarterTurns: -3,
previous: true,
),
buildButton(
config: config,
color: nextColor,
iconData: iconNext,
quarterTurns: -3,
previous: false,
)
],
);
}
return SizedBox.expand(
child: child,
);
}
}
@@ -0,0 +1,45 @@
import 'swiper_plugin.dart';
import 'transformer_page_view/index_controller.dart';
class SwipeIndexControllerEvent extends IndexControllerEventBase {
SwipeIndexControllerEvent({
required this.pos,
required bool animation,
}) : super(animation: animation);
final double pos;
}
class BuildIndexControllerEvent extends IndexControllerEventBase {
BuildIndexControllerEvent({
required bool animation,
required this.config,
}) : super(animation: animation);
final SwiperPluginConfig config;
}
class AutoPlaySwiperControllerEvent extends IndexControllerEventBase {
AutoPlaySwiperControllerEvent({
required bool animation,
required this.autoplay,
}) : super(animation: animation);
AutoPlaySwiperControllerEvent.start({
required bool animation,
}) : this(animation: animation, autoplay: true);
AutoPlaySwiperControllerEvent.stop({
required bool animation,
}) : this(animation: animation, autoplay: false);
final bool autoplay;
}
class SwiperController extends IndexController {
void startAutoplay({bool animation = true}) {
event = AutoPlaySwiperControllerEvent.start(animation: animation);
notifyListeners();
}
void stopAutoplay({bool animation = true}) {
event = AutoPlaySwiperControllerEvent.stop(animation: animation);
notifyListeners();
}
}
@@ -0,0 +1,303 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import '../card_swiper.dart';
class FractionPaginationBuilder extends SwiperPlugin {
const FractionPaginationBuilder({
this.color,
this.fontSize = 20.0,
this.key,
this.activeColor,
this.activeFontSize = 35.0,
});
///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;
@override
Widget build(BuildContext context, SwiperPluginConfig? config) {
final themeData = Theme.of(context);
final activeColor = this.activeColor ?? themeData.primaryColor;
final color = this.color ?? themeData.scaffoldBackgroundColor;
if (Axis.vertical == config!.scrollDirection) {
return Column(
key: key,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: <Widget>[
Text(
'${config.activeIndex + 1}',
style: TextStyle(color: activeColor, fontSize: activeFontSize),
),
Text(
' / ${config.itemCount}',
style: TextStyle(color: color, fontSize: fontSize),
)
],
);
}
}
}
class RectSwiperPaginationBuilder extends SwiperPlugin {
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,
});
///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;
@override
Widget build(BuildContext context, SwiperPluginConfig config) {
final themeData = Theme.of(context);
final activeColor = this.activeColor ?? themeData.primaryColor;
final color = this.color ?? themeData.scaffoldBackgroundColor;
final list = <Widget>[];
final itemCount = config.itemCount;
final activeIndex = config.activeIndex;
if (itemCount > 20) {
log(
'The itemCount is too big, we suggest use FractionPaginationBuilder '
'instead of DotSwiperPaginationBuilder in this situation',
);
}
for (var i = 0; i < itemCount; ++i) {
final active = i == activeIndex;
final 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 {
const DotSwiperPaginationBuilder({
this.activeColor,
this.color,
this.key,
this.size = 10.0,
this.activeSize = 10.0,
this.space = 3.0,
});
///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;
@override
Widget build(BuildContext context, SwiperPluginConfig config) {
if (config.itemCount > 20) {
log(
'The itemCount is too big, we suggest use FractionPaginationBuilder '
'instead of DotSwiperPaginationBuilder in this situation',
);
}
var activeColor = this.activeColor;
var color = this.color;
if (activeColor == null || color == null) {
final 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,
);
}
final list = <Widget>[];
final itemCount = config.itemCount;
final activeIndex = config.activeIndex;
for (var i = 0; i < itemCount; ++i) {
final 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 {
const SwiperCustomPagination({required this.builder});
final SwiperPaginationBuilder builder;
@override
Widget build(BuildContext context, SwiperPluginConfig config) {
return builder(context, config);
}
}
class SwiperPagination extends SwiperPlugin {
const SwiperPagination({
this.alignment,
this.key,
this.margin = const EdgeInsets.all(10.0),
this.builder = SwiperPagination.dots,
});
/// 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 widget
final SwiperPlugin builder;
final Key? key;
@override
Widget build(BuildContext context, SwiperPluginConfig config) {
final defaultAlignment = 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 ?? defaultAlignment,
child: child,
);
}
return child;
}
}
@@ -0,0 +1,53 @@
import 'package:flutter/widgets.dart';
import '../card_swiper.dart';
/// plugin to display swiper components
///
abstract class SwiperPlugin {
const SwiperPlugin();
Widget build(BuildContext context, SwiperPluginConfig config);
}
class SwiperPluginConfig {
const SwiperPluginConfig({
required this.scrollDirection,
required this.controller,
required this.activeIndex,
required this.itemCount,
this.axisDirection,
this.indicatorLayout,
this.outer,
this.pageController,
this.layout,
this.loop = false,
});
final Axis scrollDirection;
final AxisDirection? axisDirection;
final SwiperController controller;
final int activeIndex;
final int itemCount;
final PageIndicatorLayout? indicatorLayout;
final bool loop;
final bool? outer;
final PageController? pageController;
final SwiperLayout? layout;
}
class SwiperPluginView extends StatelessWidget {
const SwiperPluginView({
Key? key,
required this.plugin,
required this.config,
}) : super(key: key);
final SwiperPlugin plugin;
final SwiperPluginConfig config;
@override
Widget build(BuildContext context) {
return plugin.build(context, config);
}
}
@@ -0,0 +1,119 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
abstract class IndexControllerEventBase {
IndexControllerEventBase({
required this.animation,
});
final bool animation;
final completer = Completer<void>();
Future<void> get future => completer.future;
void complete() {
if (!completer.isCompleted) {
completer.complete();
}
}
}
mixin TargetedPositionControllerEvent on IndexControllerEventBase {
double get targetPosition;
}
mixin StepBasedIndexControllerEvent on TargetedPositionControllerEvent {
int get step;
int calcNextIndex({
required int currentIndex,
required int itemCount,
required bool loop,
required bool reverse,
}) {
var cIndex = currentIndex;
if (reverse) {
cIndex -= step;
} else {
cIndex += step;
}
if (!loop) {
if (cIndex >= itemCount) {
cIndex = itemCount - 1;
} else if (cIndex < 0) {
cIndex = 0;
}
}
return cIndex;
}
}
class NextIndexControllerEvent extends IndexControllerEventBase
with TargetedPositionControllerEvent, StepBasedIndexControllerEvent {
NextIndexControllerEvent({
required bool animation,
}) : super(
animation: animation,
);
@override
int get step => 1;
@override
double get targetPosition => 0;
}
class PrevIndexControllerEvent extends IndexControllerEventBase
with TargetedPositionControllerEvent, StepBasedIndexControllerEvent {
PrevIndexControllerEvent({
required bool animation,
}) : super(
animation: animation,
);
@override
int get step => -1;
@override
double get targetPosition => 1;
}
class MoveIndexControllerEvent extends IndexControllerEventBase
with TargetedPositionControllerEvent {
MoveIndexControllerEvent({
required this.newIndex,
required this.oldIndex,
required bool animation,
}) : super(
animation: animation,
);
final int newIndex;
final int oldIndex;
@override
double get targetPosition => newIndex > oldIndex ? 1 : 0;
}
class IndexController extends ChangeNotifier {
IndexControllerEventBase? event;
int index = 0;
Future<void> move(int index, {bool animation = true}) {
final e = event = MoveIndexControllerEvent(
animation: animation,
newIndex: index,
oldIndex: this.index,
);
notifyListeners();
return e.future;
}
Future<void> next({bool animation = true}) {
final e = event = NextIndexControllerEvent(animation: animation);
notifyListeners();
return e.future;
}
Future<void> previous({bool animation = true}) {
final e = event = PrevIndexControllerEvent(animation: animation);
notifyListeners();
return e.future;
}
}
@@ -0,0 +1,611 @@
/// transformer page view library
library transformer_page_view;
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'index_controller.dart';
///
/// NOTICE::
///
/// In order to make package smaller,currently we're not supporting any build-in page transformers
/// You can find build in transforms here:
///
///
///
const int kMaxValue = 2000000000;
const int kMiddleValue = 1000000000;
/// Default auto play transition duration (in millisecond)
const int kDefaultTransactionDuration = 300;
class TransformInfo {
TransformInfo({
this.index,
this.position,
this.width,
this.height,
this.activeIndex,
required this.fromIndex,
this.forward,
this.done,
this.viewportFraction,
this.scrollDirection,
});
/// The `width` of the `TransformerPageView`
final double? width;
/// The `height` of the `TransformerPageView`
final double? height;
/// The `position` of the widget pass to [PageTransformer.transform]
/// A `position` describes how visible the widget is.
/// The widget in the center of the screen' which is full visible, position is 0.0.
/// The widget in the left ,may be hidden, of the screen's position is less than 0.0, -1.0 when out of the screen.
/// The widget in the right ,may be hidden, of the screen's position is greater than 0.0, 1.0 when out of the screen
///
///
final double? position;
/// The `index` of the widget pass to [PageTransformer.transform]
final int? index;
/// The `activeIndex` of the PageView
final int? activeIndex;
/// The `activeIndex` of the PageView, from user start to swipe
/// It will change when user end drag
final int fromIndex;
/// Next `index` is greater than this `index`
final bool? forward;
/// User drag is done.
final bool? done;
/// Same as [TransformerPageView.viewportFraction]
final double? viewportFraction;
/// Copy from [TransformerPageView.scrollDirection]
final Axis? scrollDirection;
}
abstract class PageTransformer {
PageTransformer({this.reverse = false});
///
final bool reverse;
/// Return a transformed widget, based on child and TransformInfo
Widget transform(Widget child, TransformInfo info);
}
typedef PageTransformerBuilderCallback = Widget Function(
Widget child,
TransformInfo info,
);
class PageTransformerBuilder extends PageTransformer {
PageTransformerBuilder({bool reverse = false, required this.builder})
: super(reverse: reverse);
final PageTransformerBuilderCallback builder;
@override
Widget transform(Widget child, TransformInfo info) {
return builder(child, info);
}
}
class TransformerPageController extends PageController {
TransformerPageController({
int initialPage = 0,
bool keepPage = true,
double viewportFraction = 1.0,
this.loop = false,
this.itemCount = 0,
this.reverse = false,
}) : super(
initialPage: TransformerPageController._getRealIndexFromRenderIndex(
initialPage, loop, itemCount, reverse),
keepPage: keepPage,
viewportFraction: viewportFraction);
final bool loop;
final int itemCount;
final bool reverse;
int getRenderIndexFromRealIndex(num index) {
return _getRenderIndexFromRealIndex(index, loop, itemCount, reverse);
}
int? getRealItemCount() {
if (itemCount == 0) return 0;
return loop ? itemCount + kMaxValue : itemCount;
}
static int _getRenderIndexFromRealIndex(
num index,
bool loop,
int itemCount,
bool reverse,
) {
if (itemCount == 0) return 0;
int renderIndex;
if (loop) {
renderIndex = (index - kMiddleValue).toInt();
renderIndex = renderIndex % itemCount;
if (renderIndex < 0) {
renderIndex += itemCount;
}
} else {
renderIndex = index.toInt();
}
if (reverse) {
renderIndex = itemCount - renderIndex - 1;
}
return renderIndex;
}
double get realPage => super.page ?? 0.0;
static double? _getRenderPageFromRealPage(
double page,
bool loop,
int itemCount,
bool reverse,
) {
double? renderPage;
if (loop) {
renderPage = page - kMiddleValue;
renderPage = renderPage % itemCount;
if (renderPage < 0) {
renderPage += itemCount;
}
} else {
renderPage = page;
}
if (reverse) {
renderPage = itemCount - renderPage - 1;
}
return renderPage;
}
@override
double? get page {
return loop
? _getRenderPageFromRealPage(realPage, loop, itemCount, reverse)
: realPage;
}
int getRealIndexFromRenderIndex(num index) {
return _getRealIndexFromRenderIndex(index, loop, itemCount, reverse);
}
static int _getRealIndexFromRenderIndex(
num index, bool loop, int itemCount, bool reverse) {
var result = reverse ? itemCount - index - 1 as int : index as int;
if (loop) {
result += kMiddleValue;
}
return result;
}
}
class TransformerPageView extends StatefulWidget {
/// Creates a scrollable list that works page by page using widgets that are
/// created on demand.
///
/// This constructor is appropriate for page views with a large (or infinite)
/// number of children because the builder is called only for those children
/// that are actually visible.
///
/// Providing a non-null [itemCount] lets the [PageView] compute the maximum
/// scroll extent.
///
/// [itemBuilder] will be called only with indices greater than or equal to
/// zero and less than [itemCount].
const TransformerPageView({
Key? key,
this.index,
Duration? duration,
this.curve = Curves.ease,
this.viewportFraction = 1.0,
required this.loop,
this.scrollDirection = Axis.horizontal,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
this.controller,
this.transformer,
this.allowImplicitScrolling = false,
this.itemBuilder,
this.pageController,
required this.itemCount,
}) : assert(itemCount == 0 || itemBuilder != null || transformer != null),
duration = duration ??
const Duration(milliseconds: kDefaultTransactionDuration),
super(key: key);
factory TransformerPageView.children({
Key? key,
int? index,
Duration? duration,
Curve curve = Curves.ease,
double viewportFraction = 1.0,
bool loop = false,
Axis scrollDirection = Axis.horizontal,
ScrollPhysics? physics,
bool pageSnapping = true,
ValueChanged<int?>? onPageChanged,
IndexController? controller,
PageTransformer? transformer,
bool allowImplicitScrolling = false,
required List<Widget> children,
TransformerPageController? pageController,
}) {
return TransformerPageView(
itemCount: children.length,
itemBuilder: (context, index) {
return children[index];
},
pageController: pageController,
transformer: transformer,
pageSnapping: pageSnapping,
key: key,
index: index,
loop: loop,
duration: duration,
curve: curve,
viewportFraction: viewportFraction,
scrollDirection: scrollDirection,
physics: physics,
allowImplicitScrolling: allowImplicitScrolling,
onPageChanged: onPageChanged,
controller: controller,
);
}
/// Create a `transformed` widget base on the widget that has been passed to the [PageTransformer.transform].
/// See [TransformInfo]
///
final PageTransformer? transformer;
/// Same as [PageView.scrollDirection]
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Same as [PageView.physics]
final ScrollPhysics? physics;
/// Set to false to disable page snapping, useful for custom scroll behavior.
/// Same as [PageView.pageSnapping]
final bool pageSnapping;
/// Called whenever the page in the center of the viewport changes.
/// Same as [PageView.onPageChanged]
final ValueChanged<int>? onPageChanged;
final IndexedWidgetBuilder? itemBuilder;
// See [IndexController.mode],[IndexController.next],[IndexController.previous]
final IndexController? controller;
/// Animation duration
final Duration duration;
/// Animation curve
final Curve curve;
final TransformerPageController? pageController;
/// Set true to open infinity loop mode.
final bool loop;
/// This value is only valid when `pageController` is not set,
final int itemCount;
/// This value is only valid when `pageController` is not set,
final double viewportFraction;
/// If not set, it is controlled by this widget.
final int? index;
final bool allowImplicitScrolling;
@override
State<StatefulWidget> createState() => _TransformerPageViewState();
static int getRealIndexFromRenderIndex({
required bool reverse,
int index = 0,
int itemCount = 0,
required bool loop,
}) {
var initPage = reverse ? (itemCount - index - 1) : index;
if (loop) {
initPage += kMiddleValue;
}
return initPage;
}
static PageController createPageController({
required bool reverse,
int index = 0,
int itemCount = 0,
required bool loop,
required double viewportFraction,
}) {
return PageController(
initialPage: getRealIndexFromRenderIndex(
reverse: reverse,
index: index,
itemCount: itemCount,
loop: loop,
),
viewportFraction: viewportFraction,
);
}
}
class _TransformerPageViewState extends State<TransformerPageView> {
Size? _size;
int _activeIndex = 0;
late double _currentPixels;
bool _done = false;
///This value will not change until user end drag.
late int _fromIndex;
PageTransformer? _transformer;
late TransformerPageController _pageController;
Widget _buildItemNormal(BuildContext context, int index) {
final renderIndex = _pageController.getRenderIndexFromRealIndex(index);
return widget.itemBuilder!(context, renderIndex);
}
Widget _buildItem(BuildContext context, int index) {
return AnimatedBuilder(
animation: _pageController,
builder: (c, w) {
final renderIndex =
_pageController.getRenderIndexFromRealIndex(index);
final child = widget.itemBuilder?.call(context, renderIndex) ??
const SizedBox.shrink();
if (_size == null) {
return child;
}
double position;
final page = _pageController.realPage;
if (_transformer!.reverse) {
position = page - index;
} else {
position = index - page;
}
position *= widget.viewportFraction;
final info = TransformInfo(
index: renderIndex,
width: _size!.width,
height: _size!.height,
position: position.clamp(-1.0, 1.0),
activeIndex:
_pageController.getRenderIndexFromRealIndex(_activeIndex),
fromIndex: _fromIndex,
forward: _pageController.position.pixels - _currentPixels >= 0,
done: _done,
scrollDirection: widget.scrollDirection,
viewportFraction: widget.viewportFraction,
);
return _transformer!.transform(child, info);
});
}
double? _calcCurrentPixels() {
_currentPixels = _pageController.getRenderIndexFromRealIndex(_activeIndex) *
_pageController.position.viewportDimension *
widget.viewportFraction;
// print("activeIndex:$_activeIndex , pix:$_currentPixels");
return _currentPixels;
}
@override
Widget build(BuildContext context) {
final builder = _transformer == null ? _buildItemNormal : _buildItem;
final child = PageView.builder(
allowImplicitScrolling: widget.allowImplicitScrolling,
itemBuilder: builder,
itemCount: _pageController.getRealItemCount(),
onPageChanged: _onIndexChanged,
controller: _pageController,
scrollDirection: widget.scrollDirection,
physics: widget.physics,
pageSnapping: widget.pageSnapping,
reverse: _pageController.reverse,
);
if (_transformer == null) {
return child;
}
return NotificationListener(
onNotification: (notification) {
if (notification is ScrollStartNotification) {
_calcCurrentPixels();
_done = false;
_fromIndex = _activeIndex;
} else if (notification is ScrollEndNotification) {
_calcCurrentPixels();
_fromIndex = _activeIndex;
_done = true;
}
return false;
},
child: child,
);
}
void _onIndexChanged(int index) {
_activeIndex = index;
widget.onPageChanged
?.call(_pageController.getRenderIndexFromRealIndex(index));
}
void _onGetSize(Duration _) {
if (!mounted) return;
Size? size;
final renderObject = context.findRenderObject();
if (renderObject != null) {
final bounds = renderObject.paintBounds;
size = bounds.size;
}
_calcCurrentPixels();
onGetSize(size);
}
void onGetSize(Size? size) {
if (mounted) {
setState(() {
_size = size;
});
}
}
IndexController? _controller;
@override
void initState() {
_transformer = widget.transformer;
// int index = widget.index ?? 0;
_pageController = widget.pageController ??
TransformerPageController(
initialPage: widget.index ?? 0,
itemCount: widget.itemCount,
loop: widget.loop,
reverse: widget.transformer?.reverse ?? false,
);
// int initPage = _getRealIndexFromRenderIndex(index);
// _pageController = PageController(initialPage: initPage,viewportFraction: widget.viewportFraction);
_fromIndex = _activeIndex = _pageController.initialPage;
_controller = widget.controller;
_controller?.addListener(onChangeNotifier);
super.initState();
}
@override
void didUpdateWidget(TransformerPageView oldWidget) {
_transformer = widget.transformer;
final index = widget.index ?? 0;
var created = false;
if (_pageController != widget.pageController) {
if (widget.pageController != null) {
_pageController = widget.pageController!;
} else {
created = true;
_pageController = TransformerPageController(
initialPage: widget.index ?? 0,
itemCount: widget.itemCount,
loop: widget.loop,
reverse: widget.transformer?.reverse ?? false,
);
}
}
if (_pageController.getRenderIndexFromRealIndex(_activeIndex) != index) {
_fromIndex = _activeIndex = _pageController.initialPage;
if (!created) {
final initPage = _pageController.getRealIndexFromRenderIndex(index);
if (_pageController.hasClients) {
unawaited(_pageController.animateToPage(
initPage,
duration: widget.duration,
curve: widget.curve,
));
}
}
}
if (_transformer != null) {
_ambiguate(WidgetsBinding.instance)!.addPostFrameCallback(_onGetSize);
}
if (_controller != widget.controller) {
_controller?.removeListener(onChangeNotifier);
_controller = widget.controller;
_controller?.addListener(onChangeNotifier);
}
super.didUpdateWidget(oldWidget);
}
@override
void didChangeDependencies() {
if (_transformer != null) {
_ambiguate(WidgetsBinding.instance)!.addPostFrameCallback(_onGetSize);
}
super.didChangeDependencies();
}
Future<void> onChangeNotifier() async {
final controller = widget.controller!;
final event = controller.event;
int index;
if (event == null) return;
if (event is MoveIndexControllerEvent) {
index = _pageController.getRealIndexFromRenderIndex(event.newIndex);
} else if (event is StepBasedIndexControllerEvent) {
index = event.calcNextIndex(
currentIndex: _activeIndex,
itemCount: _pageController.itemCount,
loop: _pageController.loop,
reverse: _pageController.reverse,
);
} else {
//ignore other events
return;
}
if (_pageController.hasClients) {
if (event.animation) {
await _pageController
.animateToPage(
index,
duration: widget.duration,
curve: widget.curve,
)
.whenComplete(event.complete);
} else {
event.complete();
}
} else {
event.complete();
}
}
@override
void dispose() {
_controller?.removeListener(onChangeNotifier);
super.dispose();
}
}
/// Ref: https://docs.flutter.dev/development/tools_base/sdk/release-notes/release-notes-3.0.0#your-code
/// This allows a value of type T or T?
/// to be treated as a value of type T?.
///
/// We use this so that APIs that have become
/// non-nullable can still be used with `!` and `?`
/// to support older versions of the API as well.
T? _ambiguate<T>(T? value) => value;