From 24c7214395c23f08688c06cd614b8014811065cc Mon Sep 17 00:00:00 2001 From: hongyun Date: Thu, 8 Aug 2019 17:26:43 +0800 Subject: [PATCH] Let user scroll to next page when PageView child is zooming (#41) --- .../examples/gallery/gallery_example.dart | 124 +++++--- lib/photo_view.dart | 5 + lib/photo_view_gallery.dart | 55 +++- lib/src/photo_view_controller_delegate.dart | 38 ++- lib/src/photo_view_image_wrapper.dart | 32 +- lib/src/photo_view_pageview_wrapper.dart | 277 ++++++++++++++++++ pubspec.lock | 2 +- 7 files changed, 479 insertions(+), 54 deletions(-) create mode 100644 lib/src/photo_view_pageview_wrapper.dart diff --git a/example/lib/screens/examples/gallery/gallery_example.dart b/example/lib/screens/examples/gallery/gallery_example.dart index 45584e28..bd5b606c 100644 --- a/example/lib/screens/examples/gallery/gallery_example.dart +++ b/example/lib/screens/examples/gallery/gallery_example.dart @@ -5,20 +5,13 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view_example/screens/app_bar.dart'; import 'package:photo_view_example/screens/examples/gallery/gallery_example_item.dart'; -class GalleryExample extends StatelessWidget { - void open(BuildContext context, final int index) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GalleryPhotoViewWrapper( - galleryItems: galleryItems, - backgroundDecoration: const BoxDecoration( - color: Colors.black, - ), - initialIndex: index, - ), - )); - } +class GalleryExample extends StatefulWidget { + @override + _GalleryExampleState createState() => _GalleryExampleState(); +} + +class _GalleryExampleState extends State { + bool useCustomPageView = true; @override Widget build(BuildContext context) { @@ -32,38 +25,74 @@ class GalleryExample extends StatelessWidget { ), Expanded( child: Center( - child: Row( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - GalleryExampleItemThumbnail( - galleryExampleItem: galleryItems[0], - onTap: () { - open(context, 0); - }, - ), - GalleryExampleItemThumbnail( - galleryExampleItem: galleryItems[2], - onTap: () { - open(context, 2); - }, - ), - GalleryExampleItemThumbnail( - galleryExampleItem: galleryItems[3], - onTap: () { - open(context, 3); - }, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GalleryExampleItemThumbnail( + galleryExampleItem: galleryItems[0], + onTap: () { + open(context, 0); + }, + ), + GalleryExampleItemThumbnail( + galleryExampleItem: galleryItems[2], + onTap: () { + open(context, 2); + }, + ), + GalleryExampleItemThumbnail( + galleryExampleItem: galleryItems[3], + onTap: () { + open(context, 3); + }, + ), + ], ), + Container( + height: 30, + child: Center( + child: Row( + children: [ + const Text(" use custom pageView"), + Checkbox( + value: useCustomPageView, + onChanged: (value) { + setState(() { + useCustomPageView = value; + }); + }), + ], + ))), ], ))), ], ), ); } + + void open(BuildContext context, final int index) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GalleryPhotoViewWrapper( + usePageViewWrapper: useCustomPageView, + galleryItems: galleryItems, + backgroundDecoration: const BoxDecoration( + color: Colors.black, + ), + initialIndex: index, + ), + )); + } } class GalleryPhotoViewWrapper extends StatefulWidget { GalleryPhotoViewWrapper( {this.loadingChild, + this.usePageViewWrapper = false, this.backgroundDecoration, this.minScale, this.maxScale, @@ -78,6 +107,7 @@ class GalleryPhotoViewWrapper extends StatefulWidget { final int initialIndex; final PageController pageController; final List galleryItems; + final bool usePageViewWrapper; @override State createState() { @@ -87,6 +117,7 @@ class GalleryPhotoViewWrapper extends StatefulWidget { class _GalleryPhotoViewWrapperState extends State { int currentIndex; + @override void initState() { currentIndex = widget.initialIndex; @@ -94,9 +125,11 @@ class _GalleryPhotoViewWrapperState extends State { } void onPageChanged(int index) { - setState(() { - currentIndex = index; - }); + if(!widget.usePageViewWrapper){ + setState(() { + currentIndex = index; + }); + } } @override @@ -118,15 +151,20 @@ class _GalleryPhotoViewWrapperState extends State { backgroundDecoration: widget.backgroundDecoration, pageController: widget.pageController, onPageChanged: onPageChanged, + usePageViewWrapper: widget.usePageViewWrapper, ), - Container( - padding: const EdgeInsets.all(20.0), - child: Text( - "Image ${currentIndex + 1}", - style: const TextStyle( - color: Colors.white, fontSize: 17.0, decoration: null), - ), - ) + widget.usePageViewWrapper + ? Container() + : Container( + padding: const EdgeInsets.all(20.0), + child: Text( + "Image ${currentIndex + 1}", + style: const TextStyle( + color: Colors.white, + fontSize: 17.0, + decoration: null), + ), + ) ], )), ); diff --git a/lib/photo_view.dart b/lib/photo_view.dart index b3341de8..7ae70bb0 100644 --- a/lib/photo_view.dart +++ b/lib/photo_view.dart @@ -234,6 +234,7 @@ class PhotoView extends StatefulWidget { this.scaleStateCycle, this.onTapUp, this.onTapDown, + this.index, }) : child = null, childSize = null, super(key: key); @@ -263,11 +264,13 @@ class PhotoView extends StatefulWidget { this.scaleStateCycle, this.onTapUp, this.onTapDown, + this.index, }) : loadingChild = null, imageProvider = null, gaplessPlayback = false, super(key: key); + int index; /// Given a [imageProvider] it resolves into an zoomable image widget using. It /// is required final ImageProvider imageProvider; @@ -480,6 +483,7 @@ class _PhotoViewState extends State Widget _buildCustomChild(BuildContext context) { return PhotoViewImageWrapper.customChild( + index: widget.index, customChild: widget.child, backgroundDecoration: widget.backgroundDecoration, enableRotation: widget.enableRotation, @@ -529,6 +533,7 @@ class _PhotoViewState extends State Widget _buildWrapperImage(BuildContext context) { return PhotoViewImageWrapper( + index: widget.index, imageProvider: widget.imageProvider, backgroundDecoration: widget.backgroundDecoration, gaplessPlayback: widget.gaplessPlayback, diff --git a/lib/photo_view_gallery.dart b/lib/photo_view_gallery.dart index e7013fbb..211a0fb8 100644 --- a/lib/photo_view_gallery.dart +++ b/lib/photo_view_gallery.dart @@ -5,6 +5,7 @@ import 'package:photo_view/photo_view.dart'; import 'package:photo_view/src/photo_view_controller.dart'; import 'package:photo_view/src/photo_view_image_wrapper.dart'; import 'package:photo_view/src/photo_view_scale_state.dart'; +import 'package:photo_view/src/photo_view_pageview_wrapper.dart'; /// A type definition for a [Function] that receives a index after a page change in [PhotoViewGallery] typedef PhotoViewGalleryPageChangedCallback = void Function(int index); @@ -83,6 +84,7 @@ class PhotoViewGallery extends StatefulWidget { this.transitionOnUserGestures = false, this.scrollPhysics, this.scrollDirection = Axis.horizontal, + this.usePageViewWrapper = false, }) : _isBuilder = false, itemCount = null, builder = null, @@ -108,6 +110,7 @@ class PhotoViewGallery extends StatefulWidget { this.transitionOnUserGestures = false, this.scrollPhysics, this.scrollDirection = Axis.horizontal, + this.usePageViewWrapper = false, }) : _isBuilder = true, pageOptions = null, assert(itemCount != null), @@ -161,6 +164,9 @@ class PhotoViewGallery extends StatefulWidget { final bool _isBuilder; + ///A bool indicate to use [PageViewWrapper] + final bool usePageViewWrapper; + @override State createState() { return _PhotoViewGalleryState(); @@ -179,12 +185,15 @@ class _PhotoViewGalleryState extends State { } void scaleStateChangedCallback(PhotoViewScaleState scaleState) { - setState(() { - _locked = (scaleState == PhotoViewScaleState.initial || - scaleState == PhotoViewScaleState.zoomedOut) - ? false - : true; - }); + if (!widget.usePageViewWrapper) { + setState(() { + _locked = (scaleState == PhotoViewScaleState.initial || + scaleState == PhotoViewScaleState.zoomedOut) + ? false + : true; + }); + } + if (widget.scaleStateChangedCallback != null) { widget.scaleStateChangedCallback(scaleState); } @@ -203,6 +212,36 @@ class _PhotoViewGalleryState extends State { @override Widget build(BuildContext context) { + return widget.usePageViewWrapper? _getPageViewWrapper(): _getPageView(); + } + + PageViewWrapper _getPageViewWrapper() { + debugPrint("_getPageViewWrapper"); + final pageChangeListeners = OnPageChangedWrapper(); + pageChangeListeners.addListener(widget.onPageChanged); + + final customController = PageViewWrapperController( + pageViewController: _controller, + onPageChangedWrapper: pageChangeListeners); + + final pageView = PageView.builder( + reverse: widget.reverse, + controller: _controller, + onPageChanged: pageChangeListeners.onPageChanged, + itemCount: itemCount, + itemBuilder: _buildItem, + scrollDirection: widget.scrollDirection, + physics: + _locked ? const NeverScrollableScrollPhysics() : widget.scrollPhysics, + ); + + return PageViewWrapper( + pageView: pageView, + controller: customController, + ); + } + + PageView _getPageView() { return PageView.builder( reverse: widget.reverse, controller: _controller, @@ -211,7 +250,7 @@ class _PhotoViewGalleryState extends State { itemBuilder: _buildItem, scrollDirection: widget.scrollDirection, physics: - _locked ? const NeverScrollableScrollPhysics() : widget.scrollPhysics, + _locked ? const NeverScrollableScrollPhysics() : widget.scrollPhysics, ); } @@ -222,6 +261,7 @@ class _PhotoViewGalleryState extends State { final PhotoView photoView = isCustomChild ? PhotoView.customChild( key: ObjectKey(index), + index:index, child: pageOption.child, childSize: pageOption.childSize, backgroundDecoration: widget.backgroundDecoration, @@ -241,6 +281,7 @@ class _PhotoViewGalleryState extends State { ) : PhotoView( key: ObjectKey(index), + index:index, imageProvider: pageOption.imageProvider, loadingChild: widget.loadingChild, backgroundDecoration: widget.backgroundDecoration, diff --git a/lib/src/photo_view_controller_delegate.dart b/lib/src/photo_view_controller_delegate.dart index b55cb58a..b5923253 100644 --- a/lib/src/photo_view_controller_delegate.dart +++ b/lib/src/photo_view_controller_delegate.dart @@ -20,7 +20,7 @@ class PhotoViewControllerDelegate { final ScaleBoundaries scaleBoundaries; final ScaleStateCycle scaleStateCycle; final Alignment basePosition; - + OffsetWrapper _lastOffsetWrapper; Function(double prevScale, double nextScale) _animateScale; void startListeners() { @@ -163,7 +163,27 @@ class PhotoViewControllerDelegate { final double computedY = screenHeight < computedHeight ? y.clamp(minY, maxY) : 0.0; - return Offset(computedX, computedY); + final position = Offset(computedX, computedY); + final result = OffsetWrapper(position, x < minX, x > maxX); + _lastOffsetWrapper = result; + return position; + } + + bool canMove(double scale, Offset delta) { + if(scale!=1.0) { + //when child is zooming + return true; + } + if (_lastOffsetWrapper != null) { + final moveRight = delta.dx < 0; + if (_lastOffsetWrapper.reachLeftBound) { + return moveRight; + } else if (_lastOffsetWrapper.reachRightBound) { + return !moveRight; + } + } + + return scaleStateController.scaleState!=PhotoViewScaleState.initial; } void dispose() { @@ -172,3 +192,17 @@ class PhotoViewControllerDelegate { scaleStateController.removeIgnorableListener(_blindScaleStateListener); } } + +class OffsetWrapper { + OffsetWrapper( + this.position, this.reachRightBound, this.reachLeftBound); + final bool reachLeftBound; + final bool reachRightBound; + final Offset position; + + @override + String toString() { + return "reachLeftBound=$reachRightBound, reachRightBound=$reachLeftBound, offset=$position"; + } +} + diff --git a/lib/src/photo_view_image_wrapper.dart b/lib/src/photo_view_image_wrapper.dart index 471a7f4d..4981d172 100644 --- a/lib/src/photo_view_image_wrapper.dart +++ b/lib/src/photo_view_image_wrapper.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:photo_view/src/photo_view_controller.dart'; import 'package:photo_view/src/photo_view_controller_delegate.dart'; +import 'package:photo_view/src/photo_view_pageview_wrapper.dart'; typedef PhotoViewImageTapUpCallback = Function(BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue); @@ -20,6 +21,7 @@ class PhotoViewImageWrapper extends StatefulWidget { this.transitionOnUserGestures = false, this.onTapUp, this.onTapDown, + this.index, @required this.delegate, }) : customChild = null, super(key: key); @@ -33,11 +35,13 @@ class PhotoViewImageWrapper extends StatefulWidget { this.transitionOnUserGestures = false, this.onTapUp, this.onTapDown, + this.index, @required this.delegate, }) : imageProvider = null, gaplessPlayback = false, super(key: key); + final int index; final Decoration backgroundDecoration; final ImageProvider imageProvider; final bool gaplessPlayback; @@ -58,10 +62,11 @@ class PhotoViewImageWrapper extends StatefulWidget { } class _PhotoViewImageWrapperState extends State - with TickerProviderStateMixin { + with TickerProviderStateMixin implements GestureDetectorCallback { Offset _normalizedPosition; double _scaleBefore; double _rotationBefore; + PageViewWrapper _customViewPager; AnimationController _scaleAnimationController; Animation _scaleAnimation; @@ -84,6 +89,7 @@ class _PhotoViewImageWrapperState extends State widget.delegate.controller.rotation = _rotationAnimation.value; } + @override void onScaleStart(ScaleStartDetails details) { _rotationBefore = widget.delegate.controller.rotation; _scaleBefore = widget.delegate.scale; @@ -94,6 +100,7 @@ class _PhotoViewImageWrapperState extends State _rotationAnimationController.stop(); } + @override void onScaleUpdate(ScaleUpdateDetails details) { final double newScale = _scaleBefore * details.scale; final Offset delta = details.focalPoint - _normalizedPosition; @@ -107,6 +114,7 @@ class _PhotoViewImageWrapperState extends State rotationFocusPoint: details.focalPoint); } + @override void onScaleEnd(ScaleEndDetails details) { final double _scale = widget.delegate.scale; final Offset _position = widget.delegate.controller.position; @@ -146,6 +154,16 @@ class _PhotoViewImageWrapperState extends State widget.delegate.checkAndSetToInitialScaleState(); } + @override + bool canMove(double scale, Offset delta) { + return widget.delegate.canMove(scale, delta); + } + + @override + void onDoubleTap() { + widget.delegate.nextScaleState(); + } + void animateScale(double from, double to) { _scaleAnimation = Tween( begin: from, @@ -194,6 +212,15 @@ class _PhotoViewImageWrapperState extends State widget.delegate.addAnimateOnScaleStateUpdate(animateOnScaleStateUpdate); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _customViewPager = context.ancestorWidgetOfExactType(PageViewWrapper); + if (_customViewPager != null && widget.index != null) { + _customViewPager.controller.addChildGestureCallback(widget.index, this); + } + } + void animateOnScaleStateUpdate(double prevScale, double nextScale) { animateScale(prevScale, nextScale); animatePosition(widget.delegate.controller.position, Offset.zero); @@ -207,6 +234,9 @@ class _PhotoViewImageWrapperState extends State _positionAnimationController.dispose(); _rotationAnimationController.dispose(); widget.delegate.dispose(); + if (_customViewPager != null && widget.index != null) { + _customViewPager.controller.removeChildGestureCallback(widget.index, this); + } super.dispose(); } diff --git a/lib/src/photo_view_pageview_wrapper.dart b/lib/src/photo_view_pageview_wrapper.dart new file mode 100644 index 00000000..cfaf67cc --- /dev/null +++ b/lib/src/photo_view_pageview_wrapper.dart @@ -0,0 +1,277 @@ +import 'dart:collection'; +import 'package:flutter/material.dart'; + +/// This class wrap [PageView], use [GestureDetector] to detect touch event and decide which widget should move. +/// [PageViewWrapperController] is touch event handler, it will query [PageView] child move or not, if child can't move +/// it will use [PageController] to move [PageView] +class PageViewWrapper extends StatefulWidget { + PageViewWrapper({@required this.pageView, @required this.controller}); + + final PageView pageView; + final PageViewWrapperController controller; + + @override + _PageViewWrapperState createState() => _PageViewWrapperState(); +} + +class _PageViewWrapperState extends State { + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: widget.controller?.onDoubleTap, + onScaleStart: widget.controller?.onScaleStart, + onScaleUpdate: widget.controller?.onScaleUpdate, + onScaleEnd: widget.controller?.onScaleEnd, + child: NotificationListener( + child: IgnorePointer(child: widget.pageView), + onNotification: (ScrollNotification notify) { + if (notify.metrics.pixels <= notify.metrics.maxScrollExtent) { + widget.controller?.onScrollUpdate(notify.metrics.pixels); + } + return false; + }, + ), + ); + } +} + +abstract class GestureDetectorCallback { + GestureDetectorCallback(); + + void onDoubleTap(); + + void onScaleStart(ScaleStartDetails detail); + + void onScaleUpdate(ScaleUpdateDetails detail); + + void onScaleEnd(ScaleEndDetails details); + + bool canMove(double scale, Offset delta); +} + +///Touch event handler. +///It will dispatch event to PageView child and will move itself if needed. +class PageViewWrapperController implements GestureDetectorCallback { + PageViewWrapperController( + {this.pageViewController, this.onPageChangedWrapper}) { + onPageChangedWrapper.addListener(onPageChange); + _currentSelectPage = pageViewController.initialPage; + } + final String tag = 'PageViewGestureController'; + final bool verbose = true; + final int durationMs = 400; + final double pi = 3.14; + final OnPageChangedWrapper onPageChangedWrapper; + PageController pageViewController; + double _lastScrollPixels; + double _freshScrollPixels; + ScaleStartDetails _scaleStartDetail; + bool needNotifyChildEnd = false; + Set childCallbacks = + HashSet(); + Map callbackStartMap = HashMap(); + Map callbackIntMap = HashMap(); + Offset _startPosition; + Offset _lastDelta; + int _currentSelectPage = 0; + + void onPageChange(int value) { + _currentSelectPage = value; + if(verbose){ debugPrint('$tag onPageChange $value'); } + } + + void addChildGestureCallback( + int index, GestureDetectorCallback childCallback) { + if(verbose){ debugPrint('$tag addChildGestureCallback $index=>$childCallback'); } + childCallbacks.add(childCallback); + callbackIntMap[childCallback] = index; + } + + void removeChildGestureCallback( + int index, GestureDetectorCallback childCallback) { + if(verbose){ debugPrint('$tag removeChildGestureCallback $index=>$childCallback'); } + childCallbacks.remove(childCallback); + callbackIntMap.remove(childCallback); + } + + @override + void onDoubleTap() { + for (final callback in childCallbacks) { + final callbackIndex = callbackIntMap[callback]; + if (callbackIndex != null && callbackIndex == _currentSelectPage) { + callback.onDoubleTap(); + } + } + } + + @override + void onScaleEnd(ScaleEndDetails details) { + if(verbose){ debugPrint('$tag onScaleEnd'); } + _startPosition = null; + _scaleStartDetail = null; + final direction = details.velocity.pixelsPerSecond.direction; + final distance = details.velocity.pixelsPerSecond.distance; + if (needNotifyChildEnd) { + for (final callback in childCallbacks) { + final callbackIndex = callbackIntMap[callback]; + if (callbackIndex != null && callbackIndex == _currentSelectPage) { + final bool started = callbackStartMap[callback]; + if (started != null && started) { + callback.onScaleEnd(details); + } + } + } + needNotifyChildEnd = false; + } else { + final ScrollPositionWithSingleContext scrollPosition = + pageViewController.position; + if (direction != 0 && distance != 0) { + final bool firstQuadrant = direction >= -pi * 3 / 8 && direction <= 0; + final bool secondQuadrant = + direction >= -pi && direction <= -5 * pi / 8; + final bool thirdQuadrant = direction >= 5 * pi / 8 && direction <= pi; + final bool fourthQuadrant = direction > 0 && direction < pi * 3 / 8; + if (firstQuadrant || + secondQuadrant || + thirdQuadrant || + fourthQuadrant) { + if (secondQuadrant || thirdQuadrant) { + //向左滑动 + scrollPosition + .goBallistic(details.velocity.pixelsPerSecond.distance); + } else { + //向右滑动 + scrollPosition + .goBallistic(-details.velocity.pixelsPerSecond.distance); + } + } else { + animateToPage(scrollPosition); + } + } else { + animateToPage(scrollPosition); + } + } + } + + @override + void onScaleStart(ScaleStartDetails detail) { + if(verbose){ debugPrint('$tag onScaleStart $_startPosition'); } + _startPosition = detail.focalPoint; + _lastScrollPixels = + pageViewController.position.pixels ?? _freshScrollPixels; + _scaleStartDetail = detail; + } + + @override + void onScaleUpdate(ScaleUpdateDetails detail) { + if (childCallbacks.isNotEmpty) { + final Offset delta = detail.focalPoint - _startPosition; + bool pageViewShouldMove = false; + for (final callback in childCallbacks) { + final callbackIndex = callbackIntMap[callback]; + if (callbackIndex != null && callbackIndex == _currentSelectPage) { + if (needNotifyChildEnd) { + callback.onScaleUpdate(detail); +// if(verbose){ debugPrint('$tag child onScaleUpdate'); } + } else { + final bool childCanMove = callback.canMove(detail.scale, delta); + if (childCanMove != null && childCanMove) { + needNotifyChildEnd = true; + callbackStartMap[callback] = childCanMove; + callback.onScaleStart(_scaleStartDetail); +// if(verbose){ debugPrint('$tag child onScaleStart'); } + } else { + pageViewShouldMove = true; + } + } + } + } + + if (pageViewShouldMove) { +// if(verbose){ debugPrint('$tag move pageview 1'); } + movePageViewSelf(detail); + } + } else { +// if(verbose){ debugPrint('$tag move pageview 2'); } + movePageViewSelf(detail); + } + } + + //scroll to next or pre page after finger up + void animateToPage(ScrollPositionWithSingleContext scrollPosition) { + final tail = pageViewController.page - pageViewController.page.floor(); + if (_lastDelta == null) { + return; + } + + if (_lastDelta.dx > 0) { + //finger move right + if (tail >= 0.5) { + pageViewController.animateToPage(pageViewController.page.ceil(), + curve: Curves.decelerate, + duration: Duration(milliseconds: durationMs)); + } else { + pageViewController + ..animateToPage(pageViewController.page.floor(), + curve: Curves.decelerate, + duration: Duration(milliseconds: durationMs)); + } + } else { + //finger move left + if (tail >= 0.5) { + pageViewController + ..animateToPage(pageViewController.page.ceil(), + curve: Curves.decelerate, + duration: Duration(milliseconds: durationMs)); + } else { + pageViewController + ..animateToPage(pageViewController.page.floor(), + curve: Curves.decelerate, + duration: Duration(milliseconds: durationMs)); + } + } + } + + void onScrollUpdate(double pixels) { + _freshScrollPixels = pixels; + } + + void movePageViewSelf(ScaleUpdateDetails detail) { + final ScrollPositionWithSingleContext scrollPosition = + pageViewController.position; + if(verbose){ + debugPrint('$tag$hashCode movePageViewSelf _startPosition $_startPosition'); + } + final Offset delta = detail.focalPoint - _startPosition; + final value = _lastScrollPixels - delta.dx; + _lastDelta = delta; + scrollPosition.jumpToWithoutSettling( + value.clamp(-0.0, scrollPosition.maxScrollExtent)); + } + + @override + bool canMove(double scale, Offset delta) { + return true; + } +} + +///dispatch onPageChanged to listeners +class OnPageChangedWrapper { + final HashSet> _listeners = HashSet(); + + void addListener(ValueChanged listener) { + _listeners.add(listener); + } + + void removeListener(ValueChanged listener) { + _listeners.remove(listener); + } + + void onPageChanged(int value) { + for (final listener in _listeners) { + listener(value); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index a273c19f..509937e0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,7 +35,7 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.4" charcode: dependency: transitive description: