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

如何为SliverPersistentHeader添加展开与折叠状态间的吸附效果?

为SliverPersistentHeader实现吸附(Snap)行为

需求是让SliverPersistentHeader在滚动结束后自动吸附到完全展开或完全折叠状态,不允许停留在中间过渡状态,效果类似PageController的页面切换吸附。

问题原因

原代码中使用了FloatingHeaderSnapConfiguration,但这个配置仅对设置了floating: true的SliverPersistentHeader生效,而你的代码里设置的是pinned: true,所以吸附逻辑完全不触发。

修复方案

  1. 将snapConfiguration替换为PinnedHeaderSnapConfiguration,适配pinned类型的头部
  2. 调整shrinkOffset的计算逻辑,确保吸附阈值判断准确
  3. 优化透明度计算,避免中间状态的视觉残留

完整修复代码

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: CustomScrollView(
        slivers: [
          SliverPersistentHeader(
            pinned: true,
            delegate: CustomAppBarDelegate(
              minExtent: kToolbarHeight,
              maxExtent: MediaQuery.of(context).size.height * 0.35,
              vsync: this,
              expandedWidget: Container(
                  height: MediaQuery.of(context).size.height * 0.35,
                  color: Colors.green,
                  child: const Center(child: Text('Header expanded'))),
              collapsedAppBar: Container(
                  height: kToolbarHeight,
                  color: Colors.blue,
                  child: const Center(child: Text('Header collapsed'))),
            ),
          ),
          ...List.generate(10, (index) => const SliverToBoxAdapter(child: Container(color: Colors.red, height: 100))),
        ],
      ),
    );
  }
}

class CustomAppBarDelegate extends SliverPersistentHeaderDelegate {
  const CustomAppBarDelegate({
    required this.minExtent,
    required this.maxExtent,
    required this.vsync,
    required this.expandedWidget,
    required this.collapsedAppBar,
    this.leading,
    this.trailing,
    this.backgroundColor,
  });

  @override
  final double minExtent;

  @override
  final double maxExtent;

  @override
  final TickerProvider vsync;

  final Widget expandedWidget;

  final Widget collapsedAppBar;

  final Widget? leading;

  final Widget? trailing;

  final Color? backgroundColor;

  // 计算可滚动的有效范围:最大高度 - 最小高度
  double get _scrollRange => maxExtent - minExtent;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    // 计算当前展开比例:0=完全折叠,1=完全展开
    final expansionRatio = 1.0 - min(shrinkOffset / _scrollRange, 1.0);
    
    // 吸附状态下直接使用0或1的比例,彻底消除中间过渡值
    final effectiveRatio = expansionRatio > 0.5 ? 1.0 : 0.0;

    return Stack(
      fit: StackFit.expand,
      children: [
        // 展开状态内容
        Opacity(
          opacity: effectiveRatio,
          child: Material(child: expandedWidget),
        ),
        // 折叠状态背景
        Opacity(
          opacity: 1 - effectiveRatio,
          child: ColoredBox(color: backgroundColor ?? Colors.grey),
        ),
        // 折叠状态AppBar
        Positioned(
          left: 0,
          right: 0,
          top: 0,
          child: Opacity(
            opacity: 1 - effectiveRatio,
            child: AppBar(
              systemOverlayStyle: const SystemUiOverlayStyle(
                statusBarIconBrightness: Brightness.dark,
                statusBarBrightness: Brightness.dark,
              ),
              shadowColor: Colors.transparent,
              backgroundColor: Colors.transparent,
              automaticallyImplyLeading: false,
              titleSpacing: 0,
              title: collapsedAppBar,
              centerTitle: true,
            ),
          ),
        ),
        if (leading != null)
          Positioned(
              top: MediaQuery.of(context).padding.top + 12,
              left: 16,
              child: Material(child: leading)),
        if (trailing != null)
          Positioned(
              top: MediaQuery.of(context).padding.top + 12,
              right: 16,
              child: Material(child: trailing))
      ],
    );
  }

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

  @override
  PinnedHeaderSnapConfiguration? get snapConfiguration => PinnedHeaderSnapConfiguration(
        curve: Curves.easeOut,
        duration: const Duration(milliseconds: 200),
        // 吸附阈值:滚动超过一半高度时自动展开,否则折叠
        snapThreshold: 0.5,
      );
}

关键修改点

  • 替换FloatingHeaderSnapConfiguration为PinnedHeaderSnapConfiguration,适配pinned模式
  • 新增_scrollRange计算可滚动有效范围,避免混入状态栏高度导致的计算偏差
  • 使用snapThreshold: 0.5设置吸附阈值:滚动超过一半高度时自动展开,否则折叠
  • 修改透明度计算逻辑,使用effectiveRatio直接取0或1,彻底消除中间过渡状态

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 09:54:57