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

如何让SliverPersistentHeaderDelegate先折叠再让其余内容滚动?

问题:实现SliverPersistentHeader先完全折叠再让内容滚动的效果

当前代码中,滚动时绿色头部栏与黄色内容区域会同时移动,需求是让绿色头部栏先完全折叠到最小高度,之后黄色内容才开始滚动到绿色栏下方。

当前代码

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        physics: const PageScrollPhysics(),
        slivers: [
          SliverPersistentHeader(
            pinned: true,
            delegate: SliverAppBarDelegate(),
          ),
          const SliverToBoxAdapter(
            child: SizedBox(height: 24),
          ),
          SliverPersistentHeader(
            pinned: true,
            delegate: SliverGreenMenuBarDelegate(),
          ),
          const SliverPadding(padding: EdgeInsets.only(bottom: 15)),
          SliverFillRemaining(
            child: SizedBox(
              height: 200,
              child: Container(
                color: Colors.amber,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  SliverAppBarDelegate();

  var expandedHeight = 220.0;

  @override
  double get maxExtent => expandedHeight;

  @override
  double get minExtent => kToolbarHeight + 20;

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

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    var colorScheme = Theme.of(context).colorScheme;

    double radius() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 20.0;
      } else {
        return 20 - shrinkOffset / 11;
      }
    }

    final Widget appBar = FlexibleSpaceBar.createSettings(
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      child: AppBar(
        actions: [
          IconButton(
            padding: const EdgeInsets.all(0),
            constraints: const BoxConstraints(),
            onPressed: () {},
            icon: const Icon(Icons.abc),
          ),
          Padding(
            padding: const EdgeInsets.only(right: 16, left: 35),
            child: IconButton(
              padding: const EdgeInsets.all(0),
              constraints: const BoxConstraints(),
              onPressed: () {},
              icon: const Icon(Icons.ac_unit_sharp),
            ),
          ),
        ],
        flexibleSpace: const FlexibleSpaceBar(
          expandedTitleScale: 1,
          titlePadding: EdgeInsets.only(top: 4, left: 56, right: 56),
          title: SafeArea(
            child: Text("Hello world"),
          ),
        ),
        elevation: 10,
        shadowColor: colorScheme.secondary.withOpacity(0.2),
        primary: true,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.only(
            bottomLeft: Radius.circular(radius()),
            bottomRight: Radius.circular(radius()),
          ),
        ),
      ),
    );
    return appBar;
  }
}

class SliverGreenMenuBarDelegate extends SliverPersistentHeaderDelegate {
  @override
  double get maxExtent => 75.0;

  @override
  double get minExtent => 40.0;

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

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    double horizontalPadding() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 25.0;
      } else {
        return 25 - (shrinkOffset / 3);
      }
    }

    double cornerRadius() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 15.0;
      } else {
        return 15 - shrinkOffset / 5;
      }
    }

    return FlexibleSpaceBar.createSettings(
      maxExtent: maxExtent,
      minExtent: minExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      isScrolledUnder: true,
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: horizontalPadding()),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.green,
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(cornerRadius()),
              topRight: Radius.circular(cornerRadius()),
              bottomLeft: const Radius.circular(15),
              bottomRight: const Radius.circular(15),
            ),
          ),
        ),
      ),
    );
  }
}

当前滚动效果

当前滚动效果

期望效果

滚动前滚动后
滚动前效果滚动后效果

修改方案

核心是用NestedScrollView分离顶部固定栏与下方可折叠栏+内容,让下方的绿色栏优先完成折叠后,内容才开始滚动。具体修改如下:

  1. 将原CustomScrollView替换为NestedScrollView,把顶部AppBar和间隔放在headerSliverBuilder中;
  2. 把绿色栏、间隔和黄色内容放在NestedScrollView的body内部的CustomScrollView中;
  3. 调整滚动物理效果为ClampingScrollPhysics,保证滚动连贯性;
  4. 移除SliverFillRemaining内部的多余SizedBox,避免高度冲突。

修改后的完整代码

import 'dart:math' as math;
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        physics: const ClampingScrollPhysics(),
        headerSliverBuilder: (context, innerBoxIsScrolled) => [
          SliverPersistentHeader(
            pinned: true,
            delegate: SliverAppBarDelegate(),
          ),
          const SliverToBoxAdapter(
            child: SizedBox(height: 24),
          ),
        ],
        body: CustomScrollView(
          physics: const ClampingScrollPhysics(),
          slivers: [
            SliverPersistentHeader(
              pinned: true,
              delegate: SliverGreenMenuBarDelegate(),
            ),
            const SliverPadding(padding: EdgeInsets.only(bottom: 15)),
            SliverFillRemaining(
              child: Container(
                color: Colors.amber,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  SliverAppBarDelegate();

  var expandedHeight = 220.0;

  @override
  double get maxExtent => expandedHeight;

  @override
  double get minExtent => kToolbarHeight + 20;

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

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    var colorScheme = Theme.of(context).colorScheme;

    double radius() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 20.0;
      } else {
        return 20 - shrinkOffset / 11;
      }
    }

    final Widget appBar = FlexibleSpaceBar.createSettings(
      minExtent: minExtent,
      maxExtent: maxExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      child: AppBar(
        actions: [
          IconButton(
            padding: const EdgeInsets.all(0),
            constraints: const BoxConstraints(),
            onPressed: () {},
            icon: const Icon(Icons.abc),
          ),
          Padding(
            padding: const EdgeInsets.only(right: 16, left: 35),
            child: IconButton(
              padding: const EdgeInsets.all(0),
              constraints: const BoxConstraints(),
              onPressed: () {},
              icon: const Icon(Icons.ac_unit_sharp),
            ),
          ),
        ],
        flexibleSpace: const FlexibleSpaceBar(
          expandedTitleScale: 1,
          titlePadding: EdgeInsets.only(top: 4, left: 56, right: 56),
          title: SafeArea(
            child: Text("Hello world"),
          ),
        ),
        elevation: 10,
        shadowColor: colorScheme.secondary.withOpacity(0.2),
        primary: true,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.only(
            bottomLeft: Radius.circular(radius()),
            bottomRight: Radius.circular(radius()),
          ),
        ),
      ),
    );
    return appBar;
  }
}

class SliverGreenMenuBarDelegate extends SliverPersistentHeaderDelegate {
  @override
  double get maxExtent => 75.0;

  @override
  double get minExtent => 40.0;

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

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    double horizontalPadding() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 25.0;
      } else {
        return 25 - (shrinkOffset / 3);
      }
    }

    double cornerRadius() {
      if (!overlapsContent && shrinkOffset == 0.0) {
        return 15.0;
      } else {
        return 15 - shrinkOffset / 5;
      }
    }

    return FlexibleSpaceBar.createSettings(
      maxExtent: maxExtent,
      minExtent: minExtent,
      currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
      isScrolledUnder: true,
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: horizontalPadding()),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.green,
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(cornerRadius()),
              topRight: Radius.circular(cornerRadius()),
              bottomLeft: const Radius.circular(15),
              bottomRight: const Radius.circular(15),
            ),
          ),
        ),
      ),
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 10:55:17