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 = (
|
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 = (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue