You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何实现类似CSS position: sticky的效果?仿Spotify UI

实现Spotify风格滚动固定播放按钮的AppBar

核心思路是把播放按钮拆为两个实例:一个在初始展开区域和其他按钮同行,另一个在收起后的AppBar中间,通过监听滚动偏移控制两者的显示/隐藏(或位置动画),配合SliverAppBar的pinned特性完成过渡。

具体实现步骤

  1. 用ScrollController监听滚动,计算滚动偏移与SliverAppBar展开高度的比例,用来控制元素的透明度或位置。
  2. 配置SliverAppBar为pinned: true,确保收起后固定在顶部;设置expandedHeight为封面图的高度。
  3. 在flexibleSpace中放置封面图、遮罩层和初始按钮行(包含爱心、下载、三点和初始播放按钮)。
  4. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 23:33:22