I am building proof of concept on Flutter and trying as much as possible copy existing app. Our current app has sliding panel aka drop down at the top of the list, is there any way to do same behaviour in Flutter?
Asked
Active
Viewed 1.1k times
4
-
Something like and expansion panel could do the work ? https://docs.flutter.io/flutter/material/ExpansionPanel-class.html – Alexi Coard Jun 28 '17 at 07:29
-
Not really, tricky part here is having expanded panel over the content – nonameden Jun 30 '17 at 00:11
1 Answers
7
You can expand the panel with an Animation
. In the code below, I used AnimatedBuilder
to resize the panel as the animation ticks.
Because the panel is in the app bar, it's tricky to get it to interact well with scrolling. Once approach (shown below) is to use a NestedScrollView
and put your expanding panel in the bottom
of a SliverAppBar
. It feels nice, but it does cause the content to get pushed down by the expanding panel and has some mild interactions with scroll physics at the edges of your item list. If you want to customize the behavior further you could copy NestedScrollView
and modify it to your liking.
import 'package:flutter/material.dart';
class DayPickerBar extends StatefulWidget {
DayPickerBar({ this.selectedDate, this.onChanged });
final DateTime selectedDate;
final ValueChanged<DateTime> onChanged;
DayPickerBarState createState() => new DayPickerBarState();
}
class DayPickerBarState extends State<DayPickerBar> {
DateTime _displayedMonth = new DateTime.now();
@override
Widget build(BuildContext context) {
return new Container(
color: Theme
.of(context)
.canvasColor,
height: 250.0,
child: new Row(
children: <Widget>[
new IconButton(
icon: new Icon(Icons.chevron_left),
onPressed: () {
setState(() {
_displayedMonth = new DateTime(
_displayedMonth.year,
_displayedMonth.month - 1,
);
});
},
),
new Expanded(
child: new DayPicker(
selectedDate: widget.selectedDate,
currentDate: new DateTime.now(),
displayedMonth: _displayedMonth,
firstDate: new DateTime.now().subtract(new Duration(days: 1)),
lastDate: new DateTime.now().add(new Duration(days: 30)),
onChanged: widget.onChanged,
),
),
new IconButton(
icon: new Icon(Icons.chevron_right),
onPressed: () {
setState(() {
_displayedMonth = new DateTime(
_displayedMonth.year,
_displayedMonth.month + 1,
);
});
},
),
],
),
);
}
}
class FilterBar extends StatelessWidget {
FilterBar({ this.isExpanded, this.onExpandedChanged });
/// Whether this filter bar is showing the day picker or not
final bool isExpanded;
/// Called when the user toggles expansion
final ValueChanged<bool> onExpandedChanged;
static const Color _kFilterColor = Colors.deepOrangeAccent;
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
return new Container(
color: Theme.of(context).canvasColor,
child: new Row(
children: <Widget>[
new FlatButton(
onPressed: () => onExpandedChanged(!isExpanded),
textColor: theme.primaryColor,
child: new Row(
children: <Widget>[
new Text('Watch Today'),
new Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
),
new Expanded(
child: new Container(),
),
new Container(
decoration: new BoxDecoration(
color: _kFilterColor,
borderRadius: new BorderRadius.circular(16.0),
),
padding: const EdgeInsets.all(6.0),
child: new Text(
'All Cinemas',
style: theme.primaryTextTheme.button,
),
),
new Container(
decoration: new BoxDecoration(
color: _kFilterColor,
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(6.0),
margin: const EdgeInsets.symmetric(
horizontal: 6.0),
child: new Text(
' + ',
style: theme.primaryTextTheme.button,
),
),
],
),
);
}
}
class HomeScreen extends StatefulWidget {
HomeScreenState createState() => new HomeScreenState();
}
class HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
bool _isExpanded = false;
DateTime _selectedDate = new DateTime.now();
AnimationController _expandAnimationController;
Animation<Size> _bottomSize;
@override
void initState() {
super.initState();
_expandAnimationController = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_bottomSize = new SizeTween(
begin: new Size.fromHeight(kTextTabBarHeight + 40.0),
end: new Size.fromHeight(kTextTabBarHeight + 280.0),
).animate(new CurvedAnimation(
parent: _expandAnimationController,
curve: Curves.ease,
));
}
static const List<Tab> _tabs = const <Tab>[
const Tab(text: 'NOW SHOWING'),
const Tab(text: 'COMING SOON'),
];
Widget _buildMovie(BuildContext context, int index) {
ThemeData theme = Theme.of(context);
return new Container(
decoration: new BoxDecoration(
border: new Border(
bottom: new BorderSide(
color: Colors.grey[500],
),
),
image: new DecorationImage(
fit: BoxFit.cover,
image: new NetworkImage(
'http://cdn.bloody-disgusting.com/wp-content/uploads/2016/06/hollywood-takes-a-bite-out-of-green-lantern-star-2-new-shark-movies-on-the-horizon-835796.jpg',
),
),
),
child: new Stack(
children: <Widget>[
new Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: new Container(
padding: const EdgeInsets.all(20.0),
color: Colors.grey[800].withOpacity(0.2),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(
'27 Meters Down',
style: theme
.primaryTextTheme.subhead,
),
new Text(
'M 1h 29min, Horror',
style: theme.primaryTextTheme.
caption,
),
],
)
)
),
],
),
);
}
Widget _buildBottom() {
return new PreferredSize(
child: new SizedBox(
height: _bottomSize.value.height,
child: new Column(
children: <Widget>[
new TabBar(
tabs: _tabs,
),
new FilterBar(
onExpandedChanged: (bool value) async {
if (value &&
_expandAnimationController.isDismissed) {
await _expandAnimationController.forward();
setState(() {
_isExpanded = true;
});
} else if (!value &&
_expandAnimationController.isCompleted) {
await _expandAnimationController.reverse();
setState(() {
_isExpanded = false;
});
}
},
isExpanded: _isExpanded,
),
new Flexible(
child: new Stack(
overflow: Overflow.clip,
children: <Widget>[
new Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: new DayPickerBar(
onChanged: (DateTime value) {
setState(() {
_selectedDate = value;
});
},
selectedDate: _selectedDate,
),
)
],
),
),
],
),
),
preferredSize: _bottomSize.value,
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
drawer: new Container(),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.filter_list),
onPressed: () {},
),
body: new DefaultTabController(
length: _tabs.length,
child: new NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
new AnimatedBuilder(
animation: _bottomSize,
builder: (BuildContext context, Widget child) {
return new SliverAppBar(
pinned: true,
floating: true,
title: const Text('Movies'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.search),
onPressed: () {},
),
new PopupMenuButton(
child: new Icon(Icons.more_vert),
itemBuilder: (BuildContext context) {
return <PopupMenuEntry>[
new PopupMenuItem(
child: new Text('Not implemented'),
)
];
},
),
],
bottom: _buildBottom(),
);
},
),
];
},
body: new TabBarView(
children: <Widget>[
new ListView.builder(
itemBuilder: _buildMovie,
itemExtent: 200.0,
),
new Container(
child: new Center(
child: new Text('Coming soon!'),
),
),
],
),
),
),
);
}
}
class ExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.teal,
),
home: new HomeScreen(),
debugShowCheckedModeBanner: false,
);
}
}
void main() {
runApp(new ExampleApp());
}

Collin Jackson
- 110,240
- 31
- 221
- 152
-
Thank you, yep it exact problem which I facing, expanding panel will move whole list down =( Is it possible to have ListView and Container in Stack, and have container some how fixed as a header during a scroll? Another problem why I can not put container with filters in the "bottom" of SliverAppBar is - filters only should be visible for first tab. – nonameden Jun 28 '17 at 19:06
-
Feel free to file an issue if you feel this should be provided by the framework. And if you do end up modifying NestedScrollView I hope you'll share your solution! – Collin Jackson Jun 28 '17 at 19:09
-
thank you @collin for pointing me in a right direction. After playing with NestedScrollView I found that it does not respect minExtent from headers, so inner scroll content been scrolled under pinned headers. Not sure is it bug or supposed to work like that, but it was a reason why you cant have anything pinned to the top in the body if sliver appbar has something in the bottom and "pinned & floating" – nonameden Jul 03 '17 at 22:03