How to mask-out the overlaped section, visible through the "translucent header sliver" in the NestedScrollView?

Viewed 718

The following code yields a scrollable list together with a "translucent pinned sliver header".

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return [
              SliverPersistentHeader(
                delegate: _SliverPersistentHeaderDelegate(),
                pinned: true,
              ),
            ];
          },
          body: ListView.builder(
            itemBuilder: (context, index) {
              return ListTile(
                title: Container(
                  color: Colors.amber.withOpacity(0.3),
                  child: Text('Item $index'),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

class _SliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.blue.withOpacity(0.75),
      child: Placeholder(),
    );
  }

  @override double get maxExtent => 300;
  @override double get minExtent => 200;
  @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
}

It's all good; except, I need the "header" to be transparent, but having it translucent, causes the underneathed list-items to get revealed (as per the screenshot below).

So, how to "mask-out" the "list items" that are visible through the "translucent header"?

The items are visible through the "pinned sliver header"

2 Answers

How about using CustomClipper for List itself? Because the list height is dynamic during scrolling, the clip height must be calculated dynamically. So I pass the clipHeight into the custom clipper.

To get the clipHeight, I use MediaQuery.of(context).size.height - header height. So I create another class to get this value.

      ...
      body: CustomWidget (
        child: ListView.builder(
        ...


class CustomWidget extends StatelessWidget {

 final Widget child;

 CustomWidget({this.child,Key key}):super(key:key);

  @override
  Widget build(BuildContext context) {
    return ClipRect(
      clipper: MyCustomClipper(clipHeight: MediaQuery.of(context).size.height-200),
      child: child,
    );
  }
}

class MyCustomClipper extends CustomClipper<Rect>{

  final double clipHeight;

  MyCustomClipper({this.clipHeight});

  @override
  getClip(Size size) {
    double top = math.max(size.height - clipHeight,0) ;
    Rect rect = Rect.fromLTRB(0.0, top, size.width, size.height);
    return rect;
  }

  @override
  bool shouldReclip(CustomClipper oldClipper) {
    return false;
  }
}

Pinned SliverPersistentHeader works like "CSS position: absolute". So your body widget doesn't know that something is upon it. One of the option is to not to use the SliverPersistentHeader.

enter image description here

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

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  ScrollController controller;

  @override
  void initState() {
    currentHeight = _maxExtent;
    controller = ScrollController();
    controller.addListener(() {
      _updateHeaderHeight();
    });
    super.initState();
  }

  _updateHeaderHeight() {
    double offset = controller.offset;
    if (offset <= _maxExtent - _minExtent) {
      setState(() {
        currentHeight = math.max(_maxExtent - offset, _minExtent);
      });
    }
  }

  double currentHeight;
  final double _maxExtent = 300;
  final double _minExtent = 200;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: DecoratedBox(
          // only to prove transparency
          decoration: BoxDecoration(
            image: DecorationImage(
              colorFilter: ColorFilter.mode(Colors.white, BlendMode.color),
              image: NetworkImage(
                'https://picsum.photos/720/1280',
              ),
              fit: BoxFit.cover,
            ),
          ),
          child: Stack(
            children: [
              Header(currentHeight: currentHeight),
              Padding(
                padding: EdgeInsets.only(top: currentHeight),
                child: Container(
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.blueAccent),
                  ),
                  child: ListView.builder(
                    controller: controller,
                    itemBuilder: (context, index) {
                      return ListTile(
                        title: Container(
                          color: Colors.amber.withOpacity(0.3),
                          child: Text('Item $index'),
                        ),
                      );
                    },
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Header extends StatelessWidget {
  const Header({Key key, this.currentHeight}) : super(key: key);

  final double currentHeight;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: currentHeight,
      color: Colors.blue.withOpacity(0.75),
      child: Placeholder(),
    );
  }
}
Related