flutter实现背景容器与滚动容器的联动滚动
flutter: 3.35.4
简单描述一下要实现的功能:有一个背景容器,背景容器比页面要长,可以滚动;页面中还存在一个可滚动的前景容器,可滚动的容器盖在背景上面。当滑动可滚动的前景容器时,需要判断背景容器是否滑动到底部,如果背景没有滚动到底部,需先滚动背景容器,如果背景已经滚动到底部,则滚动前景容器,这样来实现联动滚动效果。
起初分析用双层滚动容器实现,外层滚动容器放背景,内层滚动容器放列表,通过动态控制内层容器的可滚动属性来实现效果。这样的效果就是在动态切换的时候会阻断滑动手势,需要重新抬手按下才能执行滑动,体验不是很好,不知道有没有大佬能够解决这种手势冲突。
其实进一步分析不难发现,当内层容器覆盖在背景上时,当内层容器滚动,背景跟着一起滚动就行了,所以可以监听滚动容器的滚动,当背景没有滚动到底部时,就设置背景容器距离顶部高度为滚动容器滚动的距离即可。
后来就采用滚动容器滚动距离和背景容器高度来设置背景容器距离顶部的高度,外层容器使用 Stack,将容器背景固定,监听内层容器的滚动,当背景容器没有滚动到底部时,内层滚动时就设置背景容器的距离上部分的距离,具体实现如下:
import 'package:flutter/material.dart';
const double bgHeight = 1200;
class TestScroll extends StatefulWidget {
const TestScroll({super.key});
@override
State<TestScroll> createState() => _TestScrollState();
}
class _TestScrollState extends State<TestScroll> {
/// 控制滚动容器
final ScrollController _scrollController = ScrollController();
/// 背景应该滑动的高度
double _bgScrollTop = 0;
@override
void initState() {
super.initState();
_scrollController.addListener(_handleScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
/// 处理滚动
void _handleScroll() {
final height = MediaQuery.sizeOf(context).height;
// 测试固定背景容器高度为1200,实际使用肯定以真实的背景容器高度为主
// 如果如果背景滚动到底部,则设置背景的滚动高度为屏幕高低与背景容器高度的差值
if (_scrollController.position.pixels >= (bgHeight - height)) {
setState(() {
_bgScrollTop = -(bgHeight - height);
});
} else {
// 如果如果背景没有滚动到底部,则设置背景的滚动高度为滚动容器滚动的距离
setState(() {
_bgScrollTop = -_scrollController.position.pixels;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox(
height: MediaQuery.sizeOf(context).height,
child: Stack(
children: [
// 外层背景
Positioned(
top: _bgScrollTop,
left: 0,
child: Image.asset(
'assets/images/bg01.jpg',
height: bgHeight,
fit: BoxFit.fitHeight,
),
),
// 内层滚动容器
CustomScrollView(
controller: _scrollController,
physics: const ClampingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Container(
height: 400,
color: Colors.blue.withValues(alpha: 0.5),
child: Center(
child: Text(
'顶部内容区',
style: const TextStyle(
color: Colors.white
),
),
)
),
),
SliverList(
delegate: SliverChildBuilderDelegate((_, index) => Container(
height: 80,
color: Colors.transparent,
child: Center(
child: Text(
'列表项 $index',
style: const TextStyle(
color: Colors.white
),
)
),
), childCount: 20),
),
],
),
],
),
),
);
}
}
效果如下:
可以发现,这样就初步实现了联动滚动的效果,但是还不是很完美,如果我们在滚动过程中需要滚动一部分内容在顶部该如何做呢?
因为我们使用到的滚动容器透明的,不让它拥有颜色覆盖住底部背景,此时通过 SliverAppBar 来设置固定顶部位置,如果设置 SliverAppBar 为透明的,那么底部滚动内容滑动就会被 SliverAppBar 覆盖。大致代码如下:
// ... 其他代码省略
// 内层滚动容器
CustomScrollView(
controller: _scrollController,
physics: const ClampingScrollPhysics(),
slivers: [
// 新增顶部固定区域
SliverAppBar(
pinned: true,
expandedHeight: 200,
backgroundColor: Colors.transparent,
flexibleSpace: Center(
child: Text(
'顶部固定区',
style: const TextStyle(
color: Colors.orange
),
),
),
),
// ... 其他代码省略
],
),
// ... 其他代码省略
效果如下:
不难发现当内容滑动上来时会被顶部内容覆盖,因为固定的顶部是透明的,所以看起来就很怪异。所以怎么让内容区域的展示和顶部固定部分展示不冲突,而且可以完美的展示处底部的背景?
接下来继续分析,前面我们通过 Stack 固定了底部背景区域,并设置其可以通过滚动容器的滚动而滚动,为了让这部分看起来不那么怪异,我们可以使用加模糊背景(毛玻璃效果),或者是说如果我们可以截取背景顶部的区域到我们固定的顶部栏就可以实现了。毛玻璃效果容易实现,这里就实现第二种方式,我们可以通过将固定顶部栏设置成 Stack,再一次将背景图片放入其中,并让其的滚动效果和底部背景的滚动效果一致就行了,这样就等于盖截取了底部背景的一部分放在了固定区域,这样从视觉效果上就会达到连贯的效果。具体实现如下:
// ... 其他代码省略
// 内层滚动容器
CustomScrollView(
controller: _scrollController,
physics: const ClampingScrollPhysics(),
slivers: [
// 新增顶部固定区域
SliverAppBar(
pinned: true,
expandedHeight: 200,
elevation: 0,
automaticallyImplyLeading: false,
backgroundColor: Colors.transparent,
flexibleSpace: Stack(
children: [
Positioned(
top: _bgScrollTop,
left: 0,
child: Image.asset(
'assets/images/bg01.jpg',
height: 1200,
fit: BoxFit.fitHeight,
),
),
Center(
child: Text(
'顶部固定区',
style: const TextStyle(
color: Colors.orange
)
),
)
],
),
),
// ... 其他代码省略
],
),
// ... 其他代码省略
大致效果:
这样就实现了背景和滑动容器的联动滚动,当然这只是一种实现方式,可以作为参考,希望可以帮到您。感兴趣的也可以关注我的微信公众号【前端学习小营地】,不定时会分享一些小功能~