Finished the carousel widget and added it to the gallery screen as navigation
This commit is contained in:
parent
cce2b668c0
commit
33eab452e0
4 changed files with 206 additions and 141 deletions
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ class _EventScreenState extends State<EventScreen>
|
|||
cacheStream: bloc.images,
|
||||
pathsStream: bloc.imagesPaths,
|
||||
initialScreen: imageIndex,
|
||||
thumbnailCaceStream: bloc.thumbnails,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<List<String>> pathsStream;
|
||||
final ValueObservable<List<String>> pathsStream;
|
||||
|
||||
/// An observable that emits a map that caches the files corresponding to the
|
||||
/// 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.
|
||||
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(
|
||||
_controller.animateToPage(
|
||||
index,
|
||||
curve: curve,
|
||||
duration: duration,
|
||||
);
|
||||
break;
|
||||
case Page.next:
|
||||
_controller.nextPage(
|
||||
curve: curve,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the button that closes this screen.
|
||||
|
|
@ -120,41 +120,48 @@ 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() {
|
||||
Widget buildCarousel() {
|
||||
return StreamBuilder(
|
||||
stream: thumbnailCaceStream,
|
||||
builder: (context, AsyncSnapshot<Map<String, Future<File>>> snap) {
|
||||
Widget carousel = Container();
|
||||
|
||||
if (snap.hasData) {
|
||||
final cache = snap.data;
|
||||
List<Widget> 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: 50,
|
||||
bottom: 20,
|
||||
child: FractionallyScreenSizedBox(
|
||||
widthFactor: 1,
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => animateToPage(Page.previous),
|
||||
icon: Icon(
|
||||
FontAwesomeIcons.caretLeft,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
Spacer(
|
||||
flex: 8,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => animateToPage(Page.next),
|
||||
icon: Icon(
|
||||
FontAwesomeIcons.caretRight,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
carousel,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a placeholder for an image.
|
||||
|
|
|
|||
|
|
@ -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<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();
|
||||
}
|
||||
|
||||
class _CarouselState extends State<Carousel> {
|
||||
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<int> _widgetIndex;
|
||||
|
||||
/// The current subscription to the BehaviorSubject.
|
||||
StreamSubscription<int> _subscription;
|
||||
|
||||
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) {
|
||||
final position = (kDefaultSeparatorSize + kDefaultItemSize) * itemIndex;
|
||||
final position = _spaceBetweenItems * itemIndex;
|
||||
_controller.animateTo(
|
||||
position,
|
||||
curve: Curves.easeInOut,
|
||||
|
|
@ -24,99 +113,67 @@ class _CarouselState extends State<Carousel> {
|
|||
);
|
||||
}
|
||||
|
||||
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<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(
|
||||
height: 100.0,
|
||||
height: widget.itemSize,
|
||||
child: ListView(
|
||||
controller: _controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue