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

如何实现展开折叠切换时带顺滑动画效果的Flutter SliverAppBar?

展开状态的SliverAppBar
折叠状态的SliverAppBar

实现思路

你当前过渡效果怪异的核心原因是将折叠态和展开态的内容分别放在了FlexibleSpaceBar的title和background属性中,Flutter对这两个区域有内置的过渡逻辑,无法支持你自定义的细粒度动效。

正确的实现方式是直接在FlexibleSpaceBar的child中编写所有状态的内容,通过LayoutBuilder获取SliverAppBar的实时高度,计算出0~1区间的伸缩系数,用这个系数动态控制所有元素的大小、位置、透明度,就能实现完全顺滑的过渡效果。

完整可运行代码

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const double expandedHeight = 190;
    final double collapsedHeight = kToolbarHeight + 8;
    return MaterialApp(
      title: 'SliverAppBar动画Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              elevation: 0,
              pinned: true,
              expandedHeight: expandedHeight,
              collapsedHeight: collapsedHeight,
              flexibleSpace: FlexibleSpaceBar(
                collapseMode: CollapseMode.none,
                child: SafeArea(
                  child: LayoutBuilder(
                    builder: (context, constraints) {
                      // 计算当前展开系数,0为完全折叠,1为完全展开
                      final currentHeight = constraints.biggest.height;
                      final expandFactor = (currentHeight - collapsedHeight) / (expandedHeight - collapsedHeight);
                      final clampedFactor = expandFactor.clamp(0.0, 1.0);
                      
                      return Stack(
                        children: [
                          // 返回按钮和上一页文字
                          Positioned(
                            top: 0,
                            left: 0,
                            child: Opacity(
                              opacity: clampedFactor,
                              child: Row(
                                children: [
                                  IconButton(
                                    icon: const Icon(Icons.arrow_back, color: Colors.white),
                                    onPressed: () {},
                                  ),
                                  const Text(
                                    'PREVIOUS PAGE',
                                    style: TextStyle(
                                      fontSize: 12,
                                      fontWeight: FontWeight.bold,
                                      color: Colors.white,
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ),
                          // 主体内容区
                          Padding(
                            padding: EdgeInsets.only(
                              left: 20,
                              top: 12 + (40 * clampedFactor), // 展开时往下偏移避免和返回按钮重叠
                              right: 20,
                              bottom: 12,
                            ),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              mainAxisSize: MainAxisSize.min,
                              children: [
                                Row(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    // 头像
                                    CircleAvatar(
                                      radius: 16 + (4 * clampedFactor), // 展开20,折叠16
                                    ),
                                    SizedBox(width: 12 * clampedFactor),
                                    // 姓名职位信息,仅展开时显示
                                    Opacity(
                                      opacity: clampedFactor,
                                      child: Column(
                                        crossAxisAlignment: CrossAxisAlignment.start,
                                        children: [
                                          const Text(
                                            'Sertan Hakkı İmamoğlu',
                                            style: TextStyle(
                                              fontSize: 18,
                                              color: Colors.white,
                                              fontWeight: FontWeight.bold,
                                            ),
                                          ),
                                          const Text(
                                            'Student',
                                            style: TextStyle(
                                              fontSize: 14,
                                              color: Colors.grey,
                                            ),
                                          ),
                                          const SizedBox(height: 12),
                                        ],
                                      ),
                                    ),
                                    // 右侧操作按钮组,始终显示,大小随状态变化
                                    Expanded(
                                      child: Row(
                                        mainAxisAlignment: MainAxisAlignment.end,
                                        children: [
                                          Container(
                                            decoration: const BoxDecoration(
                                              color: Colors.white10,
                                              borderRadius: BorderRadius.all(Radius.circular(12)),
                                            ),
                                            padding: EdgeInsets.symmetric(
                                              horizontal: 8 + (26 * clampedFactor), // 展开34,折叠8
                                              vertical: 8 + (4 * clampedFactor), // 展开12,折叠8
                                            ),
                                            child: const Icon(Icons.person, color: Colors.white),
                                          ),
                                          SizedBox(width: 4 + (4 * clampedFactor)),
                                          Container(
                                            decoration: const BoxDecoration(
                                              color: Colors.white10,
                                              borderRadius: BorderRadius.all(Radius.circular(12)),
                                            ),
                                            padding: EdgeInsets.symmetric(
                                              horizontal: 8 + (26 * clampedFactor),
                                              vertical: 8 + (4 * clampedFactor),
                                            ),
                                            child: const Icon(Icons.menu, color: Colors.white),
                                          ),
                                          SizedBox(width: 4 + (4 * clampedFactor)),
                                          Container(
                                            decoration: const BoxDecoration(
                                              color: Colors.white10,
                                              borderRadius: BorderRadius.all(Radius.circular(12)),
                                            ),
                                            padding: EdgeInsets.symmetric(
                                              horizontal: 8 + (26 * clampedFactor),
                                              vertical: 8 + (4 * clampedFactor),
                                            ),
                                            child: const Icon(Icons.message, color: Colors.white),
                                          ),
                                        ],
                                      ),
                                    )
                                  ],
                                ),
                              ],
                            ),
                          ),
                        ],
                      );
                    },
                  ),
                ),
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Container(
                    color: index.isOdd ? Colors.white : Colors.black12,
                    height: 100.0,
                    child: Center(
                      child: Text('$index', textScaleFactor: 5),
                    ),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

动效说明

所有属性都基于实时的伸缩系数连续插值,没有任何跳变点,完全符合你提出的4项需求:

  • 头像半径从展开时的20线性缩小到折叠时的16,位置随布局自动调整
  • 返回按钮和上一页文字的透明度从1线性降到0,折叠时完全隐藏
  • 姓名和职位信息的透明度同步降低,折叠时完全隐藏
  • 三个操作按钮的内边距随系数变化,展开时大尺寸,折叠时小尺寸,间距同步调整

内容的提问来源于stack exchange,提问作者Sero

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 01:21:01