如何在 Flutter 中做滑动面板?
How to do sliding panel in Flutter?
我正在 Flutter 上构建概念验证并尝试尽可能多地复制现有应用程序。我们当前的应用程序在列表顶部有滑动面板,也就是下拉菜单,有什么方法可以在 Flutter 中执行相同的操作吗?
您可以使用 Animation
展开面板。在下面的代码中,我使用 AnimatedBuilder
在动画滴答时调整面板大小。
因为面板在应用栏中,所以很难让它与滚动很好地交互。一种方法(如下所示)是使用 NestedScrollView
并将扩展面板放在 SliverAppBar
的 bottom
中。感觉不错,但它确实会导致内容被扩展面板向下推,并且与项目列表边缘的滚动物理有一些轻微的交互。如果您想进一步自定义行为,您可以复制 NestedScrollView
并根据自己的喜好进行修改。
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());
}
我正在 Flutter 上构建概念验证并尝试尽可能多地复制现有应用程序。我们当前的应用程序在列表顶部有滑动面板,也就是下拉菜单,有什么方法可以在 Flutter 中执行相同的操作吗?
您可以使用 Animation
展开面板。在下面的代码中,我使用 AnimatedBuilder
在动画滴答时调整面板大小。
因为面板在应用栏中,所以很难让它与滚动很好地交互。一种方法(如下所示)是使用 NestedScrollView
并将扩展面板放在 SliverAppBar
的 bottom
中。感觉不错,但它确实会导致内容被扩展面板向下推,并且与项目列表边缘的滚动物理有一些轻微的交互。如果您想进一步自定义行为,您可以复制 NestedScrollView
并根据自己的喜好进行修改。
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());
}