如何使用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
相关产品推荐
相关产品推荐

