Flutter - 隐藏 FloatingActionButton
Posted
技术标签:
【中文标题】Flutter - 隐藏 FloatingActionButton【英文标题】:Flutter - Hiding FloatingActionButton 【发布时间】:2018-01-19 17:51:33 【问题描述】:Flutter 中是否有任何内置方法可以在 ListView
上隐藏 FloatingActionButton
向下滚动然后在向上滚动时显示它?
【问题讨论】:
【参考方案1】:import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main()
runApp(new MyApp());
class MyApp extends StatelessWidget
// This widget is the root of your application.
@override
Widget build(BuildContext context)
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
class MyHomePage extends StatefulWidget
MyHomePage(Key key, this.title) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
class _MyHomePageState extends State<MyHomePage>
int _counter = 0;
ScrollController _hideButtonController;
void _incrementCounter()
setState(()
_counter++;
);
var _isVisible;
@override
initState()
super.initState();
_isVisible = true;
_hideButtonController = new ScrollController();
_hideButtonController.addListener(()
if(_hideButtonController.position.userScrollDirection == ScrollDirection.reverse)
if(_isVisible == true)
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** $_isVisible up"); //Move IO away from setState
setState(()
_isVisible = false;
);
else
if(_hideButtonController.position.userScrollDirection == ScrollDirection.forward)
if(_isVisible == false)
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** $_isVisible down"); //Move IO away from setState
setState(()
_isVisible = true;
);
);
@override
Widget build(BuildContext context)
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new CustomScrollView(
controller: _hideButtonController,
shrinkWrap: true,
slivers: <Widget>[
new SliverPadding(
padding: const EdgeInsets.all(20.0),
sliver: new SliverList(
delegate: new SliverChildListDelegate(
<Widget>[
const Text('I\'m dedicating every day to you'),
const Text('Domestic life was never quite my style'),
const Text('When you smile, you knock me out, I fall apart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('And I thought I was so smart'),
const Text('I realize I am crazy'),
],
),
),
),
],
)
),
floatingActionButton: new Visibility(
visible: _isVisible,
child: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
),
);
如果我没有使用 listview,我深表歉意,因为我不知道如何使用 listview 滚动。我会回答你问题的其他部分。
首先你需要创建一个scrollcontroller 来监听scrollPostion 事件
如果滚动控制器设法找到scrolldirection 正向或反向。您添加一个将状态设置为可见的状态。
绘制按钮时,会将按钮包装在 visibility 类中。您设置了可见标志,小部件应忽略输入命令。
编辑:我似乎无法添加指向 ScrollController、ScrollerPosition、ScrollDirection 和 Opacity 的链接。我想你可以自己搜索或其他人在链接中编辑
Edit2:使用 CopsonRoad 或使用可见性小部件,除非您想在布局树中使用未绘制的小部件
Edit3:鉴于新来者按原样使用代码,我会更新代码以鼓励更好的实践。使用可见性而不是不透明度。从 setState 中移除 io。在 Flutter 1.5.4-hotfix.2 上测试
【讨论】:
根据this flutter issue,您必须使用import 'package:flutter/rendering.dart';
才能使ScrollController 可用
我包含了 rendering.dart
不要使用不透明度。请参阅下面 Sibin 的解决方案以正确使用 Visibility 控件。
@JimGomes Visibility 在我写答案时不是一个选项github.com/flutter/flutter/pull/20365 它是在 9 个月前添加的。 CopsOnRoad 的答案比我的要干净得多
当我使用带有 setstate 的侦听器时,我的列表滚动变慢了,这对用户体验很不利,其他人也有同样的问题吗?【参考方案2】:
这是一个很老的问题,但在我看来,最新的 Flutter 有一个更好(更短)的解决方案。
其他解决方案确实有效,但如果你想要一个漂亮的动画(与 android 中的默认动画相比),你可以这样做:
当用户滚动(向上/向下)时,NotificationListener 会通知您。使用 AnimationController 可以控制 FAB 的动画。
这是一个完整的例子:
class WidgetState extends State<Widget> with TickerProviderStateMixin<Widget>
AnimationController _hideFabAnimation;
@override
initState()
super.initState();
_hideFabAnimation = AnimationController(vsync: this, duration: kThemeAnimationDuration);
@override
void dispose()
_hideFabAnimation.dispose();
super.dispose();
bool _handleScrollNotification(ScrollNotification notification)
if (notification.depth == 0)
if (notification is UserScrollNotification)
final UserScrollNotification userScroll = notification;
switch (userScroll.direction)
case ScrollDirection.forward:
if (userScroll.metrics.maxScrollExtent !=
userScroll.metrics.minScrollExtent)
_hideFabAnimation.forward();
break;
case ScrollDirection.reverse:
if (userScroll.metrics.maxScrollExtent !=
userScroll.metrics.minScrollExtent)
_hideFabAnimation.reverse();
break;
case ScrollDirection.idle:
break;
return false;
@override
Widget build(BuildContext context)
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: Scaffold(
appBar: AppBar(
title: Text('Fabulous FAB Animation')
),
body: Container(),
floatingActionButton: ScaleTransition(
scale: _hideFabAnimation,
alignment: Alignment.bottomCenter,
child: FloatingActionButton(
elevation: 8,
onPressed: () ,
child: Icon(Icons.code),
),
),
),
);
【讨论】:
感谢您提供此解决方案!一个小问题。当我打开带有 FAB 的页面时,默认情况下它是隐藏的。要显示它,我必须向下滚动然后向上滚动。有没有办法一开始就显示 FAB?谢谢 我认为最简单的方法是调用floatingActionButton: _showFab? MyFloatingActionButton() : null;
并使用scrollController
监听器控制_showFab
变量。这样你就可以获得构建动画
@themthem 回答您的问题,您可以在 AnimationController
初始化之后添加 _hideFabAnimation.forward()
initSate
我喜欢这个解决方案。我使用的一个附加功能是将我的 fab 包装在 AnimatedCrossFade 中,这使动画变得隐式,我只需要更改一个 bool 标志。【参考方案3】:
没有动画:
使用 Visibility
小部件:
floatingActionButton: Visibility(
visible: false, // Set it to false
child: FloatingActionButton(...),
)
使用 Opacity
小部件:
floatingActionButton: Opacity(
opacity: 0, // Set it to 0
child: FloatingActionButton(...),
)
使用三元运算符:
floatingActionButton: shouldShow ? FloatingActionButton() : null,
使用if
条件:
floatingActionButton: Column(
children: <Widget>[
if (shouldShow) FloatingActionButton(...), // Visible if condition is true
],
)
带动画:
这只是使用动画的一个例子,您可以使用这种方法创建不同类型的 UI。
bool _showFab = true;
@override
Widget build(BuildContext context)
const duration = Duration(milliseconds: 300);
return Scaffold(
floatingActionButton: AnimatedSlide(
duration: duration,
offset: _showFab ? Offset.zero : Offset(0, 2),
child: AnimatedOpacity(
duration: duration,
opacity: _showFab ? 1 : 0,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () ,
),
),
),
body: NotificationListener<UserScrollNotification>(
onNotification: (notification)
final ScrollDirection direction = notification.direction;
setState(()
if (direction == ScrollDirection.reverse)
_showFab = false;
else if (direction == ScrollDirection.forward)
_showFab = true;
);
return true;
,
child: ListView.builder(
itemCount: 100,
itemBuilder: (_, i) => ListTile(title: Text('$i')),
),
),
);
【讨论】:
这个Visibility
小部件让我开心,是的!【参考方案4】:
一个很好的方法......
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class Home extends StatefulWidget
@override
_HomeState createState() => _HomeState();
class _HomeState extends State<Home>
ScrollController controller;
bool fabIsVisible = true;
@override
void initState()
super.initState();
controller = ScrollController();
controller.addListener(()
setState(()
fabIsVisible =
controller.position.userScrollDirection == ScrollDirection.forward;
);
);
@override
Widget build(BuildContext context)
return Scaffold(
body: ListView(
controller: controller,
children: List.generate(
100,
(index) => ListTile(
title: Text("Text $index"),
)),
),
floatingActionButton: AnimatedOpacity(
child: FloatingActionButton(
child: Icon(Icons.add),
tooltip: "Increment",
onPressed: !fabIsVisible ? null: ()
print("Pressed");
,
),
duration: Duration(milliseconds: 100),
opacity: fabIsVisible ? 1 : 0,
),
);
【讨论】:
不能与 singleChildScrollView 一起使用【参考方案5】:您可以使用以下代码保留默认动画
floatingActionButton: _isVisible
? FloatingActionButton(...)
: null,
【讨论】:
这应该是公认的答案!这为您提供了内置动画。将其与scrollController
侦听器结合使用,该侦听器将 _isVisible
设置为 setState
【参考方案6】:
您可以使用Visibility
小部件来处理子小部件的可见性
样本:
floatingActionButton:
Visibility(visible: _visibilityFlag , child: _buildFAB(context)),
【讨论】:
应该使用可见性而不是不透明度。使用不透明度,即使您看不到它,控件仍然存在并且可以激活。这可能会给用户带来令人惊讶的结果,这是不好的。【参考方案7】:其他非常好的方法是AnimatedOpacity
AnimatedOpacity(
opacity: isEnabled ? 0.0 : 1.0,
duration: Duration(milliseconds: 1000),
child: FloatingActionButton(
onPressed: your_method,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
)
【讨论】:
【参考方案8】:@Josteve 的答案是正确的,但是每次用户滚动时调用setState()
并不是一个好主意。更好的方法如下所示:
import 'package:flutter/material.dart';
class Home extends StatefulWidget
@override
_HomeState createState() => _HomeState();
class _HomeState extends State<Home>
ScrollController controller;
bool _isFabVisible = true;
@override
void initState()
super.initState();
controller = ScrollController();
controller.addListener(()
// FAB should be visible if and only if user has not scrolled to bottom
var userHasScrolledToBottom = controller.position.atEdge && controller.position.pixels > 0;
if(_isFabVisible == userHasScrolledToBottom)
setState(() => _isFabVisible = !userHasScrolledToBottom);
);
@override
Widget build(BuildContext context)
return Scaffold(
body: ListView(
controller: controller,
children: List.generate(
100,
(index) => ListTile(
title: Text("Text $index"),
)),
),
floatingActionButton: AnimatedOpacity(
child: FloatingActionButton(
child: Icon(Icons.add),
tooltip: "Increment",
onPressed: ()
print("Pressed");
,
),
duration: Duration(milliseconds: 100),
opacity: _isFabVisible? 1 : 0,
),
);
【讨论】:
必须初始化不可为空的实例字段“控制器”。尝试添加初始化表达式,或初始化它的生成构造函数,或将其标记为“延迟”。dart(not_initialized_non_nullable_instance_field)【参考方案9】:对于使用 Rxdart 的任何人,有一种简洁的方法可以做到这一点,并且它带有额外的方便工具。
首先,将滚动位置转换为流,您以后也可以重复使用此方法。
extension ScrollControllerX on ScrollController
Stream<double> positionAsStream()
late StreamController<double> controller;
void addListener() => controller.add(position.pixels);
void onListen() => this.addListener(addListener);
void onCancel()
removeListener(addListener);
controller.close();
controller = StreamController<double>(onListen: onListen, onCancel: onCancel);
return controller.stream;
像这样使用它。
@override
void initState()
super.initState();
final subscription = scrollController
.positionAsStream()
.pairwise()
.map((p) => p.last > p.first)
.distinct() // If direction don't change, skip it
.listen((down) => down ? hideFabAnimationController.forward() : hideFabAnimationController.reverse());
FadeTransition(
opacity: hideFabAnimationController,
child: ScaleTransition(
scale: hideFabAnimationController,
child: FloatingActionButton(
onPressed: () => ,
child: const Icon(Icons.add),
),
),
)
别忘了取消订阅!
@override
void dispose()
subscription.cancel();
当用户滚动速度过快时,您可以执行其他操作,例如限制流。
【讨论】:
以上是关于Flutter - 隐藏 FloatingActionButton的主要内容,如果未能解决你的问题,请参考以下文章
Flutter - 隐藏 FloatingActionButton