dart 如何在Flutter中的SliverAppBar上覆盖可滚动的Sliver?

nkkqxpd9  于 2024-01-04  发布在  Flutter
关注(0)|答案(4)|浏览(220)

我有一个CustomScrollView与一个SliverAppBar和一些长条。我想覆盖一些图形上的SliverAppBar,并有他们滚动与其余的名单。
下面是我使用的代码:

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: SliverTest(),
    );
  }
}

class SliverTest extends StatefulWidget {
  const SliverTest({super.key});

  @override
  State<SliverTest> createState() => _SliverTestState();
}

class _SliverTestState extends State<SliverTest> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              expandedHeight: 350.0,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Container(
                  color: Colors.yellow,
                  child: const Center(child: Text('My Parallax Image!')),
                ),
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                height: 100,
                color: Colors.blueAccent,
                child: Stack(
                  children: [
                    Align(
                      alignment: const Alignment(0.0, -2.0),
                      child: Container(
                        width: 50,
                        height: 50,
                        decoration: const BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.red,
                        ),
                        child: const Center(child: Text('Red')),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    height: 50,
                    color: Colors.teal[100 * ((index % 8) + 1)],
                    child: Center(child: Text('Item #$index')),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

字符集
在这个设置中,我想要红色的圆圈(当前已剪裁)绘制在SliverAppBar的黄色部分顶部(请注意,黄色部分只是一些具有视差效果的实际图像的简单占位符,并且使用纯黄色仅仅是为了简单起见)我将红色圆圈放在一个长条内,因为我希望当用户与可滚动区域交互时,它能与列表的其余部分一起沿着。
有谁能提供一个简单的例子来说明如何在Flutter中实现这一点吗?我愿意接受任何其他使用slivers的解决方案,因为它们为我的真实的应用程序的其他部分提供了巨大的便利。否则,我知道我可能会在不使用slivers的情况下重新创建它。任何帮助都将不胜感激。提前感谢!

uqdfh47h

uqdfh47h1#

这里你有另一个没有使用滚动条/滚动条的示例,只是在玩大小:)。
测试结果:
x1c 0d1x的数据
代码:

class SliverTest extends StatefulWidget {
  const SliverTest({super.key});

  @override
  State<SliverTest> createState() => _SliverTestState();
}

class _SliverTestState extends State<SliverTest> {
  @override
  Widget build(BuildContext context) {
    const circleSize = 50.0;
    const expandedHeight = 350.0;
    const headerColor = Colors.yellow;
    return MaterialApp(
      home: Scaffold(
        backgroundColor: headerColor,
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              expandedHeight: expandedHeight - circleSize / 2,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Container(
                  color: headerColor,
                  child: const Center(child: Text('Yellow')),
                ),
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                color: Colors.blueAccent,
                height: 200, //any size
                child: Stack(
                  children: [
                    Positioned(
                      top: 0,
                      left: 0,
                      right: 0,
                      height: circleSize / 2,
                      child: Container(
                        color: headerColor,
                      ),
                    ),
                    Align(
                      alignment: Alignment.topCenter,
                      child: Container(
                        width: circleSize,
                        height: circleSize,
                        decoration: const BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.red,
                        ),
                        child: const Center(child: Text('Red')),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    height: 50,
                    color: Colors.teal[100 * ((index % 8) + 1)],
                    child: Center(child: Text('Item #$index')),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

字符集

更新

好吧,基于你最新的评论,我不得不更深入:),现在它的工作符合你最后的要求(但也更复杂)。
结果如下:



代码:

class SliverTest extends StatefulWidget {
  const SliverTest({super.key});

  @override
  State<SliverTest> createState() => _SliverTestState();
}

class _SliverTestState extends State<SliverTest> {
  final _scrollController = ScrollController();
  final _layerLink = LayerLink();
  final _expandedHeight = 350.0;

  final _circleSize = 50.0;
  double _visiblePercent = 1.0;
  OverlayEntry? _overlayEntry;

  void _onListen() {
    final total = _scrollController.offset + kToolbarHeight + _circleSize / 2;
    final difference = total - _expandedHeight;
    _visiblePercent =
        ((_circleSize - difference).clamp(0.0, _circleSize).toDouble() /
            _circleSize);
    _overlayEntry?.markNeedsBuild();
  }

  @override
  void initState() {
    _scrollController.addListener(_onListen);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _showCircularItem();
    });
    super.initState();
  }

  @override
  void dispose() {
    _scrollController.removeListener(_onListen);
    _scrollController.dispose();
    _overlayEntry?.remove();
    super.dispose();
  }

  void _showCircularItem() {
    _overlayEntry = OverlayEntry(
      builder: (BuildContext context) {
        return Positioned(
          top: 0,
          left: 0,
          right: 0,
          child: CompositedTransformFollower(
            link: _layerLink,
            offset: Offset(0.0, -_circleSize / 2),
            child: Material(
              color: Colors.transparent,
              child: ClipRect(
                clipper: _MyClipper(
                  visiblePercent: _visiblePercent,
                ),
                child: Container(
                  width: _circleSize,
                  height: _circleSize,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.red,
                  ),
                  child: const Center(child: Text('Red')),
                ),
              ),
            ),
          ),
        );
      },
    );
    Overlay.of(context).insert(_overlayEntry!);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          controller: _scrollController,
          slivers: [
            SliverAppBar(
              expandedHeight: _expandedHeight,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network(
                  'https://t4.ftcdn.net/jpg/05/49/86/39/360_F_549863991_6yPKI08MG7JiZX83tMHlhDtd6XLFAMce.jpg',
                  fit: BoxFit.cover,
                ),
              ),
            ),
            SliverToBoxAdapter(
              child: CompositedTransformTarget(
                link: _layerLink,
                child: Container(
                  color: Colors.greenAccent,
                  height: 200, //any size
                ),
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    height: 50,
                    color: Colors.teal[100 * ((index % 8) + 1)],
                    child: Center(child: Text('Item #$index')),
                  );
                },
                childCount: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _MyClipper extends CustomClipper<Rect> {
  _MyClipper({
    required this.visiblePercent,
  });

  final double visiblePercent;

  @override
  Rect getClip(Size size) {
    return Rect.fromLTRB(
      0.0,
      size.height * (1.0 - visiblePercent),
      size.width,
      size.height,
    );
  }

  @override
  bool shouldReclip(_MyClipper oldClipper) {
    return visiblePercent != oldClipper.visiblePercent;
  }
}

8fq7wneg

8fq7wneg2#

您可以考虑使用Stack widget将图形覆盖在SliverAppBar上。然而,挑战在于使图形随CustomScrollView一起沿着。您可以尝试调整图形的位置以响应CustomScrollView的滚动偏移。
使用NotificationListener监听滚动事件。根据滚动偏移量计算图形的位置。使用Stack并在用户滚动时动态调整图形的位置。

class _SliverTestState extends State<SliverTest> {
  double _offset = 0.0;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification scrollInfo) {
            setState(() {
              _offset = scrollInfo.metrics.pixels;
            });
            return true;
          },
          child: Stack(
            children: [
              CustomScrollView(
                slivers: [
                  SliverAppBar(
                    expandedHeight: 350.0,
                    floating: false,
                    pinned: true,
                    flexibleSpace: FlexibleSpaceBar(
                      background: Container(
                        color: Colors.yellow,
                        child: const Center(child: Text('Yellow')),
                      ),
                    ),
                  ),
                  SliverToBoxAdapter(
                    child: Container(
                      height: 100,
                      color: Colors.blueAccent,
                    ),
                  ),
                  // other slivers
                ],
              ),
              Positioned(
                top: 350 - _offset, // Adjust position based on scroll offset
                left: MediaQuery.of(context).size.width / 2 - 25,
                child: Container(
                  width: 50,
                  height: 50,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.red,
                  ),
                  child: const Center(child: Text('Red')),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

字符集
您可以看到NotificationListener侦听滚动事件并更新_offset变量。Stack小部件将红色圆圈覆盖在CustomScrollView上。Positioned小部件根据滚动偏移量调整红色圆圈的位置。
看看这是否会将红色圆圈放在SliverAppBar的顶部,并使其与列表的其余部分一起滚动。
有一些警告。
例如,由于图形(红色圆圈)实际上不是任何长条的一部分,它显然没有参与过度滚动效果,保持其固定位置,因此表现出一些设计缺陷,因此我在OP中强调。
另外,在检查它之后,我意识到红色圆圈需要在SliverAppBar后面,我坚持要覆盖在上面。有没有简单的解决方法?
为了满足让红色圆圈沿着滚动并出现在SliverAppBar后面的要求,您可以尝试将CustomScrollViewSliverPersistentHeader一起使用。SliverPersistentHeader将允许您创建一个自定义标题,其行为类似于SliverAppBar,但对其内容和布局有更多的控制。
因此,创建一个包含黄色背景和红色圆圈的SliverPersistentHeader。调整自定义标题中红色圆圈的位置,使其最初出现在所需位置,但仍与其余内容一起滚动。通过使用SliverPersistentHeader,红色圆圈在滚动时自然应放置在SliverAppBar的后面。

class _SliverTestState extends State<SliverTest> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              expandedHeight: 350.0,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('App Bar'),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _CustomHeaderDelegate(
                minHeight: 100.0,
                maxHeight: 350.0,
                child: Container(
                  color: Colors.yellow,
                  alignment: Alignment.topCenter,
                  child: Container(
                    width: 50,
                    height: 50,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: Colors.red,
                    ),
                    child: Center(child: Text('Red')),
                  ),
                ),
              ),
            ),
            // other slivers
          ],
        ),
      ),
    );
  }
}

class _CustomHeaderDelegate extends SliverPersistentHeaderDelegate {
  final double minHeight;
  final double maxHeight;
  final Widget child;

  _CustomHeaderDelegate({
    required this.minHeight,
    required this.maxHeight,
    required this.child,
  });

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => maxHeight;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(_CustomHeaderDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}


SliverAppBar应该保持固定在顶部,自定义标题在它下面向上滚动。
检查是否看到红色圆圈与列表的其余部分一起滚动,并停留在SliverAppBar后面。

zwghvu4y

zwghvu4y3#

这是根据我的理解为您的问题的解决方案。

class SliverTest extends StatefulWidget {
  const SliverTest({super.key});

  @override
  State<SliverTest> createState() => _SliverTestState();
}

class _SliverTestState extends State<SliverTest> {
  late ScrollController _scrollController;

  @override
  void initState() {
    _scrollController = ScrollController();
    super.initState();
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: CustomScrollView(
          controller: _scrollController,
          shrinkWrap: true,
          slivers: [
            SliverAppBar.large(
              pinned: true,
              floating: false,
              stretch: true,
              automaticallyImplyLeading: false,
              expandedHeight: 350.0,
              backgroundColor: Colors.purple,
              title: Center(
                child: Container(
                  width: 50,
                  height: 50,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.red,
                  ),
                  child: const Center(child: Text('Red')),
                ),
              ),
              flexibleSpace: FlexibleSpaceBar(
                background: Container(
                  color: Colors.yellow,
                  child: const Center(child: Text('Yellow')),
                ),
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                // height: 100,
                color: Colors.blueAccent,
                child: Stack(
                  children: [
                    SingleChildScrollView(
                      child: Column(
                        children: [
                          for (int i = 0; i < 20; i++) ...[
                            Container(
                              height: 50,
                              color: Colors.teal[100 * ((i % 8) + 1)],
                              child: Center(child: Text('Item #$i')),
                            )
                          ],
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
            // SliverList(
            //   delegate: SliverChildBuilderDelegate(
            //     (BuildContext context, int index) {
            //       return Container(
            //         height: 50,
            //         color: Colors.teal[100 * ((index % 8) + 1)],
            //         child: Center(child: Text('Item #$index')),
            //       );
            //     },
            //     childCount: 20,
            //   ),
            // ),
          ],
        ),
      ),
    );
  }
}

字符集

mwkjh3gx

mwkjh3gx4#

要让appBar不遮挡圆圈:您可以将SliverAppBar更改为SliverPersistentHeader,并在delegate部分自定义您的标题和红圈,这样红圈将始终位于顶部,不再被appBar遮挡。
如何根据列表的滚动或者appBar的滚动自定义红圈:如果你想按列表滚动,你只需要在列表中添加ScrollController,并监听scrollController。如果你想改变appBar的滚动,那么在构建函数的delegate部分,有一个选项shrinkOffset可用,你可以计算这个。

[更新2023/12/08]
source

插图

x1c 0d1x的数据

相关问题