How to build animated headers like this GIF in Flutter? - dart

In iOS, I wrote a somewhat complex custom UIViewController that handles transitioning between unique child controllers; most notably, a special header view at the top of each one. I'm still trying to really wrap my head around end to end architecture in Flutter, and would like some suggestions on how to accomplish this. There are two types of headers - Arc and Profile, and each one goes from an expanded to a collapsed state as the user scrolls. Additionally, navigation between any combination of type and state can have a transition defined.
Here is how it looks when used in a TabBar for example. Transitions are handled gracefully wether nested in Tab/NavigationControllers or not.

This is what I threw together, I hope it helps (click for video):
Note:
It would be better to reduce the amount of animation controllers, ideally to a single controller that controls both the header extent and the arc curvature
There is no animation for the content below the header, but I'm sure you could add that as well.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anim playground',
theme: ThemeData(
brightness: Brightness.dark,
),
home: AnimatedPageTest(),
);
}
}
class AnimatedPageTest extends StatefulWidget {
#override
_AnimatedPageTestState createState() => _AnimatedPageTestState();
}
class _AnimatedPageTestState extends State<AnimatedPageTest> {
bool _arc = true;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: AnimatedPage(
appearance: _arc ? HeaderAppearance.arc : HeaderAppearance.profile,
backgroundImage: _arc ? 'assets/earth.jpg' : 'assets/moon.jpg',
children: List.generate(30, (index) => ListTile(title: Text('index'),)),
),),
persistentFooterButtons: <Widget>[
FlatButton(
child: Text('Switch'),
onPressed: () {
setState(() {
_arc = !_arc;
});
},
)
],
);
}
}
enum HeaderAppearance { arc, profile }
double _getTargetMaxExtent(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 150.0;
} else {
return 75.0;
}
}
double _getTargetArcAnimationValue(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 1.0;
} else {
return 0.0;
}
}
class AnimatedPage extends StatefulWidget {
AnimatedPage({Key key, this.appearance, this.backgroundImage, this.children}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final List<Widget> children;
#override
_AnimatedPageState createState() => _AnimatedPageState();
}
class _AnimatedPageState extends State<AnimatedPage> with SingleTickerProviderStateMixin {
AnimationController _maxExtentAnimation;
#override
void initState() {
super.initState();
_maxExtentAnimation = AnimationController.unbounded(vsync: this, value: _getTargetMaxExtent(widget.appearance));
}
#override
void didUpdateWidget(AnimatedPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_maxExtentAnimation.animateTo(
_getTargetMaxExtent(widget.appearance),
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
}
#override
void dispose() {
_maxExtentAnimation.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _maxExtentAnimation,
builder: (context, child) {
return CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: AnimatedHeaderDelegate(
appearance: widget.appearance,
backgroundImage: widget.backgroundImage,
minExtent: 50.0,
maxExtent: _maxExtentAnimation.value,
),
),
child,
],
);
},
child: SliverList(delegate: SliverChildListDelegate(widget.children)),
);
}
}
class AnimatedHeaderDelegate extends SliverPersistentHeaderDelegate {
AnimatedHeaderDelegate({this.appearance, this.backgroundImage, this.minExtent, this.maxExtent});
final HeaderAppearance appearance;
final String backgroundImage;
#override
final double minExtent;
#override
final double maxExtent;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final shrinkRelative = shrinkOffset / (maxExtent - minExtent);
return AnimatedHeader(
appearance: appearance,
backgroundImage: backgroundImage,
curvatureMultiplier: 1.0 - shrinkRelative,
);
}
#override
bool shouldRebuild(AnimatedHeaderDelegate oldDelegate) {
return appearance != oldDelegate.appearance ||
minExtent != oldDelegate.minExtent ||
maxExtent != oldDelegate.maxExtent;
}
}
class AnimatedHeader extends StatefulWidget {
AnimatedHeader({Key key, this.appearance, this.backgroundImage, this.curvatureMultiplier}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final double curvatureMultiplier;
#override
_AnimatedHeaderState createState() => _AnimatedHeaderState();
}
class _AnimatedHeaderState extends State<AnimatedHeader> with TickerProviderStateMixin {
AnimationController _arcAnimation;
#override
void initState() {
super.initState();
_arcAnimation = AnimationController(
vsync: this,
value: _getTargetArcAnimationValue(widget.appearance),
duration: Duration(milliseconds: 600),
);
}
#override
void didUpdateWidget(AnimatedHeader oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_arcAnimation.animateTo(_getTargetArcAnimationValue(widget.appearance));
}
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: CurvedAnimation(parent: _arcAnimation, curve: Curves.linear),
builder: (context, child) {
return ClipPath(
clipper: ArcClipper(
curvature: _arcAnimation.value * widget.curvatureMultiplier,
),
clipBehavior: Clip.antiAlias,
child: child,
);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(milliseconds: 600),
child: Container(
key: ValueKey(widget.backgroundImage),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(widget.backgroundImage),
fit: BoxFit.cover,
),
),
),
),
Center(
child: Text(
'TITLE',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
class ArcClipper extends CustomClipper<Path> {
ArcClipper({this.curvature});
final double curvature;
#override
Path getClip(Size size) {
if (curvature == 0.0) {
return Path()..addRect(Offset.zero & size);
} else {
return Path()
..moveTo(0.0, 0.0)
..lineTo(size.width, 0.0)
..lineTo(size.width, size.height)
..quadraticBezierTo(size.width / 2, size.height - size.height * 0.4 * curvature, 0.0, size.height)
..close();
}
}
#override
bool shouldReclip(ArcClipper oldClipper) {
return curvature != oldClipper.curvature;
}
}

Related

Flutter ScrollView Animation: How do you prevent scroll animation when children change size?

When the bottom child is tapped (the height changes when its tapped), the parent color (blue) is shown while animating.
Is there anyway to prevent the animation?
Should I be thinking about this in a different way?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(color: Colors.blue, child: CustomScrollView(slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate([
TapBox(color: Colors.red),
TapBox(color: Colors.white),
TapBox(color: Colors.red),
]),
)
],))
);
}
}
class TapBox extends StatefulWidget {
final Color color;
TapBox({ this.color });
#override
State<StatefulWidget> createState() => _TapBoxState();
}
class _TapBoxState extends State<TapBox> {
double height = 500;
onTap() {
setState(() {
if (height == 500) {
height = 250;
} else {
height = 500;
}
});
}
#override
Widget build(BuildContext context) {
return Material(child: InkWell(onTap: onTap, child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return Container(width: constraints.maxWidth, height: height, color: widget.color);
})));
}
}

Flutter clicking on 1 button should animate rest of the buttons

Main Code:
ListView.builder(
itemCount: 5,
itemBuilder: (context, index) {
return MyWidget(index + 1);
},
),
MyWidget is a StatefulWidget and it's build() method is
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _controller,
child: RaisedButton(
child: Text(widget.index.toString()),
onPressed: () {},
),
);
}
What needs to be done:
So, as you can see here, I got 5 buttons named 1 2 3 4 5, what I want to achieve is when I click on any button say 3, rest of the buttons should animate except 3. How can I do it?
Screenshot:
TL;DR :> How to animate other buttons when button 3 is tapped?
Not sure if it's the most efficient way of doing it but this works, using an AnimatedWidget for each ListItem:
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Example")),
body: AnimatedListView(),
),
);
}
}
class AnimatedListView extends StatefulWidget {
#override
_AnimatedListViewState createState() => _AnimatedListViewState();
}
class _AnimatedListViewState extends State<AnimatedListView>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
int _selected;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 1000),
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.ease,
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 5,
itemBuilder: _buildListItem,
);
}
Widget _buildListItem(BuildContext context, int index) {
return AnimatedListItem(
selected: _selected == index,
animation: _animation,
onTap: () {
setState(() {
_selected = index;
});
_controller.forward();
},
);
}
}
class AnimatedListItem extends AnimatedWidget {
final Tween<double> _opacityTween = Tween(begin: 1, end: 0);
final GestureTapCallback onTap;
final bool selected;
AnimatedListItem(
{Key key,
#required Animation<double> animation,
this.onTap,
this.selected})
: super(key: key, listenable: animation);
#override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return Opacity(
opacity: selected ? 1.0 : _opacityTween.evaluate(animation),
child: ListTile(title: Text("Item"), onTap: onTap),
);
}
}

Flutter change state from StatefulWidget object

Like title states, how can one access the state of a StatefulWidget from the StatefulWidget.
Background:
I have a star rating widget that consists of 5 "StarWidget"s in a row. The StarWidget class is just an Icon with a detector wrapped around it (not using IconButton because it has a very large size). The StarWidget stores whether it is selected or not in a corresponding State object and accordingly displays a solid or outline icon.
In my main widget, I have access to the StatefulWidget objects, and would like to configure their states.
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class StarRatingWidget extends StatefulWidget {
#override
_StarRatingWidgetState createState() {
return _StarRatingWidgetState();
}
}
class _StarRatingWidgetState extends State<StarRatingWidget>
implements StarSelectionInterface {
//Properties
int _currentRating = 0;
List<RatingStarWidget> starWidgets = [];
//Methods
#override
void initState() {
super.initState();
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 0,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 1,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 2,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 3,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 4,
),
);
}
#override
Widget build(BuildContext buildContext) {
return Row(
children: starWidgets,
);
}
//Star Selection Interface Methods
void onStarSelected(_RatingStarWidgetState starWidgetState) {
print("listener: star selected ${starWidgetState._starPosition}");
//a new, rating has been selected, update rating
if (_currentRating != starWidgetState._starPosition) {
_currentRating = (starWidgetState._starPosition + 1);
}
//same star as rating has been selected, set rating to 0
else {
_currentRating = 0;
}
//update stars according to rating
for(int i = 1; i <= 5; i++) {
//what should I do here?!
}
}
}
class RatingStarWidget extends StatefulWidget {
//Properties
final int starPosition;
final StarSelectionInterface starSelectionInterface;
//Constructors
RatingStarWidget({this.starSelectionInterface, this.starPosition});
//Methods
#override
_RatingStarWidgetState createState() {
return _RatingStarWidgetState(starSelectionInterface, starPosition);
}
}
class _RatingStarWidgetState extends State<RatingStarWidget> {
//Properties
int _starPosition;
bool _isSelected = false;
StarSelectionInterface selectionListener;
//Constructors
_RatingStarWidgetState(this.selectionListener, this._starPosition);
//Methods
#override
Widget build(BuildContext buildContext) {
return AnimatedCrossFade(
firstChild: GestureDetector(
child: Icon(
FontAwesomeIcons.star,
size: 14,
),
onTap: () {
print("star: selected");
selectionListener.onStarSelected(this);
},
),
secondChild: GestureDetector(
child: Icon(
FontAwesomeIcons.solidStar,
size: 14,
),
onTap: () {
selectionListener.onStarSelected(this);
},
),
duration: Duration(milliseconds: 300),
crossFadeState:
_isSelected ? CrossFadeState.showSecond : CrossFadeState.showFirst,
);
}
}
class StarSelectionInterface {
void onStarSelected(_RatingStarWidgetState starWidgetState) {}
}
The Flutter way is to rebuild widgets whenever it is necessary. Don't be afraid to build widgets, they are cheap for the SDK, specially in this case for simple stars.
Accessing another widget state requires more work than just rebuilding it. To access the state you should use keys or you should add special methods in the widget itself.
In this case, where the star is rebuilt no matter what, it is even better and simpler to use plain stateless widgets because the selected state can be provided by the parent in the moment of rebuilding.
And since the state is stored in the parent widget, I think it is better no to store it as wall in each one of the individual stars.
Next is a very simple solution that follows that idea. But yes, it still rebuilds the stars.
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: Center(child: StarRatingWidget())),
);
}
}
class StarRatingWidget extends StatefulWidget {
#override
_StarRatingWidgetState createState() {
return _StarRatingWidgetState();
}
}
class _StarRatingWidgetState extends State<StarRatingWidget> {
int _currentRating = 0;
List<Widget> buildStars() {
List<RatingStarWidget> starWidgets = [];
for (int i = 0; i < 5; i++) {
starWidgets.add(
RatingStarWidget(
clickCallback: () => setState(() {
_currentRating = i + 1;
}),
highlighted: _currentRating > i,
),
);
}
return starWidgets;
}
#override
Widget build(BuildContext buildContext) {
return Row(
children: buildStars(),
);
}
}
class RatingStarWidget extends StatelessWidget {
//Properties
final VoidCallback clickCallback;
final bool highlighted;
//Constructors
RatingStarWidget({this.clickCallback, this.highlighted});
#override
StatelessElement createElement() {
print("Element created");
return super.createElement();
}
//Methods
#override
Widget build(BuildContext buildContext) {
return GestureDetector(
onTap: () {
clickCallback();
},
child: AnimatedCrossFade(
firstChild: Icon(
FontAwesomeIcons.star,
size: 14,
),
secondChild: Icon(
FontAwesomeIcons.solidStar,
size: 14,
),
duration: Duration(milliseconds: 300),
crossFadeState:
highlighted ? CrossFadeState.showSecond : CrossFadeState.showFirst,
),
);
}
}
I wrote my own example similar to yours. What I do here is:
Initial star rate is -1 because arrays start from 0 ;) and I create stars with position, current star rate and the callback function. We will use this callback function to update the value in the ScreenOne.
In Star widget, we have a local bool selected with default value false and we assign it a value inside the build function based on the position of the star and current rate. And we have setSelected() function which runs the callback function and updates currentRate with the value of star position.
Check the video example here.
class ScreenOne extends StatefulWidget {
#override
_ScreenOneState createState() => _ScreenOneState();
}
class _ScreenOneState extends State<ScreenOne> {
int currentRate = -1; //since array starts from 0, set non-selected as -1
List<Star> starList = []; //empty list
#override
void initState() {
super.initState();
buildStars(); //build starts here on initial load
}
Widget buildStars() {
starList = [];
for (var i = 0; i < 5; i++) {
starList.add(Star(
position: i,
current: currentRate,
updateParent: refresh, //this is callback
));
}
}
refresh(int index) {
setState(() {
currentRate = index; //update the currentRate
});
buildStars(); //build stars again
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Test page 1"),
),
body: Container(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: starList,
),
),
),
);
}
}
class Star extends StatefulWidget {
final Function(int index) updateParent; //callback
final int position; //position of star
final int current; //current selected star from parent
const Star({Key key, this.position, this.updateParent, this.current})
: super(key: key);
#override
_StarState createState() => _StarState();
}
class _StarState extends State<Star> {
bool selected = false;
void setSelected() {
widget.updateParent(widget.position);
}
#override
Widget build(BuildContext context) {
if (widget.current >= widget.position) {
selected = true;
} else {
selected = false;
}
return GestureDetector(
child: AnimatedCrossFade(
firstChild: Icon(Icons.star_border),
secondChild: Icon(Icons.star),
crossFadeState:
selected ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: Duration(milliseconds: 300),
),
onTap: () {
setSelected();
},
);
}
}

How to swipe/drag 2 or more buttons in a grid of buttons using flutter

I have made a grid of buttons using flutter but now I want to swipe through 2 or more buttons in a single drag such that all the buttons through which I am dragging gets selected.
I have checked out some questions on the same and I was redirected to use gesture detector but that's not enough. I need certain properties or better a sample code such that I am able to work through it.
an example of the dragable app is http://a5.mzstatic.com/us/r30/Purple60/v4/6f/00/35/6f0035d3-1bab-fcbb-cb13-8ab46cf3c44d/screen696x696.jpeg
You can manually hit test RenderBox and extract a specific RenderObject of your choice.
We could for example add the following renderobject above our buttons:
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({Widget child, this.index, Key key}) : super(child: child, key: key);
#override
RenderObject createRenderObject(BuildContext context) {
return _Foo()..index = index;
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
}
Then use a Listener to extract all _Foo found under the pointer.
Here's a full application using this principle:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Grid(),
);
}
}
class Grid extends StatefulWidget {
#override
GridState createState() {
return new GridState();
}
}
class GridState extends State<Grid> {
final Set<int> selectedIndexes = Set<int>();
final key = GlobalKey();
final Set<_Foo> _trackTaped = Set<_Foo>();
_detectTapedItem(PointerEvent event) {
final RenderBox box = key.currentContext.findRenderObject();
final result = BoxHitTestResult();
Offset local = box.globalToLocal(event.position);
if (box.hitTest(result, position: local)) {
for (final hit in result.path) {
/// temporary variable so that the [is] allows access of [index]
final target = hit.target;
if (target is _Foo && !_trackTaped.contains(target)) {
_trackTaped.add(target);
_selectIndex(target.index);
}
}
}
}
_selectIndex(int index) {
setState(() {
selectedIndexes.add(index);
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _detectTapedItem,
onPointerMove: _detectTapedItem,
onPointerUp: _clearSelection,
child: GridView.builder(
key: key,
itemCount: 6,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemBuilder: (context, index) {
return Foo(
index: index,
child: Container(
color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
),
);
},
),
);
}
void _clearSelection(PointerUpEvent event) {
_trackTaped.clear();
setState(() {
selectedIndexes.clear();
});
}
}
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({Widget child, this.index, Key key}) : super(child: child, key: key);
#override
_Foo createRenderObject(BuildContext context) {
return _Foo()..index = index;
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
}
I don't like this code at all, but it seems to be working
import 'package:flutter/material.dart';
class TestScaffold extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TestScaffoldState();
}
List<_SquareButton> _selectedList = [];
class _TestScaffoldState extends State<TestScaffold> {
List<_SquareButton> buttons = [
_SquareButton('1'),
_SquareButton('2'),
_SquareButton('3'),
_SquareButton('4'),
_SquareButton('5'),
_SquareButton('6'),
_SquareButton('7'),
_SquareButton('8'),
_SquareButton('9'),
_SquareButton('10'),
_SquareButton('11'),
_SquareButton('12'),
_SquareButton('13'),
_SquareButton('14'),
_SquareButton('15'),
_SquareButton('16'),
];
Map<Rect, _SquareButton> positions = {};
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Test'),),
body: GestureDetector(
onPanDown: (details) {
checkGesture(details.globalPosition);
},
onPanUpdate: (details) {
checkGesture(details.globalPosition);
},
child: GridView.count(crossAxisCount: 4,
physics: NeverScrollableScrollPhysics(),
children: buttons,),)
);
}
initPositions() {
if (positions.isNotEmpty) return;
buttons.forEach((btn) {
RenderBox box = btn.bKey.currentContext.findRenderObject();
Offset start = box.localToGlobal(Offset.zero);
Rect rect = Rect.fromLTWH(start.dx, start.dy, box.size.width, box.size.height);
positions.addAll({rect: btn});
});
}
checkGesture(Offset position) {
initPositions();
positions.forEach((rect, btn) {
if (rect.contains(position)) {
if (!_selectedList.contains(btn)) {
_selectedList.add(btn);
btn.state.setState((){});
}
}
});
}
}
class _SquareButton extends StatefulWidget {
_SquareButton(this.title);
final String title;
final GlobalKey bKey = GlobalKey();
State state;
#override
State<StatefulWidget> createState() {
state = _SquareButtonState();
return state;
}
}
class _SquareButtonState extends State<_SquareButton> {
#override
Widget build(BuildContext context) {
return Padding(key: widget.bKey, padding: EdgeInsets.all(4.0), child: Container(
color: _selectedList.contains(widget) ? Colors.tealAccent : Colors.teal,
child: Text(widget.title),
alignment: Alignment.center,
),);
}
}
There is a moment.
If you enable scrolling - GestureDetector not always work on vertical movements

How to write clickable functions for slider images of image_carousel package in flutter?

I am using imagecarousel package for displaying images from the network. I want to keep onPressed function for images in the slide.
new ImageCarousel(
<ImageProvider>[
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
new NetworkImage('http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2017/08/ferrari-portofino-reveal-2017-featured-new.jpg'),
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
],
interval: new Duration(seconds: 1),
)
After making some modifications to Image Carousel, I was able to implement click event (other events also possible). Here is the sample code.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class ImageCarousel extends StatefulWidget {
final List<ImageProvider> imageProviders;
final double height;
final TargetPlatform platform;
final Duration interval;
final TabController tabController;
final BoxFit fit;
// Images will shrink according to the value of [height]
// If you prefer to use the Material or Cupertino style activity indicator set the [platform] parameter
// Set [interval] to let the carousel loop through each photo automatically
// Pinch to zoom will be turned on by default
ImageCarousel(this.imageProviders,
{this.height = 250.0, this.platform, this.interval, this.tabController, this.fit = BoxFit.cover});
#override
State createState() => new _ImageCarouselState();
}
TabController _tabController;
class _ImageCarouselState extends State<ImageCarousel> with SingleTickerProviderStateMixin {
#override
void initState() {
super.initState();
_tabController = widget.tabController ?? new TabController(vsync: this, length: widget.imageProviders.length);
if (widget.interval != null) {
new Timer.periodic(widget.interval, (_) {
_tabController.animateTo(_tabController.index == _tabController.length - 1 ? 0 : ++_tabController.index);
});
}
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new SizedBox(
height: widget.height,
child: new TabBarView(
controller: _tabController,
children: widget.imageProviders.map((ImageProvider provider) {
return new CarouselImageWidget(widget, provider, widget.fit, widget.height);
}).toList(),
),
);
}
}
class CarouselImageWidget extends StatefulWidget {
final ImageCarousel carousel;
final ImageProvider imageProvider;
final BoxFit fit;
final double height;
CarouselImageWidget(this.carousel, this.imageProvider, this.fit, this.height);
#override
State createState() => new _CarouselImageState();
}
class _CarouselImageState extends State<CarouselImageWidget> {
bool _loading = true;
Widget _getIndicator(TargetPlatform platform) {
if (platform == TargetPlatform.iOS) {
return new CupertinoActivityIndicator();
} else {
return new Container(
height: 40.0,
width: 40.0,
child: new CircularProgressIndicator(),
);
}
}
#override
void initState() {
super.initState();
widget.imageProvider.resolve(new ImageConfiguration()).addListener((i, b) {
if (mounted) {
setState(() {
_loading = false;
});
}
});
}
#override
Widget build(BuildContext context) {
return new Container(
height: widget.height,
child: _loading
? _getIndicator(widget.carousel.platform == null ? defaultTargetPlatform : widget.carousel.platform)
: new GestureDetector(
child: new Image(
image: widget.imageProvider,
fit: widget.fit,
),
onTap: () {
int index = int.parse(_tabController.index.toString());
switch(index){
//Implement you case here
case 0:
case 1:
case 2:
default:
print(_tabController.index.toString());
}
},
),
);
}
}
void main(){
runApp(new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Demo"),
),
body: new ImageCarousel(
<ImageProvider>[
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-2.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-10.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-4.jpg'),
],
interval: new Duration(seconds: 5),
)
),
));
}
Hope it helps..!!

Resources