向下滚动时隐藏底部导航栏,反之亦然
Hide bottom navigation bar on scroll down and vice versa
我在正文和底部导航栏中有一个列表。当帖子列表向下滚动时,我想用向下滑动动画隐藏底部导航栏,当向上滚动时,我想用向上滑动动画隐藏底部导航栏。怎么做?
屏幕截图(空安全 + 优化)
代码:
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
late final ScrollListener _model;
late final ScrollController _controller;
final double _bottomNavBarHeight = 56;
@override
void initState() {
super.initState();
_controller = ScrollController();
_model = ScrollListener.initialise(_controller);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _model,
builder: (context, child) {
return Stack(
children: [
ListView.builder(
controller: _controller,
itemCount: 20,
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
),
Positioned(
left: 0,
right: 0,
bottom: _model.bottom,
child: _bottomNavBar,
),
],
);
},
),
);
}
Widget get _bottomNavBar {
return SizedBox(
height: _bottomNavBarHeight,
child: BottomNavigationBar(
backgroundColor: Colors.amber,
items: [
BottomNavigationBarItem(icon: Icon(Icons.call), label: 'Call'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Message'),
],
),
);
}
}
class ScrollListener extends ChangeNotifier {
double bottom = 0;
double _last = 0;
ScrollListener.initialise(ScrollController controller, [double height = 56]) {
controller.addListener(() {
final current = controller.offset;
bottom += _last - current;
if (bottom <= -height) bottom = -height;
if (bottom >= 0) bottom = 0;
_last = current;
if (bottom <= 0 && bottom >= -height) notifyListeners();
});
}
}
使用 BottomNavigationBar
的工作代码。
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: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController _hideBottomNavController;
bool _isVisible;
@override
initState() {
super.initState();
_isVisible = true;
_hideBottomNavController = ScrollController();
_hideBottomNavController.addListener(
() {
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_isVisible)
setState(() {
_isVisible = false;
});
}
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.forward) {
if (!_isVisible)
setState(() {
_isVisible = true;
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: CustomScrollView(
controller: _hideBottomNavController,
shrinkWrap: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _getItem(context),
childCount: 20,
),
),
),
],
),
),
bottomNavigationBar: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isVisible ? 56.0 : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
);
}
_getItem(BuildContext context) {
return Card(
elevation: 3,
margin: EdgeInsets.all(8),
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Item',
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
)
],
),
),
),
],
),
);
}
}
Working Model
虽然 Naveen 的解决方案完美运行,但我不喜欢使用 setState
来处理导航栏可见性的想法。每次用户改变滚动方向时,包括应用栏和主体在内的整个主页都会重建,这可能是一项昂贵的操作。我创建了一个单独的 class 来处理使用 ValueNotifier
跟踪当前隐藏状态的可见性。
class HideNavbar {
final ScrollController controller = ScrollController();
final ValueNotifier<bool> visible = ValueNotifier<bool>(true);
HideNavbar() {
visible.value = true;
controller.addListener(
() {
if (controller.position.userScrollDirection ==
ScrollDirection.reverse) {
if (visible.value) {
visible.value = false;
}
}
if (controller.position.userScrollDirection ==
ScrollDirection.forward) {
if (!visible.value) {
visible.value = true;
}
}
},
);
}
void dispose() {
controller.dispose();
visible.dispose();
}
}
现在您要做的就是在主页小部件中创建 HideNavbar
的最终实例。
final HideNavbar hiding = HideNavbar();
现在将实例的 ScrollController
传递给 Scaffold
的 ListView
或 CustomScrollView
正文。
body: CustomScrollView(
controller: hiding.controller,
...
然后用 ValueListenableBuilder
包围您的 bottomNavigationBar
,它从 HideNavbar
实例中获取 ValueNotifier
,然后将 bottomNavigationBar 的高度 属性 设置为为 0 或任何其他值,具体取决于 ValueNotifier
.
的状态
bottomNavigationBar: ValueListenableBuilder(
valueListenable: hiding.visible,
builder: (context, bool value, child) => AnimatedContainer(
duration: Duration(milliseconds: 500),
height: value ? kBottomNavigationBarHeight : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
),
这种方法避免了无数次重建并且不需要任何外部库。您也可以将此实现为 stream-based 方法,但这需要另一个库,例如 dart:async
并且不会改变任何东西。确保在 HomePage
的 dispose 函数中调用 HideNavbar
的 dispose 函数以清除所有使用的资源。
我在正文和底部导航栏中有一个列表。当帖子列表向下滚动时,我想用向下滑动动画隐藏底部导航栏,当向上滚动时,我想用向上滑动动画隐藏底部导航栏。怎么做?
屏幕截图(空安全 + 优化)
代码:
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
late final ScrollListener _model;
late final ScrollController _controller;
final double _bottomNavBarHeight = 56;
@override
void initState() {
super.initState();
_controller = ScrollController();
_model = ScrollListener.initialise(_controller);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _model,
builder: (context, child) {
return Stack(
children: [
ListView.builder(
controller: _controller,
itemCount: 20,
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
),
Positioned(
left: 0,
right: 0,
bottom: _model.bottom,
child: _bottomNavBar,
),
],
);
},
),
);
}
Widget get _bottomNavBar {
return SizedBox(
height: _bottomNavBarHeight,
child: BottomNavigationBar(
backgroundColor: Colors.amber,
items: [
BottomNavigationBarItem(icon: Icon(Icons.call), label: 'Call'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Message'),
],
),
);
}
}
class ScrollListener extends ChangeNotifier {
double bottom = 0;
double _last = 0;
ScrollListener.initialise(ScrollController controller, [double height = 56]) {
controller.addListener(() {
final current = controller.offset;
bottom += _last - current;
if (bottom <= -height) bottom = -height;
if (bottom >= 0) bottom = 0;
_last = current;
if (bottom <= 0 && bottom >= -height) notifyListeners();
});
}
}
使用 BottomNavigationBar
的工作代码。
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: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController _hideBottomNavController;
bool _isVisible;
@override
initState() {
super.initState();
_isVisible = true;
_hideBottomNavController = ScrollController();
_hideBottomNavController.addListener(
() {
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_isVisible)
setState(() {
_isVisible = false;
});
}
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.forward) {
if (!_isVisible)
setState(() {
_isVisible = true;
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: CustomScrollView(
controller: _hideBottomNavController,
shrinkWrap: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _getItem(context),
childCount: 20,
),
),
),
],
),
),
bottomNavigationBar: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isVisible ? 56.0 : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
);
}
_getItem(BuildContext context) {
return Card(
elevation: 3,
margin: EdgeInsets.all(8),
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Item',
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
)
],
),
),
),
],
),
);
}
}
Working Model
虽然 Naveen 的解决方案完美运行,但我不喜欢使用 setState
来处理导航栏可见性的想法。每次用户改变滚动方向时,包括应用栏和主体在内的整个主页都会重建,这可能是一项昂贵的操作。我创建了一个单独的 class 来处理使用 ValueNotifier
跟踪当前隐藏状态的可见性。
class HideNavbar {
final ScrollController controller = ScrollController();
final ValueNotifier<bool> visible = ValueNotifier<bool>(true);
HideNavbar() {
visible.value = true;
controller.addListener(
() {
if (controller.position.userScrollDirection ==
ScrollDirection.reverse) {
if (visible.value) {
visible.value = false;
}
}
if (controller.position.userScrollDirection ==
ScrollDirection.forward) {
if (!visible.value) {
visible.value = true;
}
}
},
);
}
void dispose() {
controller.dispose();
visible.dispose();
}
}
现在您要做的就是在主页小部件中创建 HideNavbar
的最终实例。
final HideNavbar hiding = HideNavbar();
现在将实例的 ScrollController
传递给 Scaffold
的 ListView
或 CustomScrollView
正文。
body: CustomScrollView(
controller: hiding.controller,
...
然后用 ValueListenableBuilder
包围您的 bottomNavigationBar
,它从 HideNavbar
实例中获取 ValueNotifier
,然后将 bottomNavigationBar 的高度 属性 设置为为 0 或任何其他值,具体取决于 ValueNotifier
.
bottomNavigationBar: ValueListenableBuilder(
valueListenable: hiding.visible,
builder: (context, bool value, child) => AnimatedContainer(
duration: Duration(milliseconds: 500),
height: value ? kBottomNavigationBarHeight : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
),
这种方法避免了无数次重建并且不需要任何外部库。您也可以将此实现为 stream-based 方法,但这需要另一个库,例如 dart:async
并且不会改变任何东西。确保在 HomePage
的 dispose 函数中调用 HideNavbar
的 dispose 函数以清除所有使用的资源。