0

I have implemented a screen with the CustomScrollView, SliverAppBar and FlexibleSpaceBar like the following:

enter image description here

Now, I'm stuck trying to further expand the functionality by trying to replicate the following effect:

Expand image to fullscreen on scroll

Can something like this be done by using the slivers in Flutter?

Basically, I want the image in it's initial size when screen opens, but depending on scroll direction, it should animate -> contract/fade (keeping the list scrolling functionality) or expand to fullscreen (maybe to new route?).

Please help as I'm not sure in which direction I should go.

Here's the code for the above screen:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  static const double bottomNavigationBarHeight = 48;

  @override
  Widget build(BuildContext context) => MaterialApp(
        debugShowCheckedModeBanner: false,
        home: SliverPage(),
      );
}

class SliverPage extends StatefulWidget {
  @override
  _SliverPageState createState() => _SliverPageState();
}

class _SliverPageState extends State<SliverPage> {
  double appBarHeight = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        physics: AlwaysScrollableScrollPhysics(),
        slivers: <Widget>[
          SliverAppBar(
            centerTitle: true,
            expandedHeight: MediaQuery.of(context).size.height * 0.4,
            pinned: true,
            flexibleSpace: LayoutBuilder(builder: (context, boxConstraints) {
              appBarHeight = boxConstraints.biggest.height;
              return FlexibleSpaceBar(
                centerTitle: true,
                title: AnimatedOpacity(
                    duration: Duration(milliseconds: 200),
                    opacity: appBarHeight < 80 + MediaQuery.of(context).padding.top ? 1 : 0,
                    child: Padding(padding: EdgeInsets.only(bottom: 2), child: Text("TEXT"))),
                background: Image.network(
                  'https://images.pexels.com/photos/443356/pexels-photo-443356.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
                  fit: BoxFit.cover,
                ),
              );
            }),
          ),
          SliverList(delegate: SliverChildListDelegate(_buildList(40))),
        ],
      ),
    );
  }

  List _buildList(int count) {
    List<Widget> listItems = List();

    for (int i = 0; i < count; i++) {
      listItems.add(
          new Padding(padding: new EdgeInsets.all(20.0), child: new Text('Item ${i.toString()}', style: new TextStyle(fontSize: 25.0))));
    }

    return listItems;
  }
}
cobster
  • 105
  • 1
  • 12
  • something like this: `child: LayoutBuilder( builder: (context, constraints) { return CustomScrollView( slivers: [ SliverAppBar( pinned: true, expandedHeight: constraints.maxHeight, flexibleSpace: Image.asset('images/bg.jpg', fit: BoxFit.cover, height: constraints.maxHeight,), ), SliverList( delegate: SliverChildBuilderDelegate( (ctx, i) => Container(height: 64, color: i.isOdd? Colors.green : Colors.blue), childCount: 16, ), ), ], ); }, ),` ? – pskink Mar 12 '20 at 05:15
  • @pskink thanks, it's close! I don't want the header image to be fullscreen on start, is there any way I could set the 'initial size' when screen opens so the both image and list are visible? – cobster Mar 12 '20 at 06:18
  • check `controller` param, it gives you a way to `jumpTo()` to some position – pskink Mar 12 '20 at 06:21
  • @pskink Thanks, I tried with that also. I converted the widget to `stateful`, and trying to do that jump in the `initState`, but it gives me error because `ScrollController` isn't attached yet. Any other ideas? – cobster Mar 12 '20 at 06:38
  • ok, no need for jumpTo, check parameters of `ScrollController` constructor – pskink Mar 12 '20 at 06:55
  • but dont ask me what value to pass: it seems that you need constraints.maxHeight - heightOfSliverAppBar but i have no idea how to get heightOfSliverAppBar - i used hardcoded 64 but of course its a workaround – pskink Mar 12 '20 at 07:13
  • @pskink thanks, I used it like this `_scrollController = ScrollController(initialScrollOffset: WidgetsBinding.instance.window.physicalSize.height * 0.4);` – cobster Mar 12 '20 at 07:21
  • no, no, no its even worse workaround ;-) why `* 0.4`? better use `controller: ScrollController(initialScrollOffset: constraints.maxHeight),` - using physical height and some magic factor `0.4` will work only on your device and not on mine ;-) – pskink Mar 12 '20 at 07:31
  • well, like I said, I don't wan't to display full screen image on start, but like on preview, that's why I'm using that factor, so the image is initially covering i.e 1/4 of total screen height... – cobster Mar 12 '20 at 07:45
  • ok, but still don't use phisical pixels, use logical pixels like constraints.maxHeight / 2, otherwise it will work only on one device – pskink Mar 12 '20 at 08:10
  • @pskink thanks.. Also, I would gladly like to hear your input on the effect in the video above? Could it be done with slivers, or it would require a lot of custom code? – cobster Mar 12 '20 at 08:16
  • ask uncle google for `SliverPersistentHeader` – pskink Mar 12 '20 at 08:24
  • @pskink I'm working on solution, will post an update when I'm done! – cobster Mar 13 '20 at 08:45
  • @pskink yes I saw it, thank you very much. It's close and a good starting point, but actually I was looking I'm trying to refine it further to reflect the effect in the video above. I need the expanding part to have more _delay_, like `BouncingScollPhysics` and when user gesture slides past certain point, or depending on swipe down speed - only then expand it, otherwise fling it back to original size.. – cobster Mar 14 '20 at 07:46

1 Answers1

2

use CustomScrollView with SliverPersistentHeader

child: LayoutBuilder(
  builder: (context, constraints) {
    return CustomScrollView(
      controller: ScrollController(initialScrollOffset: constraints.maxHeight * 0.6),
      slivers: <Widget>[
        SliverPersistentHeader(
          pinned: true,
          delegate: Delegate(constraints.maxHeight),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (ctx, i) => Container(height: 100, color: i.isOdd? Colors.green : Colors.green[700]),
            childCount: 12,
          ),
        ),
      ],
    );
  },
),

the Delegate class used by SliverPersistentHeader looks like:

class Delegate extends SliverPersistentHeaderDelegate {
  final double _maxExtent;

  Delegate(this._maxExtent);

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    var t = shrinkOffset / maxExtent;
    return Material(
      elevation: 4,
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          Image.asset('images/bg.jpg', fit: BoxFit.cover,),
          Opacity(
            opacity: t,
            child: Container(
              color: Colors.deepPurple,
              alignment: Alignment.bottomCenter,
              child: Transform.scale(
                scale: ui.lerpDouble(16, 1, t),
                child: Text('scroll me down', 
                  style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white)),
              ),
            ),
          ),
        ],
      ),
    );
  }

  @override double get maxExtent => _maxExtent;
  @override double get minExtent => 64;
  @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
}
pskink
  • 23,874
  • 6
  • 66
  • 77