【Flutter】两档可拉伸 BottomSheet 中嵌入 ListView
我是从一位博主文章里学习的,在他的基础上增加了两档BottomSheet(我也不知道应该叫什么名字好,修改后觉得的还不错,本想直接在那篇博主文章的评论里贴代码的,但是评论区不支持Markdown,也发不了图片,就觉得还是发篇文章出来的好)
原博客链接:简书:https://www.jianshu.com/p/4f2d10750f5c
掘金:https://juejin.cn/post/6997774040236572680
GitHub:https://github.com/JeremyMing/scroll_bottom_sheet
效果图展示:
增加返回顶部按钮
【注意】本代码是在原博主代码上改的,如需学习思路推荐先看原博客,原博有详细的说明解释
动图的代码(无返回顶部按钮)
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'sliver',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final ScrollController _scrollController = ScrollController();
/// 用来发送事件 改变弹框高度的stream
final StreamController<double> _streamController =
StreamController<double>.broadcast();
// 列表弹起的默认高度,即一档,
final double _initHeight = 400;
// 二档,【注意】如果需要修改一二档数值,需要重新编译,热启动没效果
final double _maxHeight = 650;
// 是否是最大拉伸状态,即二档
bool _isMaxHeight = false;
/// 记录手指按下的位置
double _pointerDy = 0;
void _incrementCounter() {
_isMaxHeight = false;
showModalBottomSheet(
barrierColor: Colors.black54,
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20), topRight: Radius.circular(20))),
builder: (ctx) {
return StreamBuilder<double>(
stream: _streamController.stream,
initialData: _initHeight,
builder: (context, snapshot) {
double currentHeight =
snapshot.data ?? (_isMaxHeight ? _maxHeight : _initHeight);
return AnimatedContainer(
duration: const Duration(milliseconds: 30),
height: currentHeight,
child: Listener(
onPointerMove: (event) {
// 触摸事件过程 手指一直在屏幕上且发生距离滑动
if (_scrollController.offset != 0) {
// 只有列表滚动到顶部时才触发下拉动画效果
print("onPointerMove:${_scrollController.offset}");
return;
}
double distance = event.position.dy - _pointerDy;
if (distance != 0) {
// 获取手指滑动的距离,计算弹框实时高度,并发送事件
double _currentHeight = _isMaxHeight ? (_maxHeight - distance)
: (_initHeight - distance);
// 上划达到最大高度后,就不再响应 BottomSheet,直接 return 去响应 Listview
// distance < 0 => 判断是否是上划操作
// 这里减了100 是为了优化用户操体验,不用上划太多
if (distance < 0 && _currentHeight >= _maxHeight - 100) {
// 已经是最大高度,但状态不对,更新状态
if (!_isMaxHeight) {
_isMaxHeight = true;
}
return;
}
// 不是最大高度,响应 BottomSheet,既可上划拉伸,也可下划关闭
_streamController.sink.add(_currentHeight);
}
},
onPointerUp: (event) {
// 触摸事件结束 手指离开屏幕
// 这里认为滑动超过一半就认为用户要退出了,值可以根据实际体验修改
if (_isMaxHeight
? currentHeight < (_maxHeight * 0.8)
: currentHeight < (_initHeight * 0.5)) {
Navigator.pop(context);
} else {
_streamController.sink
.add(_isMaxHeight ? _maxHeight : _initHeight);
}
},
onPointerDown: (event) {
// 触摸事件开始 手指开始接触屏幕
_pointerDy = event.position.dy + _scrollController.offset;
},
// child 这里嵌套一个 Padding,可以让 ListView 上划时有更好的融入感
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
child: ListView.builder(
controller: _scrollController,
// 如果上划,且已经达到 BottomSheet 的最大高度,则响应 ListView
physics: (currentHeight == _maxHeight)
? const ClampingScrollPhysics()
: const NeverScrollableScrollPhysics(),
itemCount: 100,
// ListView 中的 item 批量生成
itemBuilder: (BuildContext context, int index) {
if (index % 5 == 0) {
return Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 10),
child: ListTile(
title: Text(
"标题${(index / 5).round()}",
style: const TextStyle(fontSize: 30),
),
),
);
} else {
return ListBody(
mainAxis: Axis.vertical,
children: [
SizedBox(
width: 150,
height: 150,
child: Image.network(
"https://i2.hdslb.com/bfs/archive/1e87e11e12e28aa84ac0610fed3791eb2c3427f6.jpg@336w_190h_!web-video-rcmd-cover.webp"),
),
Center(child: Text("图片描述 $index")),
],
);
}
},
),
),
),
);
},
);
},
);
}
// 只有一个按钮的主页面
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('sliver'),
actions: const <Widget>[
Padding(
padding: EdgeInsets.only(right: 10),
child: Icon(Icons.share),
)
],
),
body: Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
_incrementCounter();
},
child: const Text('打开底部表'));
}),
);
}
}
返回顶部按钮代码
可参考教程书的这里 https://book.flutterchina.club/chapter6/scroll_controller.html 有示例
判断上划高度是否大于一定值,即 _scrollController.offset > 1000
时,控制悬浮按钮的是否显示。
注意的是 FloatActionButton
为 Scaffold
专属,所以要套一层 Scaffold
,在里面再添加 FloatActionButton
。
// 上面的代码省略
/// 列表弹起的正常高度
final double _initHeight = 400;
final double _maxHeight = 650;
bool _isMaxHeight = false;
// --------修改处-----------
bool _showTopButton = false;
// -------- 结束 -----------
void _incrementCounter() {
_isMaxHeight = false;
showModalBottomSheet(
barrierColor: Colors.black54,
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20), topRight: Radius.circular(20))),
builder: (ctx) {
return StreamBuilder<double>(
stream: _streamController.stream,
initialData: _initHeight,
builder: (context, snapshot) {
double currentHeight =
snapshot.data ?? (_isMaxHeight ? _maxHeight : _initHeight);
return AnimatedContainer(
duration: const Duration(milliseconds: 50),
height: currentHeight,
child: Listener(
onPointerMove: (event) {
// 触摸事件过程 手指一直在屏幕上且发生距离滑动
if (_scrollController.offset != 0) {
// 只有列表滚动到顶部时才触发下拉动画效果
print("onPointerMove:${_scrollController.offset}");
// ----------------修改处-------------------
if (_scrollController.offset > 1000) {
_showTopButton = true;
} else if (_showTopButton) {
setState(() {
_showTopButton = false;
});
}
return;
}
// -----------------修改结束----------------------
double distance = event.position.dy - _pointerDy;
if (distance != 0) {
// 获取手指滑动的距离,计算弹框实时高度,并发送事件
double _currentHeight = _isMaxHeight
? (_maxHeight - distance)
: (_initHeight - distance);
// 上划达到最大高度后,就不再响应 BottomSheet,直接 return 去响应 Listview
// distance < 0 => 判断是否是上划操作
// 这里减了100 是为了优化用户操体验,不用上划太多
if (distance < 0 && _currentHeight >= _maxHeight - 100) {
// 已经是最大高度,但状态不对,更新状态
if (!_isMaxHeight) {
_isMaxHeight = true;
}
// _currentHeight = _maxHeight;
return;
}
// 不是最大高度,响应 BottomSheet,即可上划拉伸,也可下划关闭
_streamController.sink.add(_currentHeight);
}
},
onPointerUp: (event) {
// 触摸事件结束 手指离开屏幕
// 这里认为滑动超过一半就认为用户要退出了,值可以根据实际体验修改
if (_isMaxHeight
? currentHeight < (_maxHeight * 0.8)
: currentHeight < (_initHeight * 0.5)) {
Navigator.pop(context);
} else {
_streamController.sink
.add(_isMaxHeight ? _maxHeight : _initHeight);
}
},
onPointerDown: (event) {
// 触摸事件开始 手指开始接触屏幕
_pointerDy = event.position.dy + _scrollController.offset;
},
// child 这里嵌套一个 Padding,可以让 ListView 上划时有更好的融入感
// ---------修改处,嵌套Scaffold,加入FloatActionButton---------
child: Padding(
padding: EdgeInsets.fromLTRB(0, 15, 0, 0),
child: Scaffold(
body: ListView.builder(
controller: _scrollController,
// 如果上划,且已经达到 BottomSheet 的最大高度,则响应 ListView
physics: (currentHeight == _maxHeight)
? const ClampingScrollPhysics()
: const NeverScrollableScrollPhysics(),
itemCount: 100,
// ListView 中的 item 批量生成
itemBuilder: (BuildContext context, int index) {
if (index % 5 == 0) {
return Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 10),
child: ListTile(
title: Text(
"标题${(index / 5).round()}",
style: const TextStyle(fontSize: 30),
),
),
);
} else {
return ListBody(
mainAxis: Axis.vertical,
children: [
SizedBox(
width: 150,
height: 150,
child: Image.network(
"https://i2.hdslb.com/bfs/archive/1e87e11e12e28aa84ac0610fed3791eb2c3427f6.jpg@336w_190h_!web-video-rcmd-cover.webp"),
),
Center(child: Text("图片描述 $index")),
],
);
}
},
),
// --------------修改处--------------
floatingActionButton: !_showTopButton
? null
: FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//返回到顶部时执行动画
_scrollController.animateTo(
.0,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
setState(
() {
_showTopButton = false;
},
);
},
),
),
),
),
);
},
);
},
);
}
void dispose() {
_scrollController.dispose();
super.dispose();
}
// 只有一个按钮的主页 代码省略。。。
增加的代码旁都有注释说明~,希望大家学习愉快。
Flutter 相关资料:
一滴也没了~