From e14b734b95a66a795f529c0d38af0c8c3e91c996 Mon Sep 17 00:00:00 2001 From: hongduc6 Date: Tue, 11 Apr 2023 14:49:51 +0700 Subject: [PATCH] Update documentation & version --- CHANGELOG.md | 8 +- README.md | 261 +++++++++++++++------------------ example/lib/main.dart | 2 +- lib/stacked_list_carousel.dart | 52 ++++--- pubspec.yaml | 2 +- 5 files changed, 160 insertions(+), 165 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65da652..3fa8a64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,10 @@ ## [0.0.3] * Reduced SDK constraints (>= 2.12.0) * Added outermost item discarded effect and notify discarded swipe direction -* Improve performance \ No newline at end of file +* Improve performance + +## [1.0.0] +* Added consume mode, which allow cards to disappear after swiped. +* Take a custom StackedListController as a param for StackedListCarousel constructor. +* More smooth and clean animation. +* Allow user to customize outermost card and inner cards decoration. \ No newline at end of file diff --git a/README.md b/README.md index e092da9..2ce2f64 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,20 @@ # stacked_list_carousel -**This package allows you to create a carousel of stacked items that is highly customizable and interactive. It is particularly well-suited for specific use cases, such as in-app banners.** +**This package allows you to create a carousel of stacked items that is highly customizable and interactive. It is particularly well-suited for specific use cases, such as in-app banners. Support 2 carousel behavior: loop and consume.** -Demo +### Loop mode +The item won't be removed from items list after being swiped and will be inserted to last position. It also implements auto slide in a exact set duration. -## Features -* Support customize widget's items builder which provides variety attributes. The itemBuilder provides displayed size of built target, its index inside item list and whether built target is outermost displayed widget. - -* Allow config fixed aspect ratio of display items. If cardAspectRatio is not provided, the rendered view will occupy all remain space. +Demo - * 4 x 3 - Demo +### Consume mode +The item will be removed from items list after being swiped. The auto slide feature is not enabled in this mode. Also, an empty builder must be provided to build empty UI when all cards are consumed. - * 16 x 9 - Demo +Demo -* Provide item discard listener, which provides its information and discarded swipe direction. +## Features +* Create a widget carousel which align its children vertically with well-animated and smooth transition animation. +* Provide item discard action listener, which provides its information and discarded swipe direction. ## Documentation @@ -29,6 +28,29 @@ dependencies: stacked_list_carousel: ``` + +### Attributes + +| **Name** | **Type** | **Notes** | **Description** | +|:------------------------------:|:---------------------------------:|:------------------------------------------------------------------------------------------------:|:--------------------------------------------------------:| +| items | List | | List of card models with T type | +| cardBuilder | Function(BuildContext,T,Size) | - second params is built card model - third params are rendered card size | Card widget builder function | +| behavior | CarouselBehavior | enum: loop / consume | Config carousel transition behavior | +| outermostCardWrapper | Widget Function(Widget)? | wrap built outermost card with another widget | Avoid using complicated build functions if possible | +| innerCardsWrapper | Widget Function(Widget)? | wrap built inner cards with another widget | -- | +| cardSwipedCallback | void Function(T, SwipeDirection)? | - first param is swiped card model - second param is swiped direction when the card is discarded | Notify card discarded | +| cardAspectRatio | double? | | The width / height ratio of each card | +| controller | StackedListController? | | Provide a custom StackedListController | +| emptyBuilder | WidgetBuilder? | | Must be provided in consume mode | +| alignment | StackedListAxisAlignment | enum: top / bottom | Aligns card vertically top or bottom | +| outermostCardHeightFactor | double | Must be lower than 1 | The ratio between outermost card height / view height | +| animationDuration | Duration | | Transition animation duration | +| transitionCurve | Curve | | Defaults to Curves.easeIn | +| maxDisplayedItemCount | int | | Config max amount of displayed card | +| itemGapHeightFactor | double | outermostCardHeightFactor + (maxDisplayedItemCount - 1) * itemGapHeightFactor <= 1 | The height factor of gap between cards and view height | +| autoSlideDuration | Duration | | Config auto slide duration in loop mode | +| outermostCardAnimationDuration | Duration | | The duration of outermost card's flying effect animation | + ### Implements Import the package: @@ -40,145 +62,104 @@ import 'package:stacked_list_carousel/stacked_list_carousel.dart'; Then use StackedCardCarousel widget ``` -import 'package:flutter/material.dart'; -import 'package:stacked_list_carousel/stacked_list_carousel.dart'; - -void main() => runApp(const MyApp()); - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Stacked Cards Example', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const Home(title: 'Awesome Card Carousel'), - ); - } -} - -class Home extends StatefulWidget { - const Home({Key? key, required this.title}) : super(key: key); - final String title; - - @override - State createState() => _HomeState(); -} - -class _HomeState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey, - body: StackedListCarousel( +StackedListCarousel( items: banners, - // Highly customizable builder function which actual widget's size, - // its index inside item list, and whether built item is outermost - itemBuilder: (context, size, index, isOutermost) => ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Stack( - children: [ - Image.network( - banners[index].imgUrl, - width: size.width, - height: size.height, - ), - Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - banners[index].title, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - fontSize: - 30.0 * size.width / MediaQuery.of(context).size.width, + behavior: CarouselBehavior.consume, + // A widget builder callback to build cards with context, card model + // and its size attributes. + cardBuilder: (context, item, size) { + return ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: Stack( + children: [ + Image.network( + item.imgUrl, + width: size.width, + height: size.height, + fit: BoxFit.cover, + ), + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + item.title, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 30.0 * + size.width / + MediaQuery.of(context).size.width, + ), ), ), ), - ), - if (!isOutermost) - SizedBox.expand( - child: Container(color: Colors.grey.withOpacity(0.65)), - ) - ], - ), - ), - // Config card's aspect ratio - cardAspectRatio: 2 / 3, + ], + ), + ); + }, + // You can config fixed card width / height ratio. For default, the ratio + // is view size width / height + // cardAspectRatio: 0.75, // Config outermost card height factor relative to view height - outermostCardHeightFactor: 0.7, + outermostCardHeightFactor: 0.8, + // Gap height factor relative to view height + itemGapHeightFactor: 0.05, // Config max item displayed count - maxDisplayedItemsCount: 3, - // Config view size height factor relative to view height - viewSizeHeightFactor: 0.85, - // Config animation transitions duration - autoSlideDuration: const Duration(seconds: 4), - transitionDuration: const Duration(milliseconds: 250), - outermostTransitionDuration: const Duration(milliseconds: 200), - // You can listen for discarded item and its swipe direction - onItemDiscarded: (index, direction) { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'banner ${banners[index].title} discarded in $direction direction!'), + maxDisplayedItemCount: 3, + // You can config transition duration here (the animation between cards + // swap). Defaults to 450 milliseconds + animationDuration: const Duration(milliseconds: 550), + // You can config auto slide duration. This only works in loop mode. + autoSlideDuration: const Duration(seconds: 8), + // Define cards align + alignment: StackedListAxisAlignment.bottom, + // In consume mode, you must declare the empty builder, which will be + // built when user swiped all cards. + emptyBuilder: (context) => const Center( + child: Text('You have consumed all cards!'), + ), + // You can customize inner cards wrapper builder. For example, you want to + // shade the unready cards, just wrap it with a gray decorated box. + innerCardsWrapper: (child) { + return Stack( + children: [ + child, + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + color: const Color(0xffA6A3CC).withOpacity(0.64), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ); + }, + // You can also customize outermost card builder for some special effects. + outermostCardWrapper: (child) { + return DecoratedBox( + decoration: const BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.blueAccent, + blurRadius: 12, + blurStyle: BlurStyle.normal, + spreadRadius: 6, + ), + ], ), + child: child, ); }, + // When implementing use case like tinder card swipe, + // you'll wish to know what swipe behavior user did. + // This callback will provide the discard direction of + // corresponding item for you. + cardSwipedCallback: (item, direction) { + debugPrint('card swiped: ${item.title}, $direction'); + }, ), - ); - } -} - -class AwesomeInAppBanner { - final String imgUrl; - final String title; - final Color color; - - const AwesomeInAppBanner( - this.imgUrl, - this.title, - this.color, - ); -} - -List banners = [ - AwesomeInAppBanner( - 'https://picsum.photos/id/100/600/900', - 'My awesome banner 1', - Colors.green.shade300, - ), - AwesomeInAppBanner( - 'https://picsum.photos/id/200/600/900', - 'My awesome banner 2', - Colors.red.shade300, - ), - AwesomeInAppBanner( - 'https://picsum.photos/id/300/600/900', - 'My awesome banner 3', - Colors.purple.shade300, - ), - AwesomeInAppBanner( - 'https://picsum.photos/id/400/600/900', - 'My awesome banner 4', - Colors.yellow.shade300, - ), - AwesomeInAppBanner( - 'https://picsum.photos/id/500/600/900', - 'My awesome banner 5', - Colors.blue.shade300, - ), - AwesomeInAppBanner( - 'https://picsum.photos/id/600/600/900', - 'My awesome banner 6', - Colors.orange.shade300, - ), -]; ``` ### Contribution diff --git a/example/lib/main.dart b/example/lib/main.dart index 00d67ee..0ee99bd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -41,7 +41,7 @@ class _HomeState extends State { ), body: StackedListCarousel( items: banners, - behavior: CarouselBehavior.loop, + behavior: CarouselBehavior.consume, // A widget builder callback to build cards with context, card model // and its size attributes. cardBuilder: (context, item, size) { diff --git a/lib/stacked_list_carousel.dart b/lib/stacked_list_carousel.dart index c038ffc..1493b92 100644 --- a/lib/stacked_list_carousel.dart +++ b/lib/stacked_list_carousel.dart @@ -14,12 +14,10 @@ typedef SizedWidgetBuilder = Widget Function(BuildContext, T, Size); typedef WrapperBuilder = Widget Function(Widget); class StackedListCarousel extends StatefulWidget { - StackedListCarousel({ + const StackedListCarousel({ required this.items, required this.cardBuilder, required this.behavior, - WrapperBuilder? innerCardsWrapper, - WrapperBuilder? outermostCardWrapper, this.cardSwipedCallback, this.cardAspectRatio, this.controller, @@ -32,6 +30,8 @@ class StackedListCarousel extends StatefulWidget { this.itemGapHeightFactor = 0.05, this.autoSlideDuration = const Duration(seconds: 5), this.outermostCardAnimationDuration = const Duration(milliseconds: 450), + this.innerCardsWrapper, + this.outermostCardWrapper, Key? key, }) : assert( behavior != CarouselBehavior.consume || emptyBuilder != null, @@ -48,10 +48,7 @@ class StackedListCarousel extends StatefulWidget { 'Not enough space. The total height of outermost card and gaps must ' 'be lower than 1', ), - super(key: key) { - this.outermostCardWrapper = outermostCardWrapper ?? (c) => c; - this.innerCardsWrapper = innerCardsWrapper ?? (c) => c; - } + super(key: key); /// A list of [T] items which used to render cards. final List items; @@ -64,10 +61,10 @@ class StackedListCarousel extends StatefulWidget { final CarouselBehavior behavior; /// A widget builder which helps you customize outermost card. - late final WrapperBuilder outermostCardWrapper; + final WrapperBuilder? outermostCardWrapper; /// A widget builder which helps you customize inner cards. - late final WrapperBuilder innerCardsWrapper; + final WrapperBuilder? innerCardsWrapper; /// Notify card discarded callback. It provides discarded item and discarded /// quarter direction @@ -138,6 +135,9 @@ class _StackedListCarouselState extends State> Size viewSize = Size.zero; + bool get hasInnerWrapper => widget.innerCardsWrapper != null; + bool get hasOutermostWrapper => widget.outermostCardWrapper != null; + @override void initState() { super.initState(); @@ -371,6 +371,10 @@ class _StackedListCarouselState extends State> Size size, Alignment scaleAlignment, ) { + final child = cards[(reorderCardsCount + controller.swapCount + 1) % + controller.itemCount] ?? + const SizedBox.shrink(); + return AnimatedBuilder( animation: innermostCardMarginAnimation, builder: (context, child) => _alignPositioned( @@ -384,11 +388,7 @@ class _StackedListCarouselState extends State> child: child, ), ), - child: widget.innerCardsWrapper.call( - cards[(reorderCardsCount + controller.swapCount + 1) % - controller.itemCount] ?? - const SizedBox.shrink(), - ), + child: hasInnerWrapper ? widget.innerCardsWrapper!.call(child) : child, ); } @@ -413,9 +413,13 @@ class _StackedListCarouselState extends State> ? sizeFactorAnimations[i].value : cardsSizeFactors[i], alignment: scaleAlignment, - child: (i == reorderCardsCount - 1 && controller.transitionForwarding) - ? widget.outermostCardWrapper.call(child) - : widget.innerCardsWrapper.call(child), + child: hasInnerWrapper + ? ((i == reorderCardsCount - 1 && controller.transitionForwarding) + ? hasOutermostWrapper + ? widget.outermostCardWrapper!.call(child) + : child + : widget.innerCardsWrapper!.call(child)) + : child, ), ), ); @@ -425,6 +429,9 @@ class _StackedListCarouselState extends State> Size size, Alignment scaleAlignment, ) { + final child = + cards[controller.realOutermostIndex] ?? const SizedBox.shrink(); + return _alignPositioned( margin: cardsMargin.last * size.height, child: ValueListenableBuilder( @@ -448,15 +455,16 @@ class _StackedListCarouselState extends State> ), child: AnimatedBuilder( animation: controller.transitionController, - builder: (context, child) => Transform.scale( + builder: (context, _) => Transform.scale( scale: cardsSizeFactors.last, alignment: scaleAlignment, child: Visibility( visible: !controller.transitionForwarding, - child: widget.outermostCardWrapper.call( - cards[controller.realOutermostIndex] ?? - const SizedBox.shrink(), - ), + child: hasOutermostWrapper + ? widget.outermostCardWrapper!.call( + child, + ) + : child, ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index f68d833..efc2bc6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: stacked_list_carousel description: Interactive carousel layout that arrange items vertically stacked, which most suitable for implementing in-app banners use case. -version: 0.0.3+1 +version: 1.0.0 repository: https://github.com/hongduc6/stacked_list_carousel environment: