Flutter中如何实现类似CSS position: sticky的效果?仿Spotify UI
实现Spotify风格滚动固定播放按钮的AppBar
核心思路是把播放按钮拆为两个实例:一个在初始展开区域和其他按钮同行,另一个在收起后的AppBar中间,通过监听滚动偏移控制两者的显示/隐藏(或位置动画),配合SliverAppBar的pinned特性完成过渡。
具体实现步骤
- 用
ScrollController监听滚动,计算滚动偏移与SliverAppBar展开高度的比例,用来控制元素的透明度或位置。 - 配置
SliverAppBar为pinned: true,确保收起后固定在顶部;设置expandedHeight为封面图的高度。 - 在
flexibleSpace中放置封面图、遮罩层和初始按钮行(包含爱心、下载、三点和初始播放按钮)。 - 在SliverAppBar的
title区域放置固定的中间播放按钮,通过滚动比例控制其透明度,与初始播放按钮形成切换效果。
完整代码示例
import 'package:flutter/material.dart'; class SpotifyStyleUI extends StatefulWidget { const SpotifyStyleUI({super.key}); @override State<SpotifyStyleUI> createState() => _SpotifyStyleUIState(); } class _SpotifyStyleUIState extends State<SpotifyStyleUI> { final ScrollController _scrollController = ScrollController(); double _scrollOffset = 0.0; final double _expandedHeight = 260.0; @override void initState() { super.initState(); _scrollController.addListener(() { setState(() { _scrollOffset = _scrollController.offset; }); }); } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // 计算滚动进度:0为完全展开,1为完全收起 final scrollProgress = (_scrollOffset / (_expandedHeight - kToolbarHeight)).clamp(0.0, 1.0); return Scaffold( backgroundColor: Colors.black, body: CustomScrollView( controller: _scrollController, slivers: [ SliverAppBar( pinned: true, expandedHeight: _expandedHeight, backgroundColor: Colors.black.withOpacity(scrollProgress), elevation: scrollProgress > 0.5 ? 4 : 0, // 收起后的AppBar:中间固定播放按钮 title: Opacity( opacity: scrollProgress, child: const Center( child: Icon( Icons.play_circle_fill, color: Colors.green, size: 40, ), ), ), // 展开区域内容 flexibleSpace: FlexibleSpaceBar( background: Stack( fit: StackFit.expand, children: [ // 封面图 Image.network( 'https://picsum.photos/id/1015/800/800', fit: BoxFit.cover, ), // 渐变遮罩,提升底部按钮可读性 LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withOpacity(0.8), ], ), // 初始状态按钮行 Align( alignment: Alignment.bottomLeft, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), child: Row( children: [ const Icon(Icons.favorite, color: Colors.green, size: 28), const SizedBox(width: 18), const Icon(Icons.download, color: Colors.white, size: 28), const SizedBox(width: 18), const Icon(Icons.more_vert, color: Colors.white, size: 28), const Spacer(), // 初始播放按钮:随滚动消失 Opacity( opacity: 1 - scrollProgress, child: const Icon( Icons.play_circle_fill, color: Colors.green, size: 56, ), ), ], ), ), ), ], ), ), ), // 页面内容列表 SliverList( delegate: SliverChildBuilderDelegate( (context, index) => ListTile( leading: const Icon(Icons.music_note, color: Colors.grey), title: Text('Track ${index + 1}', style: const TextStyle(color: Colors.white)), subtitle: Text('Artist ${index + 1}', style: TextStyle(color: Colors.grey[400])), trailing: const Icon(Icons.more_vert, color: Colors.grey), tileColor: Colors.black, ), childCount: 30, ), ), ], ), ); } }
关键细节说明
- 滚动进度计算:通过
scrollProgress控制元素的透明度、AppBar背景透明度和阴影,实现平滑过渡。 - 双播放按钮切换:初始播放按钮随滚动逐渐隐藏,AppBar中的固定按钮逐渐显示,模拟Spotify的切换效果。
- 进阶动画优化:如果想要播放按钮平滑移动到中间而非切换显示,可以把播放按钮从两个实例改成一个,用
AnimatedPositioned包裹,根据scrollProgress计算其位置,实现移动过渡。
内容的提问来源于stack exchange,提问作者Sanketh B. K
相关产品推荐
相关产品推荐

