From 33eab452e0018ed3b54ec024a3440071272243ce Mon Sep 17 00:00:00 2001 From: AYM1607 Date: Mon, 15 Apr 2019 16:55:37 -0500 Subject: [PATCH] Finished the carousel widget and added it to the gallery screen as navigation --- ios/Runner.xcodeproj/project.pbxproj | 2 +- lib/src/screens/event_screen.dart | 1 + lib/src/screens/gallery_screen.dart | 107 ++++++------ lib/src/widgets/carousel.dart | 237 +++++++++++++++++---------- 4 files changed, 206 insertions(+), 141 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b4e6d8f..8f2981f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -304,7 +304,7 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", + "${PODS_ROOT}/../.symlinks/flutter/ios-release/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( diff --git a/lib/src/screens/event_screen.dart b/lib/src/screens/event_screen.dart index e210c05..eebe5c9 100644 --- a/lib/src/screens/event_screen.dart +++ b/lib/src/screens/event_screen.dart @@ -183,6 +183,7 @@ class _EventScreenState extends State cacheStream: bloc.images, pathsStream: bloc.imagesPaths, initialScreen: imageIndex, + thumbnailCaceStream: bloc.thumbnails, ); }, ), diff --git a/lib/src/screens/gallery_screen.dart b/lib/src/screens/gallery_screen.dart index 729fa4e..d2d2048 100644 --- a/lib/src/screens/gallery_screen.dart +++ b/lib/src/screens/gallery_screen.dart @@ -6,6 +6,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:rxdart/rxdart.dart'; import '../widgets/async_image.dart'; +import '../widgets/async_thumbnail.dart'; +import '../widgets/carousel.dart'; import '../widgets/fractionally_screen_sized_box.dart'; import '../widgets/loading_indicator.dart'; @@ -14,11 +16,15 @@ import '../widgets/loading_indicator.dart'; class GalleryScreen extends StatelessWidget { /// An observable that emits a list of all the paths of the images the gallery /// will show. - final Observable> pathsStream; + final ValueObservable> pathsStream; /// An observable that emits a map that caches the files corresponding to the /// images to be shown. - final Observable>> cacheStream; + final ValueObservable>> cacheStream; + + /// An observable that emits a map that caches the files corresponding to the + /// thumbnails of the images to be shown. + final ValueObservable>> thumbnailCaceStream; /// A function to be called when an image needs to be fetched. final Function(String) fetchImage; @@ -40,9 +46,11 @@ class GalleryScreen extends StatelessWidget { @required this.cacheStream, this.initialScreen = 0, @required this.fetchImage, + @required this.thumbnailCaceStream, }) : assert(pathsStream != null), assert(cacheStream != null), assert(fetchImage != null), + assert(thumbnailCaceStream != null), _controller = PageController( initialPage: initialScreen, keepPage: false, @@ -80,29 +88,21 @@ class GalleryScreen extends StatelessWidget { }, ), buildCloseButton(context), - buildPageNavigation(), + buildCarousel(), ], ), ); } /// Jumps to the specified page. - void animateToPage(Page page) { + void animateToPage(int index) { final curve = Curves.easeInOut; final duration = Duration(milliseconds: 300); - switch (page) { - case Page.previous: - _controller.previousPage( - curve: curve, - duration: duration, - ); - break; - case Page.next: - _controller.nextPage( - curve: curve, - duration: duration, - ); - } + _controller.animateToPage( + index, + curve: curve, + duration: duration, + ); } /// Builds the button that closes this screen. @@ -120,40 +120,47 @@ class GalleryScreen extends StatelessWidget { ); } - // TODO: Create a navigation control similar to the one in iOS gallery, or - // the whatsapp one. - /// Builds the navigation controls. - /// - /// Two icon buttons allow the user to navigate to the next or previous page. - Widget buildPageNavigation() { - return Positioned( - bottom: 50, - child: FractionallyScreenSizedBox( - widthFactor: 1, - child: Row( - children: [ - Spacer(), - IconButton( - onPressed: () => animateToPage(Page.previous), - icon: Icon( - FontAwesomeIcons.caretLeft, - size: 40, - ), + Widget buildCarousel() { + return StreamBuilder( + stream: thumbnailCaceStream, + builder: (context, AsyncSnapshot>> snap) { + Widget carousel = Container(); + + if (snap.hasData) { + final cache = snap.data; + List carouselChildren = []; + + cache.keys.forEach( + (key) { + carouselChildren.add( + AsyncThumbnail( + cacheId: key, + cacheStream: thumbnailCaceStream, + ), + ); + }, + ); + + carousel = Carousel( + onChanged: (index) => animateToPage(index), + itemCount: cache.length, + initialItem: initialScreen, + children: carouselChildren, + ); + } + + return Positioned( + bottom: 20, + child: FractionallyScreenSizedBox( + widthFactor: 1, + child: Column( + children: [ + carousel, + ], ), - Spacer( - flex: 8, - ), - IconButton( - onPressed: () => animateToPage(Page.next), - icon: Icon( - FontAwesomeIcons.caretRight, - size: 40, - ), - ), - Spacer(), - ], - ), - ), + ), + ); + }, ); } diff --git a/lib/src/widgets/carousel.dart b/lib/src/widgets/carousel.dart index 1f65a1d..256c093 100644 --- a/lib/src/widgets/carousel.dart +++ b/lib/src/widgets/carousel.dart @@ -1,22 +1,111 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; + +// TODO: Add a builder constructor. to avoid accessing non existing keys in the +// cache map. +// +// If you open the gallery screen without seeing all the items in the media +// screen, the cache will not conatin all the items in the paths list and thus +// the list will call methods on null. + +/// A widget that shows a list of widgets and lets you navigate through them. +/// +/// Touch events on individual items will make the list animate to show that +/// parituclar item in the center of the screen. class Carousel extends StatefulWidget { + /// The default spacing between items. + static const kDefaultSeparatorSize = 10.0; + + /// The default size of the individual items in the carousel. + static const kDefaultItemSize = 100.0; + + /// The size of the individual items to be shown. + final double itemSize; + + /// The spacing between every item. + final double spacing; + + /// The index of the item to be shown in the center. + final int initialItem; + + /// The amount of items to be shown in the carousel. + final int itemCount; + + /// Widgets to be shown in the carousel. + final List children; + + /// Function to the be called when the widget on the center of the screen] + /// changes. + final Function(int) onChanged; + + Carousel({ + this.itemSize = kDefaultItemSize, + this.spacing = kDefaultSeparatorSize, + this.initialItem = 0, + this.itemCount, + @required this.children, + this.onChanged, + }) { + assert(children != null); + assert(children.length > 0); + if (itemCount != null) { + assert(children.length == itemCount); + } + if (initialItem != 0) { + assert(itemCount != null); + assert(initialItem < itemCount); + } + } + _CarouselState createState() => _CarouselState(); } class _CarouselState extends State { - double _offset; - static const kDefaultItemSize = 100.0; - static const kDefaultSeparatorSize = 10.0; - ScrollController _controller = ScrollController(); - int _currentWidgetIndex = 0; + /// Spacing necessary to show the first and last items on the center of the + /// screen. + double _edgeSpacing; + + /// The space between each item of the carousel + double _spaceBetweenItems; + + /// The controller for the underlaying [ListView]. + ScrollController _controller; + + /// Subject of the index of the center widget. + BehaviorSubject _widgetIndex; + + /// The current subscription to the BehaviorSubject. + StreamSubscription _subscription; + initState() { super.initState(); - _controller.addListener(displayCurrentWidget); + _spaceBetweenItems = widget.itemSize + widget.spacing; + _widgetIndex = BehaviorSubject(seedValue: widget.initialItem); + _controller = ScrollController( + initialScrollOffset: getOffsetFromIndex(widget.initialItem), + ); + _controller.addListener(updateCurrentWidgetIndex); } + void didUpdateWidget(Carousel oldWidget) { + super.didUpdateWidget(oldWidget); + updateSubscription(); + } + + Future updateSubscription() async { + if (_subscription != null) { + await _subscription.cancel(); + } + _subscription = _widgetIndex + .debounce(Duration(milliseconds: 400)) + .listen((int index) => widget.onChanged(index)); + } + + /// Moves the item in index [itemIndex] to the center of the screen. void scrollToItem(int itemIndex) { - final position = (kDefaultSeparatorSize + kDefaultItemSize) * itemIndex; + final position = _spaceBetweenItems * itemIndex; _controller.animateTo( position, curve: Curves.easeInOut, @@ -24,99 +113,67 @@ class _CarouselState extends State { ); } - void displayCurrentWidget() { - final currentPosition = _controller.offset; - int newWidgetIndex; - if (currentPosition < 0) { - newWidgetIndex = 0; + /// Gets the offset from the left of the screen necessary to center the item + /// on [index] on the middle of the screen. + double getOffsetFromIndex(int index) => _spaceBetweenItems * index; + + /// Adds a new item to the [_widgetIndex] subject if the the item on the + /// center of the screen changes. + void updateCurrentWidgetIndex() { + int newWidgetIndex = getIndexFromScrollOffset(_controller.offset); + if (newWidgetIndex != _widgetIndex.value) { + _widgetIndex.sink.add(newWidgetIndex); + } + } + + /// Gets the index of the item in the middle of the screen from a given + /// scroll offset. + int getIndexFromScrollOffset(double scrollOffset) { + int widgetIndex; + if (scrollOffset < 0) { + widgetIndex = 0; } else { - final distanceFromLower = - currentPosition % (kDefaultSeparatorSize + kDefaultItemSize); - if (distanceFromLower <= (kDefaultSeparatorSize + kDefaultItemSize) / 2) { - newWidgetIndex = - currentPosition ~/ (kDefaultSeparatorSize + kDefaultItemSize); - } else { - newWidgetIndex = - currentPosition ~/ (kDefaultSeparatorSize + kDefaultItemSize) + 1; + // get the distance to the closest multiple of the + final distanceFromLower = scrollOffset % _spaceBetweenItems; + widgetIndex = scrollOffset ~/ _spaceBetweenItems; + if (distanceFromLower > _spaceBetweenItems / 2) { + widgetIndex += 1; } } - if (newWidgetIndex != _currentWidgetIndex) { - _currentWidgetIndex = newWidgetIndex; - print(_currentWidgetIndex); - } + return widgetIndex; } Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; - _offset = width / 2 - 50; + _edgeSpacing = width / 2 - 50; + + List listChildren = []; + listChildren.add(SizedBox(width: _edgeSpacing)); + + for (int i = 0; i < widget.children.length; i++) { + listChildren.add( + GestureDetector( + onTap: () => scrollToItem(i), + behavior: HitTestBehavior.opaque, + child: Container( + width: widget.itemSize, + child: widget.children[i], + ), + ), + ); + if (i != widget.children.length - 1) { + listChildren.add(SizedBox(width: widget.spacing)); + } + } + + listChildren.add(SizedBox(width: _edgeSpacing)); + return Container( - height: 100.0, + height: widget.itemSize, child: ListView( controller: _controller, scrollDirection: Axis.horizontal, - children: [ - SizedBox(width: _offset), - GestureDetector( - onTap: () => scrollToItem(0), - behavior: HitTestBehavior.opaque, - child: Container( - color: Colors.white, - width: kDefaultItemSize, - child: Center( - child: Text('0'), - ), - ), - ), - SizedBox(width: kDefaultSeparatorSize), - GestureDetector( - onTap: () => scrollToItem(1), - behavior: HitTestBehavior.opaque, - child: Container( - color: Colors.white, - width: kDefaultItemSize, - child: Center( - child: Text('1'), - ), - ), - ), - SizedBox(width: kDefaultSeparatorSize), - GestureDetector( - onTap: () => scrollToItem(2), - behavior: HitTestBehavior.opaque, - child: Container( - color: Colors.white, - width: kDefaultItemSize, - child: Center( - child: Text('2'), - ), - ), - ), - SizedBox(width: kDefaultSeparatorSize), - GestureDetector( - onTap: () => scrollToItem(3), - behavior: HitTestBehavior.opaque, - child: Container( - color: Colors.white, - width: kDefaultItemSize, - child: Center( - child: Text('3'), - ), - ), - ), - SizedBox(width: kDefaultSeparatorSize), - GestureDetector( - onTap: () => scrollToItem(4), - behavior: HitTestBehavior.opaque, - child: Container( - color: Colors.white, - width: kDefaultItemSize, - child: Center( - child: Text('4'), - ), - ), - ), - SizedBox(width: _offset), - ], + children: listChildren, ), ); }