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

如何使用SliverPersistentHeader实现Flutter带动效的自定义AppBar

Flutter 自定义动效AppBar实现方案

实现思路

你的判断方向正确,该效果核心依赖 SliverPersistentHeader 监听滚动偏移量,直接基于偏移值计算元素属性实现连贯动效,无需额外使用AnimatedPositioned组件,滚动事件本身就会触发组件重绘。
你需要先确定两个基础阈值:

  • 完全展开时的AppBar最大高度,可根据封面图、大标题的布局需求自定义,示例中取240
  • 完全收起时的AppBar最小高度,即常规导航栏高度+状态栏高度,示例中取系统默认导航栏高度kToolbarHeight + 状态栏高度
    核心逻辑是通过 SliverPersistentHeaderDelegate 提供的shrinkOffset参数获取当前滚动偏移量,计算展开进度:
    progress = (shrinkOffset / (maxExtent - minExtent)).clamp(0.0, 1.0)
    progress 取值范围为0(完全展开)到1(完全收起),所有元素的位移、大小、透明度、颜色等动效都可以基于该值做插值计算。
    示例动效包含的几个核心变化对应逻辑:
  • 封面背景图:滚动时向上偏移,透明度随进度从1降到0
  • 标题:字号从大缩小到导航栏标题大小,同时从封面底部位置平滑移动到导航栏居中位置
  • 导航栏操作按钮:透明度随进度从0升到1
  • 导航栏背景:从透明渐变到主题色

核心代码

首先自定义SliverPersistentHeaderDelegate:

import 'package:flutter/material.dart';

class CustomAnimatedAppBarDelegate extends SliverPersistentHeaderDelegate {
  final double minHeight;
  final double maxHeight;
  final String title;
  final ImageProvider coverImage;

  CustomAnimatedAppBarDelegate({
    required this.minHeight,
    required this.maxHeight,
    required this.title,
    required this.coverImage,
  });

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    // 计算展开进度 0=完全展开 1=完全收起
    final progress = (shrinkOffset / (maxExtent - minExtent)).clamp(0.0, 1.0);
    final statusBarPadding = MediaQuery.of(context).padding.top;

    return Stack(
      fit: StackFit.expand,
      children: [
        // 背景封面图
        Positioned(
          top: -progress * 80, // 滚动时向上偏移80dp
          left: 0,
          right: 0,
          child: Opacity(
            opacity: 1 - progress,
            child: Image(
              image: coverImage,
              height: maxHeight,
              fit: BoxFit.cover,
            ),
          ),
        ),
        // 导航栏背景渐变
        Container(
          color: Theme.of(context).primaryColor.withOpacity(progress),
        ),
        // 标题过渡动效
        Positioned(
          left: 16 + progress * 44, // 从左侧16dp移动到导航栏标题位置(避开返回按钮)
          bottom: 20 + progress * (statusBarPadding + kToolbarHeight / 2 - 28), // 从底部移动到导航栏垂直居中
          child: Text(
            title,
            style: TextStyle(
              color: Colors.white, // 可根据需求调整颜色过渡逻辑
              fontSize: 30 - progress * 16, // 字号从30sp缩小到14sp
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
        // 导航栏返回按钮
        Positioned(
          left: 12,
          top: statusBarPadding + 4,
          child: Opacity(
            opacity: progress,
            child: IconButton(
              icon: const Icon(Icons.arrow_back_ios, color: Colors.white, size: 20),
              onPressed: () => Navigator.pop(context),
            ),
          ),
        ),
        // 导航栏右侧更多按钮
        Positioned(
          right: 12,
          top: statusBarPadding + 4,
          child: Opacity(
            opacity: progress,
            child: IconButton(
              icon: const Icon(Icons.more_horiz, color: Colors.white, size: 24),
              onPressed: () {},
            ),
          ),
        ),
      ],
    );
  }

  @override
  double get maxExtent => maxHeight;

  @override
  double get minExtent => minHeight;

  @override
  bool shouldRebuild(covariant CustomAnimatedAppBarDelegate oldDelegate) {
    return minHeight != oldDelegate.minHeight ||
        maxHeight != oldDelegate.maxHeight ||
        title != oldDelegate.title ||
        coverImage != oldDelegate.coverImage;
  }
}

使用示例

将自定义代理放到CustomScrollView的sliver列表中即可:

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

  @override
  Widget build(BuildContext context) {
    final statusBarHeight = MediaQuery.of(context).padding.top;
    final appBarMinHeight = kToolbarHeight + statusBarHeight;
    const appBarMaxHeight = 240.0;

    return Scaffold(
      body: CustomScrollView(
        physics: const BouncingScrollPhysics(),
        slivers: [
          SliverPersistentHeader(
            pinned: true, // 滚动到顶部时保持吸附
            delegate: CustomAnimatedAppBarDelegate(
              minHeight: appBarMinHeight,
              maxHeight: appBarMaxHeight,
              title: "演示页面标题",
              coverImage: const AssetImage("assets/cover.jpg"), // 替换为你的封面图资源
            ),
          ),
          // 页面其余内容区域
          SliverToBoxAdapter(
            child: Container(
              padding: const EdgeInsets.all(20),
              child: const Text(
                "页面内容区域\n\n可以任意扩展其他组件",
                style: TextStyle(fontSize: 16, height: 2),
              ),
            ),
          )
        ],
      ),
    );
  }
}

自定义调整说明

你可以根据实际动效需求,修改build方法中各个属性和progress的绑定逻辑:

  • 调整位移的距离,修改Positioned的top/left/bottom/right的插值参数
  • 调整字号、透明度、颜色的变化范围,修改对应属性的插值公式
  • 新增其他动效元素,只需要在Stack中添加对应的子组件,将需要变化的属性和progress绑定即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 11:45:08