diff --git a/example/android/build.gradle b/example/android/build.gradle index 3100ad2..59fcef7 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() jcenter() diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146..51e7468 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.3-all.zip diff --git a/example/lib/main.dart b/example/lib/main.dart index 1fbfb1f..7429e84 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -27,6 +27,7 @@ class CarouselDemo extends StatelessWidget { routes: { '/': (ctx) => CarouselDemoHome(), '/basic': (ctx) => BasicDemo(), + '/basicbutton': (ctx) => BasicWithButtonDemo(), '/nocenter': (ctx) => NoCenterDemo(), '/image': (ctx) => ImageSliderDemo(), '/complicated': (ctx) => ComplicatedImageDemo(), @@ -82,6 +83,7 @@ class CarouselDemoHome extends StatelessWidget { body: ListView( children: [ DemoItem('Basic demo', '/basic'), + DemoItem('Basic with button demo', '/basicbutton'), DemoItem('No center mode demo', '/nocenter'), DemoItem('Image carousel slider', '/image'), DemoItem('More complicated image slider', '/complicated'), @@ -122,6 +124,29 @@ class BasicDemo extends StatelessWidget { } } +class BasicWithButtonDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + List list = [1, 2, 3, 4, 5]; + return Scaffold( + appBar: AppBar(title: Text('Basic demo')), + body: Container( + child: CarouselSlider( + options: CarouselOptions( + withButtons: true, + enableInfiniteScroll: false, + ), + items: list + .map((item) => Container( + child: Center(child: Text(item.toString())), + color: Colors.green, + )) + .toList(), + )), + ); + } +} + class NoCenterDemo extends StatelessWidget { @override Widget build(BuildContext context) { @@ -217,6 +242,7 @@ class ComplicatedImageDemo extends StatelessWidget { autoPlay: true, aspectRatio: 2.0, enlargeCenterPage: true, + withButtons: true, ), items: imageSliders, ), @@ -269,7 +295,10 @@ class _ManuallyControlledSliderState extends State { children: [ CarouselSlider( items: imageSliders, - options: CarouselOptions(enlargeCenterPage: true, height: 200), + options: CarouselOptions( + enlargeCenterPage: true, + height: 200, + ), carouselController: _controller, ), Row( diff --git a/lib/carousel_options.dart b/lib/carousel_options.dart index db20ed5..fe5e672 100644 --- a/lib/carousel_options.dart +++ b/lib/carousel_options.dart @@ -124,6 +124,12 @@ class CarouselOptions { /// Exposed clipBehavior of PageView final Clip clipBehavior; + /// Whether to show previous/next buttons on the side of + /// the carousels or not. + /// + /// Defaults to false. + final bool withButtons; + CarouselOptions({ this.height, this.aspectRatio: 16 / 9, @@ -132,6 +138,7 @@ class CarouselOptions { this.enableInfiniteScroll: true, this.reverse: false, this.autoPlay: false, + this.withButtons: false, this.autoPlayInterval: const Duration(seconds: 4), this.autoPlayAnimationDuration = const Duration(milliseconds: 800), this.autoPlayCurve: Curves.fastOutSlowIn, @@ -161,6 +168,7 @@ class CarouselOptions { bool? enableInfiniteScroll, bool? reverse, bool? autoPlay, + bool? withButtons, Duration? autoPlayInterval, Duration? autoPlayAnimationDuration, Curve? autoPlayCurve, @@ -186,6 +194,7 @@ class CarouselOptions { enableInfiniteScroll: enableInfiniteScroll ?? this.enableInfiniteScroll, reverse: reverse ?? this.reverse, autoPlay: autoPlay ?? this.autoPlay, + withButtons: withButtons ?? this.withButtons, autoPlayInterval: autoPlayInterval ?? this.autoPlayInterval, autoPlayAnimationDuration: autoPlayAnimationDuration ?? this.autoPlayAnimationDuration, diff --git a/lib/carousel_slider.dart b/lib/carousel_slider.dart index c2f2199..4026765 100644 --- a/lib/carousel_slider.dart +++ b/lib/carousel_slider.dart @@ -73,6 +73,8 @@ class CarouselSliderState extends State PageController? pageController; + ValueNotifier _currentPageNotifier = ValueNotifier(0); + /// mode is related to why the page is being changed CarouselPageChangedReason mode = CarouselPageChangedReason.controller; @@ -219,6 +221,97 @@ class CarouselSliderState extends State ); } + Widget getButtonWrapper({ + required Widget child, + }) { + if (!widget.options.withButtons) { + return child; + } + + return ValueListenableBuilder( + valueListenable: _currentPageNotifier, + builder: (context, currentPage, _) { + final itemCount = widget.itemCount ?? widget.items!.length; + final nextPage = currentPage < (itemCount - 1) + ? currentPage + 1 + : currentPage; + + final enableInfiniteScroll = widget.options.enableInfiniteScroll; + + final hasPrevious = currentPage > 0 || enableInfiniteScroll; + final hasNext = nextPage != currentPage || enableInfiniteScroll; + + final isHorizontal = options.scrollDirection == Axis.horizontal; + + return Stack( + children: [ + child, + Positioned( + top: 0, + bottom: isHorizontal ? 0 : null, + left: isHorizontal ? 16 : 0, + right: isHorizontal ? null : 0, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 250), + child: hasPrevious + ? InkWell( + onTap: () => carouselController.previousPage(), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + padding: EdgeInsets.all(8), + child: Center( + child: Icon( + isHorizontal + ? Icons.chevron_left + : Icons.keyboard_arrow_up, + size: 24, + color: Colors.black, + ), + ), + ), + ) + : null, + ), + ), + Positioned( + top: isHorizontal ? 0 : null, + bottom: 0, + right: isHorizontal ? 16 : 0, + left: isHorizontal ? null : 0, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 250), + child: hasNext + ? InkWell( + onTap: () => carouselController.nextPage(), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + padding: EdgeInsets.all(8), + child: Center( + child: Icon( + isHorizontal + ? Icons.chevron_right + : Icons.keyboard_arrow_down, + size: 24, + color: Colors.black, + ), + ), + ), + ) + : null, + ), + ), + ], + ); + }, + ); + } + Widget getCenterWrapper(Widget child) { if (widget.options.disableCenter) { return Container( @@ -260,95 +353,99 @@ class CarouselSliderState extends State void dispose() { super.dispose(); clearTimer(); + _currentPageNotifier.dispose(); } @override Widget build(BuildContext context) { - return getGestureWrapper(PageView.builder( - padEnds: widget.options.padEnds, - scrollBehavior: ScrollConfiguration.of(context).copyWith( - scrollbars: false, - overscroll: false, - dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, - ), - clipBehavior: widget.options.clipBehavior, - physics: widget.options.scrollPhysics, - scrollDirection: widget.options.scrollDirection, - pageSnapping: widget.options.pageSnapping, - controller: carouselState!.pageController, - reverse: widget.options.reverse, - itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount, - key: widget.options.pageViewKey, - onPageChanged: (int index) { - int currentPage = getRealIndex(index + carouselState!.initialPage, - carouselState!.realPage, widget.itemCount); - if (widget.options.onPageChanged != null) { - widget.options.onPageChanged!(currentPage, mode); - } - }, - itemBuilder: (BuildContext context, int idx) { - final int index = getRealIndex(idx + carouselState!.initialPage, - carouselState!.realPage, widget.itemCount); - - return AnimatedBuilder( - animation: carouselState!.pageController!, - child: (widget.items != null) - ? (widget.items!.length > 0 ? widget.items![index] : Container()) - : widget.itemBuilder!(context, index, idx), - builder: (BuildContext context, child) { - double distortionValue = 1.0; - // if `enlargeCenterPage` is true, we must calculate the carousel item's height - // to display the visual effect - - if (widget.options.enlargeCenterPage != null && - widget.options.enlargeCenterPage == true) { - // pageController.page can only be accessed after the first build, - // so in the first build we calculate the itemoffset manually - double itemOffset = 0; - var position = carouselState?.pageController?.position; - if (position != null && - position.hasPixels && - position.hasContentDimensions) { - var _page = carouselState?.pageController?.page; - if (_page != null) { - itemOffset = _page - idx; - } - } else { - BuildContext storageContext = carouselState! - .pageController!.position.context.storageContext; - final double? previousSavedPosition = + return getButtonWrapper( + child: getGestureWrapper(PageView.builder( + padEnds: widget.options.padEnds, + scrollBehavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + overscroll: false, + dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, + ), + clipBehavior: widget.options.clipBehavior, + physics: widget.options.scrollPhysics, + scrollDirection: widget.options.scrollDirection, + pageSnapping: widget.options.pageSnapping, + controller: carouselState!.pageController, + reverse: widget.options.reverse, + itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount, + key: widget.options.pageViewKey, + onPageChanged: (int index) { + int currentPage = getRealIndex(index + carouselState!.initialPage, + carouselState!.realPage, widget.itemCount); + if (widget.options.onPageChanged != null) { + widget.options.onPageChanged!(currentPage, mode); + } + _currentPageNotifier.value = index; + }, + itemBuilder: (BuildContext context, int idx) { + final int index = getRealIndex(idx + carouselState!.initialPage, + carouselState!.realPage, widget.itemCount); + + return AnimatedBuilder( + animation: carouselState!.pageController!, + child: (widget.items != null) + ? (widget.items!.length > 0 ? widget.items![index] : Container()) + : widget.itemBuilder!(context, index, idx), + builder: (BuildContext context, child) { + double distortionValue = 1.0; + // if `enlargeCenterPage` is true, we must calculate the carousel item's height + // to display the visual effect + + if (widget.options.enlargeCenterPage != null && + widget.options.enlargeCenterPage == true) { + // pageController.page can only be accessed after the first build, + // so in the first build we calculate the itemoffset manually + double itemOffset = 0; + var position = carouselState?.pageController?.position; + if (position != null && + position.hasPixels && + position.hasContentDimensions) { + var _page = carouselState?.pageController?.page; + if (_page != null) { + itemOffset = _page - idx; + } + } else { + BuildContext storageContext = carouselState! + .pageController!.position.context.storageContext; + final double? previousSavedPosition = PageStorage.of(storageContext)?.readState(storageContext) - as double?; - if (previousSavedPosition != null) { - itemOffset = previousSavedPosition - idx.toDouble(); - } else { - itemOffset = - carouselState!.realPage.toDouble() - idx.toDouble(); + as double?; + if (previousSavedPosition != null) { + itemOffset = previousSavedPosition - idx.toDouble(); + } else { + itemOffset = + carouselState!.realPage.toDouble() - idx.toDouble(); + } + } + + final num distortionRatio = + (1 - (itemOffset.abs() * 0.3)).clamp(0.0, 1.0); + distortionValue = + Curves.easeOut.transform(distortionRatio as double); } - } - final num distortionRatio = - (1 - (itemOffset.abs() * 0.3)).clamp(0.0, 1.0); - distortionValue = - Curves.easeOut.transform(distortionRatio as double); - } + final double height = widget.options.height ?? + MediaQuery.of(context).size.width * + (1 / widget.options.aspectRatio); - final double height = widget.options.height ?? - MediaQuery.of(context).size.width * - (1 / widget.options.aspectRatio); - - if (widget.options.scrollDirection == Axis.horizontal) { - return getCenterWrapper(getEnlargeWrapper(child, - height: distortionValue * height, scale: distortionValue)); - } else { - return getCenterWrapper(getEnlargeWrapper(child, - width: distortionValue * MediaQuery.of(context).size.width, - scale: distortionValue)); - } + if (widget.options.scrollDirection == Axis.horizontal) { + return getCenterWrapper(getEnlargeWrapper(child, + height: distortionValue * height, scale: distortionValue)); + } else { + return getCenterWrapper(getEnlargeWrapper(child, + width: distortionValue * MediaQuery.of(context).size.width, + scale: distortionValue)); + } + }, + ); }, - ); - }, - )); + )), + ); } }