如何在Flutter中实现带Tween字号动画的吸顶头部及下方吸顶搜索栏
Flutter 带搜索框的渐变字号吸顶头部实现方案
核心思路是通过CustomScrollView组合Sliver组件,监听滚动偏移量动态计算标题字号,同时用固定高度的SliverPersistentHeader实现搜索框吸顶效果,具体实现步骤如下:
- 配置滚动容器:使用
CustomScrollView作为根滚动容器,所有头部、搜索框、列表内容都以Sliver形式作为子组件传入 - 实现渐变字号的头部:使用
SliverAppBar作为头部载体,设置pinned: true保证头部不会完全滚出屏幕,在flexibleSpace的LayoutBuilder中获取当前滚动收缩偏移,计算收缩进度后通过Tween映射标题字号,核心计算逻辑示例:// 头部最大展开高度、最小收缩高度 const double maxHeaderHeight = 120; const double minHeaderHeight = kToolbarHeight; // 在LayoutBuilder中获取当前收缩偏移 final currentHeight = constraints.biggest.height; final shrinkProgress = ((maxHeaderHeight - currentHeight) / (maxHeaderHeight - minHeaderHeight)).clamp(0.0, 1.0); // 计算当前字号:展开时28sp,收缩时18sp final titleFontSize = Tween<double>(begin: 28, end: 18).transform(1 - shrinkProgress); - 实现吸顶搜索框:使用
SliverPersistentHeader承载搜索TextField,自定义SliverPersistentHeaderDelegate,将minExtent和maxExtent都设置为搜索框的固定高度(示例为56),同时设置pinned: true,即可保证搜索框滚动到头部下方后始终吸附固定 - 填充列表内容:下方的列表内容使用
SliverList或SliverGrid实现,正常传入子项即可
完整可运行Demo代码如下:
import 'package:flutter/material.dart'; class StickySearchHeaderDemo extends StatefulWidget { const StickySearchHeaderDemo({super.key}); @override State<StickySearchHeaderDemo> createState() => _StickySearchHeaderDemoState(); } class _StickySearchHeaderDemoState extends State<StickySearchHeaderDemo> { static const double maxHeaderHeight = 120; static const double minHeaderHeight = kToolbarHeight; static const double searchBarHeight = 56; @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: [ // 渐变标题头部 SliverAppBar( pinned: true, expandedHeight: maxHeaderHeight, automaticallyImplyLeading: false, flexibleSpace: LayoutBuilder( builder: (context, constraints) { final currentHeight = constraints.biggest.height; final shrinkProgress = ((maxHeaderHeight - currentHeight) / (maxHeaderHeight - minHeaderHeight)).clamp(0.0, 1.0); final titleFontSize = Tween<double>(begin: 28, end: 18).transform(1 - shrinkProgress); return Padding( padding: EdgeInsets.only(left: 16, bottom: 16, top: MediaQuery.of(context).padding.top + 8), child: Align( alignment: Alignment.bottomLeft, child: Text( "首页", style: TextStyle(fontSize: titleFontSize, fontWeight: FontWeight.bold), ), ), ); }, ), ), // 吸顶搜索框 SliverPersistentHeader( pinned: true, delegate: _SearchBarDelegate(height: searchBarHeight), ), // 列表内容 SliverList.builder( itemCount: 50, itemBuilder: (context, index) => ListTile(title: Text("列表项 $index")), ) ], ), ); } } class _SearchBarDelegate extends SliverPersistentHeaderDelegate { final double height; _SearchBarDelegate({required this.height}); @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( height: height, color: Theme.of(context).scaffoldBackgroundColor, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: const TextField( decoration: InputDecoration( hintText: "搜索内容", prefixIcon: Icon(Icons.search), border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(8))), contentPadding: EdgeInsets.zero ), ), ); } @override double get maxExtent => height; @override double get minExtent => height; @override bool shouldRebuild(covariant _SearchBarDelegate oldDelegate) { return oldDelegate.height != height; } }
内容的提问来源于stack exchange,提问作者Komal
相关产品推荐
相关产品推荐

