diff --git a/example/lib/assets/food.jpeg b/example/lib/assets/food.jpeg new file mode 100644 index 00000000..09f1fcef Binary files /dev/null and b/example/lib/assets/food.jpeg differ diff --git a/example/lib/main.dart b/example/lib/main.dart index a61dfa18..93595921 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -14,6 +14,15 @@ import 'package:ui_kit/position/gf_position.dart'; import 'package:ui_kit/types/gf_type.dart'; import 'package:ui_kit/components/image/gf_image_overlay.dart'; import 'package:ui_kit/shape/gf_shape.dart'; +import 'package:ui_kit/components/slider/gf_slider.dart'; + +final List imageList = [ + "https://cdn.pixabay.com/photo/2017/12/03/18/04/christmas-balls-2995437_960_720.jpg", + "https://cdn.pixabay.com/photo/2017/12/13/00/23/christmas-3015776_960_720.jpg", + "https://cdn.pixabay.com/photo/2019/12/19/10/55/christmas-market-4705877_960_720.jpg", + "https://cdn.pixabay.com/photo/2019/12/20/00/03/road-4707345_960_720.jpg" +]; + void main() => runApp(MyApp()); @@ -51,56 +60,93 @@ class _MyHomePageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - GFCard( - boxFit: BoxFit.cover, - colorFilter: new ColorFilter.mode( - Colors.black.withOpacity(0.67), BlendMode.darken), - image: Image.asset("lib/assets/pizza.jpeg"), -// imageOverlay: AssetImage("lib/assets/pizza.jpeg"), - titlePosition: GFPosition.end, - title: GFTitleBar( - avatar: GFAvatar( - child: Text("tb"), - ), - title: Text( - 'title', - style: TextStyle(color: Colors.grey), - ), - subTitle: Text( - 'subtitle', - style: TextStyle(color: Colors.grey), - ), - icon: GFIconButton( - onPressed: null, - icon: Icon(Icons.favorite_border), - type: GFType.transparent, - ), - ), - content: Text( - "Flutter " - "Flutter is Google's mobile UI framework for crafting" - " high-quality native interfaces on iOS and Android in " - "Flutter ", - style: TextStyle(color: Colors.grey), - ), - buttonBar: GFButtonBar( - mainAxisSize: MainAxisSize.min, - children: [ - GFButton( - onPressed: null, - child: Text("favorite"), - icon: Icon(Icons.favorite_border), - type: GFType.transparent, - ), - GFButton( - onPressed: null, - child: Text("share"), - icon: Icon(Icons.share), - type: GFType.outline, + + GFSlider( + viewportFraction: 0.9, + aspectRatio: 2.0, + autoPlay: true, + enlargeMainPage: true, + items: imageList.map( + (url) { + return Container( + margin: EdgeInsets.all(5.0), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + child: Image.network( + url, + fit: BoxFit.cover, + width: 1000.0, ), - ], - ), - ), + ), + ); + }, + ).toList(), + ), + +// GFSlider( +// autoPlay: true, +// viewportFraction: 1.0, +// aspectRatio: MediaQuery.of(context).size.aspectRatio, +// items: imageList.map((url) { +// return Image.network( +// url, +// fit: BoxFit.cover, +// width: 1000.0, +// ); +// }, +// ).toList(), +// ), + +// GFCard( +// boxFit: BoxFit.cover, +// colorFilter: new ColorFilter.mode( +// Colors.black.withOpacity(0.67), BlendMode.darken), +// image: Image.asset("lib/assets/food.jpeg"), +//// imageOverlay: AssetImage("lib/assets/food.jpeg"), +// titlePosition: GFPosition.end, +// title: GFTitleBar( +// avatar: GFAvatar( +// child: Text("tb"), +// ), +// title: Text( +// 'title', +// style: TextStyle(color: Colors.grey), +// ), +// subTitle: Text( +// 'subtitle', +// style: TextStyle(color: Colors.grey), +// ), +// icon: GFIconButton( +// onPressed: null, +// icon: Icon(Icons.favorite_border), +// type: GFType.transparent, +// ), +// ), +// content: Text( +// "Flutter " +// "Flutter is Google's mobile UI framework for crafting" +// " high-quality native interfaces on iOS and Android in " +// "Flutter ", +// style: TextStyle(color: Colors.grey), +// ), +// buttonBar: GFButtonBar( +// mainAxisSize: MainAxisSize.min, +// children: [ +// GFButton( +// onPressed: null, +// child: Text("favorite"), +// icon: Icon(Icons.favorite_border), +// type: GFType.transparent, +// ), +// GFButton( +// onPressed: null, +// child: Text("share"), +// icon: Icon(Icons.share), +// type: GFType.outline, +// ), +// ], +// ), +// ), // GFButtonBar( // mainAxisSize: MainAxisSize.min, @@ -124,70 +170,70 @@ class _MyHomePageState extends State { // ), // ), - GFImageOverlay( - width: MediaQuery.of(context).size.width, - margin: EdgeInsets.all(16.0), - padding: EdgeInsets.all(16.0), - child: Column( - children: [ - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - new Text( - 'Hello world', - style: TextStyle(color: Colors.white), - ), - ], - ), -// color: Colors.orange, - image: AssetImage("lib/assets/pizza.jpeg"), - boxFit: BoxFit.cover, - colorFilter: new ColorFilter.mode( - Colors.black.withOpacity(0.6), BlendMode.darken), - borderRadius: new BorderRadius.circular(5.0), -// border: Border.all(color: Colors.pink, width: 2.0), - ), +// GFImageOverlay( +// width: MediaQuery.of(context).size.width, +// margin: EdgeInsets.all(16.0), +// padding: EdgeInsets.all(16.0), +// child: Column( +// children: [ +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// new Text( +// 'Hello world', +// style: TextStyle(color: Colors.white), +// ), +// ], +// ), +//// color: Colors.orange, +// image: AssetImage("lib/assets/food.jpeg"), +// boxFit: BoxFit.cover, +// colorFilter: new ColorFilter.mode( +// Colors.black.withOpacity(0.6), BlendMode.darken), +// borderRadius: new BorderRadius.circular(5.0), +//// border: Border.all(color: Colors.pink, width: 2.0), +// ), // // GFAvatar( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 95eca7a1..4ebcc8a4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,4 +1,4 @@ -name: ui_kit_example +name: example description: A new Flutter project. version: 1.0.0+1 @@ -19,4 +19,6 @@ dev_dependencies: sdk: flutter flutter: - uses-material-design: true \ No newline at end of file + uses-material-design: true + assets: + - lib/assets/food.jpeg \ No newline at end of file diff --git a/lib/components/slider/gf_slider.dart b/lib/components/slider/gf_slider.dart new file mode 100644 index 00000000..03db5594 --- /dev/null +++ b/lib/components/slider/gf_slider.dart @@ -0,0 +1,254 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; + +class GFSlider extends StatefulWidget { + GFSlider( + {@required this.items, + this.height, + this.aspectRatio: 16 / 9, + this.viewportFraction: 0.8, + this.initialPage: 0, + int realPage: 10000, + this.enableInfiniteScroll: true, + this.reverse: false, + this.autoPlay: false, + this.autoPlayInterval: const Duration(seconds: 4), + this.autoPlayAnimationDuration = const Duration(milliseconds: 800), + this.autoPlayCurve: Curves.fastOutSlowIn, + this.pauseAutoPlayOnTouch, + this.enlargeMainPage = false, + this.onPageChanged, + this.scrollPhysics, + this.scrollDirection: Axis.horizontal}) + : this.realPage = enableInfiniteScroll ? realPage + initialPage : initialPage, + this.pageController = PageController( + viewportFraction: viewportFraction, + initialPage: enableInfiniteScroll ? realPage + initialPage : initialPage, + ); + + /// The widgets to be shown as sliders. + final List items; + + /// Set slide widget height and overrides any existing [aspectRatio]. + final double height; + + /// Aspect ratio is used if no height have been declared. Defaults to 16:9 aspect ratio. + final double aspectRatio; + + /// The fraction of the viewport that each page should occupy. Defaults to 0.8, which means each page fills 80% of the slide. + final num viewportFraction; + + /// The initial page to show when first creating the [GFSlider]. Defaults to 0. + final num initialPage; + + /// The actual index of the [PageView]. + final num realPage; + + /// Determines if slides should loop infinitely or be limited to item length. Defaults to true, i.e. infinite loop. + final bool enableInfiniteScroll; + + /// Reverse the order of items if set to true. Defaults to false. + final bool reverse; + + /// Enables auto play, sliding one page at a time. Use [autoPlayInterval] to determent the frequency of slides. Defaults to false. + final bool autoPlay; + + /// Sets Duration to determent the frequency of slides when [autoPlay] is set to true. Defaults to 4 seconds. + final Duration autoPlayInterval; + + /// The animation duration between two transitioning pages while in auto playback. Defaults to 800 ms. + final Duration autoPlayAnimationDuration; + + /// Determines the animation curve physics. Defaults to [Curves.fastOutSlowIn]. + final Curve autoPlayCurve; + + /// Sets a timer on touch detected that pause the auto play with the given [Duration]. Touch Detection is only active if [autoPlay] is true. + final Duration pauseAutoPlayOnTouch; + + /// Determines if current page should be larger then the side images, + /// creating a feeling of depth in the carousel. Defaults to false. + final bool enlargeMainPage; + + /// The axis along which the page view scrolls. Defaults to [Axis.horizontal]. + final Axis scrollDirection; + + /// Called whenever the page in the center of the viewport changes. + final Function(int index) onPageChanged; + + /// How the carousel should respond to user input. + /// + /// For example, determines how the items continues to animate after the + /// user stops dragging the page view. + /// + /// The physics are modified to snap to page boundaries using + /// [PageScrollPhysics] prior to being used. + /// + /// Defaults to matching platform conventions. + final ScrollPhysics scrollPhysics; + + /// [pageController] is created using the properties passed to the constructor + /// and can be used to control the [PageView] it is passed to. + final PageController pageController; + + /// Animates the controlled [GFSlider] to the next page. + /// + /// The animation lasts for the given duration and follows the given curve. + /// The returned [Future] resolves when the animation completes. + Future nextPage({Duration duration, Curve curve}) { + return pageController.nextPage(duration: duration, curve: curve); + } + + /// Animates the controlled [GFSlider] to the previous page. + /// + /// The animation lasts for the given duration and follows the given curve. + /// The returned [Future] resolves when the animation completes. + Future previousPage({Duration duration, Curve curve}) { + return pageController.previousPage(duration: duration, curve: curve); + } + + /// Changes which page is displayed in the controlled [GFSlider]. + /// + /// Jumps the page position from its current value to the given value, + /// without animation, and without checking if the new value is in range. + void jumpToPage(int page) { + final index = _getRealIndex(pageController.page.toInt(), realPage, items.length); + return pageController.jumpToPage(pageController.page.toInt() + page - index); + } + + /// Animates the controlled [GFSlider] from the current page to the given page. + /// + /// The animation lasts for the given duration and follows the given curve. + /// The returned [Future] resolves when the animation completes. + Future animateToPage(int page, {Duration duration, Curve curve}) { + final index = _getRealIndex(pageController.page.toInt(), realPage, items.length); + return pageController.animateToPage(pageController.page.toInt() + page - index, + duration: duration, curve: curve); + } + + @override + _GFSliderState createState() => _GFSliderState(); +} + +class _GFSliderState extends State with TickerProviderStateMixin { + Timer timer; + + @override + void initState() { + super.initState(); + timer = getPlayTimer(); + } + + Timer getPlayTimer() { + return Timer.periodic(widget.autoPlayInterval, (_) { + if (widget.autoPlay) { + widget.pageController + .nextPage(duration: widget.autoPlayAnimationDuration, curve: widget.autoPlayCurve); + } + }); + } + + void pauseOnTouch() { + timer.cancel(); + timer = Timer(widget.pauseAutoPlayOnTouch, () { + timer = getPlayTimer(); + }); + } + + Widget getPageWrapper(Widget child) { + if (widget.height != null) { + final Widget wrapper = Container(height: widget.height, child: child); + return widget.autoPlay && widget.pauseAutoPlayOnTouch != null + ? addGestureDetection(wrapper) + : wrapper; + } else { + final Widget wrapper = AspectRatio(aspectRatio: widget.aspectRatio, child: child); + return widget.autoPlay && widget.pauseAutoPlayOnTouch != null + ? addGestureDetection(wrapper) + : wrapper; + } + } + + Widget addGestureDetection(Widget child) => + GestureDetector(onPanDown: (_) => pauseOnTouch(), child: child); + + @override + void dispose() { + super.dispose(); + timer?.cancel(); + } + + @override + Widget build(BuildContext context) { + return getPageWrapper(PageView.builder( + physics: widget.scrollPhysics, + scrollDirection: widget.scrollDirection, + controller: widget.pageController, + reverse: widget.reverse, + itemCount: widget.enableInfiniteScroll ? null : widget.items.length, + onPageChanged: (int index) { + int currentPage = _getRealIndex(index + widget.initialPage, widget.realPage, widget.items.length); + if (widget.onPageChanged != null) { + widget.onPageChanged(currentPage); + } + }, + itemBuilder: (BuildContext context, int i) { + final int index = + _getRealIndex(i + widget.initialPage, widget.realPage, widget.items.length); + + return AnimatedBuilder( + animation: widget.pageController, + child: widget.items[index], + builder: (BuildContext context, child) { + // on the first render, the pageController.page is null, + // this is a dirty hack + if (widget.pageController.position.minScrollExtent == null || + widget.pageController.position.maxScrollExtent == null) { + Future.delayed(Duration(microseconds: 1), () { + setState(() {}); + }); + return Container(); + } + double value = widget.pageController.page - i; + value = (1 - (value.abs() * 0.3)).clamp(0.0, 1.0); + + final double height = + widget.height ?? MediaQuery.of(context).size.width * (1 / widget.aspectRatio); + final double distortionValue = + widget.enlargeMainPage ? Curves.easeOut.transform(value) : 1.0; + + if (widget.scrollDirection == Axis.horizontal) { + return Center(child: SizedBox(height: distortionValue * height, child: child)); + } else { + return Center( + child: SizedBox( + width: distortionValue * MediaQuery.of(context).size.width, child: child)); + } + }, + ); + }, + )); + } +} + +/// Converts an index of a set size to the corresponding index of a collection of another size +/// as if they were circular. +/// +/// Takes a [position] from collection Foo, a [base] from where Foo's index originated +/// and the [length] of a second collection Baa, for which the correlating index is sought. +/// +/// For example; We have a Carousel of 10000(simulating infinity) but only 6 images. +/// We need to repeat the images to give the illusion of a never ending stream. +/// By calling _getRealIndex with position and base we get an offset. +/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image +/// to be placed in the given position. +int _getRealIndex(int position, int base, int length) { + final int offset = position - base; + return _remainder(offset, length); +} + +/// Returns the remainder of the modulo operation [input] % [source], and adjust it for +/// negative values. +int _remainder(int input, int source) { + final int result = input % source; + return result < 0 ? source + result : result; +} \ No newline at end of file diff --git a/lib/components/toggle/gf_toggle.dart b/lib/components/toggle/gf_toggle.dart index 167c2619..60384abb 100644 --- a/lib/components/toggle/gf_toggle.dart +++ b/lib/components/toggle/gf_toggle.dart @@ -86,8 +86,8 @@ class GFToggle extends StatelessWidget { activeColor: activeColor, inactiveThumbColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor, - hoverColor: hoverColor, - focusColor: focusColor, +// hoverColor: hoverColor, +// focusColor: focusColor, onChanged: onChanged,