0

I have a CustomScrollView which holds

  • a SliverPersistentHeader as well as
  • a SliverList.

I apply some ShapeBorder to the Widget returned within the SliverPersistentHeaderDelegate.

I would now like my "underneath" placed SliverList to use some of the cut out area of the SliverPersistentHeader, like shown in this screenshot:

Red arrows indicate where I would like the SliverList to be placed

How is this possible?

This is my code:

import 'package:flutter/material.dart';

void main() => runApp(
    MediaQuery(data: MediaQueryData(), child: MaterialApp(home: MyApp())));

class MyApp extends StatefulWidget {
  // This widget is the root of your application.
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
            child: CustomScrollView(
      slivers: <Widget>[
        SliverPersistentHeader(
          pinned: true,
          delegate: CustomSliverPersistentHeader(),
        ),
        SliverList(delegate: SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            return Card(
                child: Padding(
              padding: EdgeInsets.all(10),
              child: Text('text $index'),
            ));
          },
        ))
      ],
    )));
  }
}

class CustomSliverPersistentHeader extends SliverPersistentHeaderDelegate {
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return LayoutBuilder(builder: (context, constraints) {
      return Container(
          decoration: ShapeDecoration(
              color: Colors.amber, shape: CustomShape(shrinkOffset)),
          height: constraints.maxHeight,
          child: Container());
    });
  }

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate _) => true;

  @override
  double get maxExtent => 400.0;

  @override
  double get minExtent => 100.0;
}

class CustomShape extends ShapeBorder {
  final double shrinkOffset;

  CustomShape(this.shrinkOffset);

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    double scrollOffset = 0.0;
    if (shrinkOffset <= 100) {
      scrollOffset = 100.00 - shrinkOffset;
    }

    Offset controllPoint1 = Offset(0, rect.size.height - scrollOffset);
    Offset endPoint1 = Offset(scrollOffset, rect.size.height - scrollOffset);
    Offset controllPoint2 =
        Offset(rect.size.width, rect.size.height - scrollOffset);
    Offset endPoint2 =
        Offset(rect.size.width, rect.size.height - scrollOffset * 2);

    return Path()
      ..lineTo(0, rect.size.height)
      ..quadraticBezierTo(
          controllPoint1.dx, controllPoint1.dy, endPoint1.dx, endPoint1.dy)
      ..lineTo(rect.size.width - scrollOffset, rect.size.height - scrollOffset)
      ..quadraticBezierTo(
          controllPoint2.dx, controllPoint2.dy, endPoint2.dx, endPoint2.dy)
      ..lineTo(rect.size.width, 0);
  }

  @override
  EdgeInsetsGeometry get dimensions => EdgeInsets.only(bottom: 0);

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) => null;

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {}

  @override
  ShapeBorder scale(double t) => this;
}
Jonathan Rhein
  • 1,616
  • 3
  • 23
  • 47
  • `SizedOverflowBox` maybe? – pskink Aug 16 '21 at 17:30
  • I'm able to a move up the List differently, but our header is covering Top item of ListView. i think the best way would be using stack. – Md. Yeasin Sheikh Aug 16 '21 at 18:59
  • Thank you @pskink! I tried applying it as parent to my `SliverList` which then had to be converted to a `ListView` and all of it had to become a `SliverToBoxAdapter` which moved my `List` up, but it behaves oddly and the rest of the `List` was cut off. Where and how exactly would you apply the `SizedOverflowBox` in the code above? – Jonathan Rhein Aug 16 '21 at 19:02
  • @YeasinSheikh, thank you! Is there a way to use `Stack` in such a way as to also smoothly change the `ShapeBorder` of the amberish area which is now achieved using `SliverPersistentHeader`? Could you point me to some example which implements the same sliver-behaviour using `Stack`? – Jonathan Rhein Aug 16 '21 at 19:06
  • I would use it as a root widget for sliverpersistentheader that has an overflowed yoellow container/material with your custom shape – pskink Aug 16 '21 at 19:08
  • But then I would loose all the benefits of `SliverPersistentHeader` like the constantly changing `shrinkOffset` property (which I need to adjust the `ShapeBorder` constantly) as well as the ability to `pin` the header if I replace it with "just" a `Container`, or am I getting you wrong? – Jonathan Rhein Aug 16 '21 at 19:24
  • Thank you, that works great! What a clever solution to simply make the rounded edge extend over the `rect.size.height`! Do you see any possibility to add a **background image** to the yellow area which has the same shape and transforms according to the scrolling? I tried adding the image as `child` in the `Container` as well as `image` property within `ShapeDecoration` but it is always rendered as straight rectangle. Adding it within its own `ClipPath` also did not help... any ideas? – Jonathan Rhein Aug 17 '21 at 15:05
  • 1
    so its not that clever ;-( - it seems that my original idea was better: but with `OverflowBox` (not `SizedOverflowBox`), check https://paste.ubuntu.com/p/CQ9BrjQ4j3/ (and honestly i am not sure why it works: `size: constraints.biggest + Offset(0, 100)` - imho there should not be fixed `100` here ...) – pskink Aug 18 '21 at 05:10
  • WOW! For me your former solution was already pretty clever ;-) Anyways, it works very well right now! Thank you very much! Also, how you handle the calculation of the `Path` is so much smoother now! Would you like to make it the official answer, or else I would submit as answer for future reference. – Jonathan Rhein Aug 18 '21 at 14:42
  • 1
    thanks a lot, check https://paste.ubuntu.com/p/ctSMCqxFdh/ - i changed `leftRect` / `rightRect` calculations and now it uses `size: constraints.biggest + Offset(0, max(0, 100 - shrinkOffset)),` instead of `size: constraints.biggest + Offset(0, 100),` - imho it looks better now - if you want you can use it in your self answer – pskink Aug 18 '21 at 15:07

1 Answers1

0

Thanks to @pskink's great help in this regard, I learned that I had to use an OverflowBox (which lets its child overflow itself):

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

void main() => runApp(
    MediaQuery(data: MediaQueryData(), child: MaterialApp(home: MyApp())));

class MyApp extends StatefulWidget {
  // This widget is the root of your application.
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
            child: CustomScrollView(
      slivers: <Widget>[
        SliverPersistentHeader(
          pinned: true,
          delegate: CustomSliverPersistentHeader(),
        ),
        SliverList(delegate: SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            return Card(
                child: Padding(
              padding: EdgeInsets.all(10),
              child: Text('text $index'),
            ));
          },
        ))
      ],
    )));
  }
}

class CustomSliverPersistentHeader extends SliverPersistentHeaderDelegate {
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return LayoutBuilder(builder: (context, constraints) {
      return OverflowBox(
        maxHeight: constraints.biggest.height + 100,
        alignment: Alignment.topCenter,
        child: SizedBox.fromSize(
          size: constraints.biggest + Offset(0, max(0, 100 - shrinkOffset)),
          // The following Container can be replaced by a ClipPath with a
          // ShapeBorderClipper, albeit at the expense of not being able to add
          // shadows and other fancy "Container" stuff ;-)
          child: Container(
            clipBehavior: Clip.antiAlias,
            decoration: ShapeDecoration(shape: CustomShape(shrinkOffset)),
            child: Container(
              color: Colors.amber,
            ),
          ),
        ),
      );
    });
  }

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate _) => true;

  @override
  double get maxExtent => 400.0;

  @override
  double get minExtent => 100.0;
}

class CustomShape extends ShapeBorder {
  final double shrinkOffset;

  CustomShape(this.shrinkOffset);

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    double scrollOffset = 0.0;
    if (shrinkOffset <= 100) {
      scrollOffset = 100.00 - shrinkOffset;
    }

    Offset controllPoint1 = Offset(0, rect.size.height - scrollOffset);
    Offset endPoint1 = Offset(scrollOffset, rect.size.height - scrollOffset);
    Offset controllPoint2 =
        Offset(rect.size.width, rect.size.height - scrollOffset);
    Offset endPoint2 =
        Offset(rect.size.width, rect.size.height - scrollOffset * 2);

    return Path()
      ..lineTo(0, rect.size.height)
      ..quadraticBezierTo(
          controllPoint1.dx, controllPoint1.dy, endPoint1.dx, endPoint1.dy)
      ..lineTo(rect.size.width - scrollOffset, rect.size.height - scrollOffset)
      ..quadraticBezierTo(
          controllPoint2.dx, controllPoint2.dy, endPoint2.dx, endPoint2.dy)
      ..lineTo(rect.size.width, 0);
  }

  @override
  EdgeInsetsGeometry get dimensions => EdgeInsets.only(bottom: 0);

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) => null;

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {}

  @override
  ShapeBorder scale(double t) => this;
}

Should you want to add some spacing in between the SliverPersistentHeader and the SliverList you can wrap the latter in a SliverPadding.

Jonathan Rhein
  • 1,616
  • 3
  • 23
  • 47