Skip to content

Instantly share code, notes, and snippets.

@dnys1
Created July 26, 2019 03:58
Show Gist options
  • Save dnys1/52ecef595a66cd1bf88048e8d6138708 to your computer and use it in GitHub Desktop.
Save dnys1/52ecef595a66cd1bf88048e8d6138708 to your computer and use it in GitHub Desktop.
Carousel widget in Flutter
import 'dart:math';
import 'package:flutter/material.dart';
class DotsIndicator extends AnimatedWidget {
const DotsIndicator({
@required this.controller,
@required this.itemCount,
@required this.onPageSelected,
this.color = Colors.white,
}) : assert(controller != null),
assert(itemCount != null),
assert(onPageSelected != null),
super(listenable: controller);
final PageController controller;
final int itemCount;
final ValueChanged<int> onPageSelected;
final Color color;
static const double _kDotSize = 6.0;
static const double _kMaxZoom = 1.5;
static const double _kDotSpacing = 25.0;
Widget _buildDot(int index) {
final double page = controller.hasClients
? controller?.page ?? controller.initialPage.toDouble()
: controller.initialPage.toDouble();
final double correctedPage = page % itemCount;
double selectedness;
if (correctedPage > itemCount - 1 && index == 0) {
selectedness =
Curves.easeOut.transform(correctedPage - correctedPage.floor());
} else {
selectedness = Curves.easeOut
.transform(max(0.0, 1.0 - (correctedPage - index).abs()));
}
final double zoom = 1.0 + (_kMaxZoom - 1.0) * selectedness;
final int pageForClicking = (page / itemCount).floor() * itemCount + index;
return Container(
width: _kDotSpacing,
child: Center(
child: Material(
borderRadius: BorderRadius.circular(_kDotSize * zoom / 2),
color: color,
child: Container(
width: _kDotSize * zoom,
height: _kDotSize * zoom,
child: InkWell(onTap: () => onPageSelected(pageForClicking))),
)));
}
Widget build(BuildContext context) {
return Container(
height:
_kDotSize * 2, // put in fixed container to avoid "bouncing" on resize
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List<Widget>.generate(itemCount, _buildDot),
),
);
}
}
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/painting.dart';
import 'dots_indicator.dart';
class _SpotlightLayoutDelegate extends MultiChildLayoutDelegate {
_SpotlightLayoutDelegate({
@required this.page,
@required this.itemCount,
});
final double page;
final int itemCount;
final double _z = 1.30;
@override
void performLayout(Size size) {
final double offset = (page % itemCount) / itemCount;
final Offset center = Offset(size.width / 2, size.height / 4);
for (int i = 0; i < itemCount; i++) {
final String childId = 'item$i';
final double alpha = (i + offset * itemCount) * (2 * pi / itemCount);
final double x = (1 - sin(alpha)) / 2;
final double z = _z - (1 - cos(alpha)) / 2;
if (hasChild(childId)) {
final Size imageSize =
layoutChild(childId, BoxConstraints.tight((size / 4) * z));
positionChild(childId,
Offset(size.width * x - (size.width / 8 * z), size.height / 6));
}
}
}
@override
bool shouldRelayout(_SpotlightLayoutDelegate oldDelegate) =>
page != oldDelegate.page || itemCount != oldDelegate.itemCount;
}
class Spotlight extends StatefulWidget {
const Spotlight({
Key key,
@required this.images,
@required this.titles,
@required this.descriptions,
}) : assert(images.length == descriptions.length),
super(key: key);
final List<Image> images;
final List<String> titles;
final List<String> descriptions;
@override
_SpotlightState createState() => _SpotlightState();
}
class _SpotlightState extends State<Spotlight> {
final PageController _pageController = PageController(keepPage: false);
static const Duration _kDuration = Duration(milliseconds: 300);
static const Cubic _kCurve = Curves.ease;
double _page = 0.0;
int _pageIndex = 0;
int get itemCount => widget.images.length;
@override
Widget build(BuildContext context) {
final List<Widget> imagesWithId = <Widget>[];
final List<Widget> renderLast = <Widget>[];
// Paint order is determined by order of layout ids
for (int i = 0; i < itemCount; i++) {
final double offset = (_page % itemCount) / itemCount;
final double alpha = (i + offset * itemCount) * (2 * pi / itemCount);
// If in foreground, render last
if (alpha % (2 * pi) < pi / 2 || alpha % (2 * pi) > 3 * pi / 2) {
renderLast.add(LayoutId(
id: 'item$i',
child: widget.images[i],
));
continue;
}
imagesWithId.add(LayoutId(
id: 'item$i',
child: widget.images[i],
));
}
imagesWithId.addAll(renderLast);
return Stack(
children: <Widget>[
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics;
if (metrics.page >= 0) {
setState(() {
_page = metrics.page;
_pageIndex =
(itemCount - (_page % itemCount).round()) % itemCount;
});
}
}
return false;
},
child: Scrollable(
dragStartBehavior: DragStartBehavior.start,
axisDirection: AxisDirection.right,
controller: _pageController,
physics: const PageScrollPhysics(),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
offset: position,
axisDirection: AxisDirection.right,
slivers: <Widget>[
SliverFixedExtentList(
itemExtent: MediaQuery.of(context).size.width,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.transparent,
);
}),
),
],
);
},
),
),
CustomMultiChildLayout(
children: imagesWithId,
delegate: _SpotlightLayoutDelegate(
itemCount: itemCount,
page: _page,
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: MediaQuery.of(context).size.height / 2,
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(widget.titles[_pageIndex],
style: Theme.of(context).textTheme.headline),
Text(
widget.descriptions[_pageIndex],
style: Theme.of(context)
.textTheme
.body2
.copyWith(fontSize: 18.0),
textAlign: TextAlign.center,
),
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: DotsIndicator(
controller: _pageController,
itemCount: itemCount,
color: CupertinoColors.inactiveGray,
onPageSelected: (int page) {
_pageController.animateToPage(
page,
duration: _kDuration,
curve: _kCurve,
);
},
),
),
),
],
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment