kev
kev

Reputation: 2346

How to overlap SliverList on a SliverAppBar

I'm trying to overlap a SliverList a few pixels over the SliverAppBar. Similar to this post. I'd like the image in the FlexibleSpaceBar to go under the radius of my SliverList. I'm attempting to achieve the below.

enter image description here

I can only get the radius like so. Without the ability to overlap the SliverList onto he SliverAppBar. enter image description here

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            floating: false,
            expandedHeight: MediaQuery.of(context).size.height * 0.50,
            flexibleSpace: FlexibleSpaceBar(
              background: Image.network(pet.photos.first)
            ),
          ),
          SliverList(
            delegate: SliverChildListDelegate([
              Container(
                height: 40,
                decoration: BoxDecoration(
                  color: Colors.red,
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(30),
                    topRight: Radius.circular(30),
                  ),
                ),
              ),
            ]),
          )
        ],
      ),
    );
  }

Any direction would be appreciated, thanks!

Upvotes: 35

Views: 13738

Answers (4)

이민행
이민행

Reputation: 1

I tried to solve this problem but failed.

My solution is just giving up sliver app bar and make it on my own.

Jsut use stack widget. Make app bar by using container and wrap it with animated builder.

Put singlechildscrollview on it and connect appbar with scrollController.

You can make sliver app bar effect using scrollController just modifying height of container

Upvotes: 0

Mohsen Farshid
Mohsen Farshid

Reputation: 29

I found an way to achieve that:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
  debugShowCheckedModeBanner: false,
  title: 'Flutter Demo',
  theme: ThemeData(

  ),
  home: MyHomePage(title: 'Flutter Demo Home Page'),
  );
  }
  }

  class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);


  final String title;

   @override
  _MyHomePageState createState() => _MyHomePageState();
  }

  class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  AnimationController animController;
  Animation<double> animation;
  bool monhani = true;

  int _counter = 0;
  ScrollController _scrollController;

  @override
  void initState() {
  animController = AnimationController(
  duration: Duration(microseconds: 300000),
  vsync: this,
  );
  animation = Tween<double>(begin: 150.0, end: 0.0).animate(animController)
   ..addListener(() {
    setState(() {});
   });

   // TODO: implement initState
   super.initState();
  _scrollController = ScrollController()
  ..addListener(() {
    if (_scrollController.offset.toInt() >= 283 && monhani == true) {
      setState(() {
        animController.forward();
        monhani=false;
      });
    } else if(_scrollController.offset.toInt() <= 283) {
      animController.reverse();
      monhani=true;
    }
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    backgroundColor: Colors.lightBlue[100],
    body: CustomScrollView(
    controller: _scrollController,
    slivers: <Widget>[
      SliverAppBar(
        bottom: PreferredSize(
          child: Container(
            color: Colors.orange,
          ),
          preferredSize: Size(0, 60),
        ),
        pinned: true,
        expandedHeight: 400,
        automaticallyImplyLeading: true,
        primary: false,
        flexibleSpace: Stack(
          children: <Widget>[
            Center(
              child: Container(
                height: 400,
                width: 450,
                decoration: BoxDecoration(
                    // color: Colors.green,
                    image: DecorationImage(
                        image: NetworkImage(
                            "https://images.pexels.com/photos/62389/pexels-photo- 
           62389.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260"),
                        fit: BoxFit.cover)),
              ),
            ),
            Positioned(
              child: Container(
                height: 100,
                decoration: BoxDecoration(
                  color: Colors.lightBlue[100],
                  borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(animation.value)),
                ),
              ),
              bottom: -1,
              left: 0,
              right: 0,
            ),
          ],
        ),
      ),
      SliverList(
        delegate: SliverChildListDelegate([
          Column(
            children: <Widget>[
              Container(
                height: MediaQuery.of(context).size.height - 100,
                width: double.infinity,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  color: Colors.red,
                  borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(animation.value)),
                ),
              ),
              Container(
                height: MediaQuery.of(context).size.height - 100,
                width: double.infinity,
                alignment: Alignment.center,
                color: Colors.green,
              ),
              Container(
                height: MediaQuery.of(context).size.height - 100,
                width: double.infinity,
                alignment: Alignment.center,
                color: Colors.amber,
              ),
            ],
          )
        ]),
      ),
    ],
  ),
);
}
}

this image is result

Upvotes: 2

Eduardo Vital
Eduardo Vital

Reputation: 1445

I found an easier way to achieve that:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'How to overlap SliverList on a SliverAppBar',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            bottom: PreferredSize(
              child: Container(),
              preferredSize: Size(0, 20),
            ),
            pinned: false,
            expandedHeight: MediaQuery.of(context).size.height * 0.4,
            flexibleSpace: Stack(
              children: [
                Positioned(
                    child: Image(
                      fit: BoxFit.cover,
                      image: NetworkImage(
                        "https://images.pexels.com/photos/62389/pexels-photo-62389.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
                      ),
                    ),
                    top: 0,
                    left: 0,
                    right: 0,
                    bottom: 0),
                Positioned(
                  child: Container(
                    height: 30,
                    decoration: BoxDecoration(
                      color: Colors.lightBlue[100],
                      borderRadius: BorderRadius.vertical(
                        top: Radius.circular(50),
                      ),
                    ),
                  ),
                  bottom: -1,
                  left: 0,
                  right: 0,
                ),
              ],
            ),
          ),
          SliverFixedExtentList(
            itemExtent: 50.0,
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * (index + 1 % 9)],
                  child: Text('List Item $index'),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

You can see it in DartPad: https://dartpad.dev/e2976ad6ede98813b75ee44b1217cc96

The important part is to use a stack in the flexibleSpace that contains the content you wanna show as a positioned widget and another position widget with the border decoration.

You also need to use the bottom section of the SliverAppBar setting the preferred size as the same as the widget with border decoration.

The DartPad does not render the widget properly (it shows a margin at the bottom, but this does not happen in the iOS/Android).

Upvotes: 20

Rick
Rick

Reputation: 506

You can use the following widget which uses Stack and scroll listeners to achieve something like the screenshots. It's based on https://pub.dartlang.org/packages/sliver_fab.

The widget:

import 'package:flutter/material.dart';

class DetailScaffold extends StatefulWidget {
  final ScrollController controller;
  final ScrollPhysics physics;
  final List<Widget> slivers;

  final double expandedHeight;

  /// Changes edge behavior to account for [SliverAppBar.pinned].
  ///
  /// Hides the edge when the [ScrollController.offset] reaches the collapsed
  /// height of the [SliverAppBar] to prevent it from overlapping the app bar.
  final bool hasPinnedAppBar;

  DetailScaffold({
    @required this.expandedHeight,
    this.controller,
    this.physics,
    this.slivers,
    this.hasPinnedAppBar = false,
  }) {
    assert(expandedHeight != null);
    assert(hasPinnedAppBar != null);
  }

  @override
  _DetailScaffoldState createState() => _DetailScaffoldState();
}

class _DetailScaffoldState extends State<DetailScaffold> {
  ScrollController ctrl;

  @override
  void initState() {
    super.initState();

    ctrl = widget.controller ?? ScrollController();
    ctrl.addListener(() => setState(() {}));
  }

  @override
  void dispose() {
    if (widget.controller == null) {
      ctrl.dispose();
    }

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        CustomScrollView(
          controller: ctrl,
          physics: widget.physics,
          slivers: widget.slivers,
        ),
        _buildEdge(),
      ],
    );
  }

  _buildEdge() {
    var edgeHeight = 12.0;
    var paddingTop = MediaQuery.of(context).padding.top;

    var defaultOffset =
        (paddingTop + widget.expandedHeight) - edgeHeight;

    var top = defaultOffset;
    var edgeSize = edgeHeight;

    if (ctrl.hasClients) {
      double offset = ctrl.offset;
      top -= offset > 0 ? offset : 0;

      if (widget.hasPinnedAppBar) {
        // Hide edge to prevent overlapping the toolbar during scroll.
        var breakpoint =
            widget.expandedHeight - kToolbarHeight - edgeHeight;

        if (offset >= breakpoint) {
          edgeSize = edgeHeight - (offset - breakpoint);
          if (edgeSize < 0) {
            edgeSize = 0;
          }

          top += (edgeHeight - edgeSize);
        }
      }
    }

    return Positioned(
      top: top,
      left: 0,
      right: 0,
      child: Container(
        height: edgeSize,
        decoration: BoxDecoration(
          color: Theme.of(context).canvasColor,
          borderRadius: BorderRadius.vertical(
            top: Radius.circular(12),
          ),
        ),
      ),
    );
  }
}

Using it:

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var expandedHeight = 128.0;

    return DetailScaffold(
      expandedHeight: expandedHeight,
      slivers: <Widget>[
        SliverAppBar(
          expandedHeight: expandedHeight,
          flexibleSpace: FlexibleSpaceBar(
            background: Container(color: Colors.purple),
          ),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate((_, i) {
            return ListTile(title: Text('Item $i'));
          }, childCount: 50),
        ),
      ],
    );
  }
}

Upvotes: 13

Related Questions