Finished the carousel widget and added it to the gallery screen as navigation

This commit is contained in:
Mariano Uvalle 2019-04-15 16:55:37 -05:00
parent cce2b668c0
commit 33eab452e0
4 changed files with 206 additions and 141 deletions

View file

@ -304,7 +304,7 @@
); );
inputPaths = ( inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${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"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (

View file

@ -183,6 +183,7 @@ class _EventScreenState extends State<EventScreen>
cacheStream: bloc.images, cacheStream: bloc.images,
pathsStream: bloc.imagesPaths, pathsStream: bloc.imagesPaths,
initialScreen: imageIndex, initialScreen: imageIndex,
thumbnailCaceStream: bloc.thumbnails,
); );
}, },
), ),

View file

@ -6,6 +6,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import '../widgets/async_image.dart'; import '../widgets/async_image.dart';
import '../widgets/async_thumbnail.dart';
import '../widgets/carousel.dart';
import '../widgets/fractionally_screen_sized_box.dart'; import '../widgets/fractionally_screen_sized_box.dart';
import '../widgets/loading_indicator.dart'; import '../widgets/loading_indicator.dart';
@ -14,11 +16,15 @@ import '../widgets/loading_indicator.dart';
class GalleryScreen extends StatelessWidget { class GalleryScreen extends StatelessWidget {
/// An observable that emits a list of all the paths of the images the gallery /// An observable that emits a list of all the paths of the images the gallery
/// will show. /// will show.
final Observable<List<String>> pathsStream; final ValueObservable<List<String>> pathsStream;
/// An observable that emits a map that caches the files corresponding to the /// An observable that emits a map that caches the files corresponding to the
/// images to be shown. /// images to be shown.
final Observable<Map<String, Future<File>>> cacheStream; final ValueObservable<Map<String, Future<File>>> cacheStream;
/// An observable that emits a map that caches the files corresponding to the
/// thumbnails of the images to be shown.
final ValueObservable<Map<String, Future<File>>> thumbnailCaceStream;
/// A function to be called when an image needs to be fetched. /// A function to be called when an image needs to be fetched.
final Function(String) fetchImage; final Function(String) fetchImage;
@ -40,9 +46,11 @@ class GalleryScreen extends StatelessWidget {
@required this.cacheStream, @required this.cacheStream,
this.initialScreen = 0, this.initialScreen = 0,
@required this.fetchImage, @required this.fetchImage,
@required this.thumbnailCaceStream,
}) : assert(pathsStream != null), }) : assert(pathsStream != null),
assert(cacheStream != null), assert(cacheStream != null),
assert(fetchImage != null), assert(fetchImage != null),
assert(thumbnailCaceStream != null),
_controller = PageController( _controller = PageController(
initialPage: initialScreen, initialPage: initialScreen,
keepPage: false, keepPage: false,
@ -80,29 +88,21 @@ class GalleryScreen extends StatelessWidget {
}, },
), ),
buildCloseButton(context), buildCloseButton(context),
buildPageNavigation(), buildCarousel(),
], ],
), ),
); );
} }
/// Jumps to the specified page. /// Jumps to the specified page.
void animateToPage(Page page) { void animateToPage(int index) {
final curve = Curves.easeInOut; final curve = Curves.easeInOut;
final duration = Duration(milliseconds: 300); final duration = Duration(milliseconds: 300);
switch (page) { _controller.animateToPage(
case Page.previous: index,
_controller.previousPage( curve: curve,
curve: curve, duration: duration,
duration: duration, );
);
break;
case Page.next:
_controller.nextPage(
curve: curve,
duration: duration,
);
}
} }
/// Builds the button that closes this screen. /// 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 Widget buildCarousel() {
// the whatsapp one. return StreamBuilder(
/// Builds the navigation controls. stream: thumbnailCaceStream,
/// builder: (context, AsyncSnapshot<Map<String, Future<File>>> snap) {
/// Two icon buttons allow the user to navigate to the next or previous page. Widget carousel = Container();
Widget buildPageNavigation() {
return Positioned( if (snap.hasData) {
bottom: 50, final cache = snap.data;
child: FractionallyScreenSizedBox( List<Widget> carouselChildren = [];
widthFactor: 1,
child: Row( cache.keys.forEach(
children: <Widget>[ (key) {
Spacer(), carouselChildren.add(
IconButton( AsyncThumbnail(
onPressed: () => animateToPage(Page.previous), cacheId: key,
icon: Icon( cacheStream: thumbnailCaceStream,
FontAwesomeIcons.caretLeft, ),
size: 40, );
), },
);
carousel = Carousel(
onChanged: (index) => animateToPage(index),
itemCount: cache.length,
initialItem: initialScreen,
children: carouselChildren,
);
}
return Positioned(
bottom: 20,
child: FractionallyScreenSizedBox(
widthFactor: 1,
child: Column(
children: <Widget>[
carousel,
],
), ),
Spacer( ),
flex: 8, );
), },
IconButton(
onPressed: () => animateToPage(Page.next),
icon: Icon(
FontAwesomeIcons.caretRight,
size: 40,
),
),
Spacer(),
],
),
),
); );
} }

View file

@ -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 { 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<Widget> 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(); _CarouselState createState() => _CarouselState();
} }
class _CarouselState extends State<Carousel> { class _CarouselState extends State<Carousel> {
double _offset; /// Spacing necessary to show the first and last items on the center of the
static const kDefaultItemSize = 100.0; /// screen.
static const kDefaultSeparatorSize = 10.0; double _edgeSpacing;
ScrollController _controller = ScrollController();
int _currentWidgetIndex = 0; /// 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<int> _widgetIndex;
/// The current subscription to the BehaviorSubject.
StreamSubscription<int> _subscription;
initState() { initState() {
super.initState(); super.initState();
_controller.addListener(displayCurrentWidget); _spaceBetweenItems = widget.itemSize + widget.spacing;
_widgetIndex = BehaviorSubject<int>(seedValue: widget.initialItem);
_controller = ScrollController(
initialScrollOffset: getOffsetFromIndex(widget.initialItem),
);
_controller.addListener(updateCurrentWidgetIndex);
} }
void didUpdateWidget(Carousel oldWidget) {
super.didUpdateWidget(oldWidget);
updateSubscription();
}
Future<void> 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) { void scrollToItem(int itemIndex) {
final position = (kDefaultSeparatorSize + kDefaultItemSize) * itemIndex; final position = _spaceBetweenItems * itemIndex;
_controller.animateTo( _controller.animateTo(
position, position,
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -24,99 +113,67 @@ class _CarouselState extends State<Carousel> {
); );
} }
void displayCurrentWidget() { /// Gets the offset from the left of the screen necessary to center the item
final currentPosition = _controller.offset; /// on [index] on the middle of the screen.
int newWidgetIndex; double getOffsetFromIndex(int index) => _spaceBetweenItems * index;
if (currentPosition < 0) {
newWidgetIndex = 0; /// 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 { } else {
final distanceFromLower = // get the distance to the closest multiple of the
currentPosition % (kDefaultSeparatorSize + kDefaultItemSize); final distanceFromLower = scrollOffset % _spaceBetweenItems;
if (distanceFromLower <= (kDefaultSeparatorSize + kDefaultItemSize) / 2) { widgetIndex = scrollOffset ~/ _spaceBetweenItems;
newWidgetIndex = if (distanceFromLower > _spaceBetweenItems / 2) {
currentPosition ~/ (kDefaultSeparatorSize + kDefaultItemSize); widgetIndex += 1;
} else {
newWidgetIndex =
currentPosition ~/ (kDefaultSeparatorSize + kDefaultItemSize) + 1;
} }
} }
if (newWidgetIndex != _currentWidgetIndex) { return widgetIndex;
_currentWidgetIndex = newWidgetIndex;
print(_currentWidgetIndex);
}
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
_offset = width / 2 - 50; _edgeSpacing = width / 2 - 50;
List<Widget> 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( return Container(
height: 100.0, height: widget.itemSize,
child: ListView( child: ListView(
controller: _controller, controller: _controller,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: <Widget>[ children: listChildren,
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),
],
), ),
); );
} }