Flutter, custom scroll effects

2,028

It's hard to understand slivers logic of from the first look.

But what is important is SliverGeometry class

  • paintOrigin - think about it as kind of delta y. When you want to make widget fixed on a screen, you need to push it from the top.
  • constraints.scrollOffset shows scroll offset of logical place of widget.
  • scrollExtent shows logical height of widget. It help widget to know that you scrolled all slivers.

enter image description here

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        body: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey _key = GlobalKey();

  RenderObject ansestor;
  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(_getPosition);

    super.initState();
  }

  _getPosition(_) {
    setState(() {
      ansestor = _key.currentContext.findRenderObject();
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      return CustomScrollView(
        physics: ClampingScrollPhysics(),
        key: _key,
        slivers: <Widget>[
          CustomSliver(
            isInitiallyExpanded: true,
            ansestor: ansestor,
            child: _Item(
              title: 'first title',
              fileName: 'item_1',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'second title',
              fileName: 'item_2',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'third title',
              fileName: 'item_3',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'fourth title',
              fileName: 'item_4',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'fifth title',
              fileName: 'item_5',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'first title',
              fileName: 'item_6',
            ),
          ),
          SliverToBoxAdapter(
            child: Container(
              child: Center(
                child: Text('end'),
              ),
              height: 1200,
              color: Colors.green.withOpacity(0.3),
            ),
          ),
        ],
      );
    });
  }
}

class CustomSliver extends SingleChildRenderObjectWidget {
  CustomSliver({
    this.child,
    Key key,
    this.ansestor,
    this.isInitiallyExpanded = false,
  }) : super(key: key);

  final RenderObject ansestor;
  final bool isInitiallyExpanded;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return CustomRenderSliver(
      isInitiallyExpanded: isInitiallyExpanded,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    CustomRenderSliver renderObject,
  ) {
    renderObject.ansestor = ansestor;
    renderObject.markNeedsLayout();
  }

  final Widget child;
}

class CustomRenderSliver extends RenderSliverSingleBoxAdapter {
  CustomRenderSliver({
    RenderBox child,
    this.isInitiallyExpanded,
  }) : super(child: child);

  final double max = 250;
  final double min = 100;

  RenderObject ansestor;
  final bool isInitiallyExpanded;
  void performLayout() {
    var constraints = this.constraints;

    double distanceToTop;

    double maxExtent;

    if (ansestor != null) {
      distanceToTop = child.localToGlobal(Offset.zero, ancestor: ansestor).dy;
    }

    if (ansestor == null) {
      if (isInitiallyExpanded) {
        maxExtent = max;
      } else {
        maxExtent = min;
      }
    } else {
      if (constraints.scrollOffset > 0) {
        maxExtent = (max - constraints.scrollOffset).clamp(0.0, max);
      } else if (distanceToTop < max) {
        maxExtent = min + (3 * (250 - distanceToTop) / 5);
      } else {
        maxExtent = min;
      }
    }

    child.layout(
      constraints.asBoxConstraints(maxExtent: maxExtent),
      parentUsesSize: true,
    );

    var paintExtent = math.min(maxExtent, constraints.remainingPaintExtent);

    geometry = SliverGeometry(
      paintOrigin: maxExtent == 0 ? 0.0 : constraints.scrollOffset,
      scrollExtent: max,
      paintExtent: paintExtent,
      maxPaintExtent: paintExtent,
      hasVisualOverflow: true,
    );

    constraints = constraints.copyWith(remainingPaintExtent: double.infinity);
    setChildParentData(child, constraints, geometry);
  }
}

class _Item extends StatelessWidget {
  const _Item({
    Key key,
    @required this.title,
    @required this.fileName,
  }) : super(key: key);

  final String title;
  final String fileName;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Container(
          height: 250,
          decoration: BoxDecoration(
            image: DecorationImage(
              image: AssetImage('assets/images/$fileName.png'),
              fit: BoxFit.fitWidth,
            ),
          ),
          child: Padding(
            padding: const EdgeInsets.only(top: 40),
            child: Text(
              title,
              style: Theme.of(context).textTheme.headline4.copyWith(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                    fontSize: 60,
                  ),
            ),
          ),
        );
      },
    );
  }
}
Share:
2,028
Renato Stauffer
Author by

Renato Stauffer

iOS Dev and Swift ninja! Interested in ML Strange stuff: I don't like beer ¯\_(ツ)/¯ I eat the popcorn before the movie starts ¯\(ツ)_/¯

Updated on December 03, 2022

Comments

  • Renato Stauffer
    Renato Stauffer over 1 year

    I would like to implement a layout like in this video (at 5:50) https://www.youtube.com/watch?v=KYUTQQ1usZE&index=1&list=PL23Revp-82LKxKN9SXqQ5Nxaa1ZpYEQuaadd#t=05m50s

    How would you tackle this? I tried with a ListView & GridLayout, but this seems to be limited to archive this. Would I need to use something like CustomMultiChildLayout (https://docs.flutter.io/flutter/widgets/CustomMultiChildLayout-class.html) or maybe a CustomScrollView (https://docs.flutter.io/flutter/widgets/CustomScrollView-class.html)? Any suggestions would be appreciated, thx :)

    Update: As far as I could find out, I would need to use a CustomScrollView (Correct me if I am wrong). But I am a bit overwhelmed with the options that the Flutter framework leaves me. And I am not sure from the documentation what classes I need to extend or which interfaces I would need to implement to archive my goal. I dont't know how deep I need to dive into the framework. There are the following classes involved when it comes to slivers and lists with custom scroll effects:

    • RenderSliver This is really the base for render objects which implement scroll effects. I guess it would be overkill to reimplement this. But maybe subclass it and start from there (maybe overkill too)?
    • RenderSliverMultiBoxAdaptor If we go higher in the hierarchy we find the abstract class RenderSliverMultiBoxAdaptor. A sliver with multiple box children. A RenderSliverBoxChildManager This provides children on the fly for the RenderSliverMultiBoxAdaptor. These are both abstract classes. So maybe start here and extend these classes?
    • RenderSliverList This extends the RenderSliverMultiBoxAdaptor and provides box children laid out along the main axis. The children are delivered by a class which implement RenderSliverBoxChildManager. SliverMultiBoxAdaptorElement implements RenderSliverBoxChildManager. So RenderSliverList and SliverMultiBoxAdaptorElement are a concrete implementation of RenderSliverMultiBoxAdaptor and RenderSliverBoxChildManager. I thought that I could extend these classes. But if I do so, I would anyway have to reimplement the performLayout method. So maybe reuse the SliverMultiBoxAdaptorElement and extend RenderSliverMultiBoxAdaptor?
    • SliverList This class eventually creates the render object (a RenderSliverList with a SliverMultiBoxAdaptorElement as a child manager) and provides a SliverChildDelegate to the SliverMultiBoxAdaptorElement, which in turn lazily builds children for SliverMultiBoxAdaptorWidget. The SliverList places multiple box children in a linear array along the main axis. It uses a class that extends SliverChildDelegate to provide children on the fly. It can be placed inside a CustomScrollViews slivers array. This is the most concrete sliver which creates a list in a CustomScrollView. So could I also archive my goal to have a layout according to the video simply with this? So far I tried to provide the CustomScrollView a ScrollController to intercept the scroll offset and then build the child elements according to the scroll offset and the index of the element with a SliverChildBuilderDelegate. But when doing so, the scrollview does not scroll anymore. It only scrolls, when the total height of all cells exceeds the viewport.

    So do I really have to extend RenderSliverMultiBoxAdaptor and implement the perfromLayout method myself? For me it seems to be the only option now...

    • Thomas
      Thomas about 6 years
      why not monitoring if a ListViewItem is the second from top and change it's height proprtional to the scrolling?
    • Omar Awamry
      Omar Awamry over 5 years
      i'm facing the same issue of not being able to customize the scroll, i'm thinking of doing it myself using the class Scrollable, but unfortunately there isn't enough documentation for it yet.
    • braulio.cassule
      braulio.cassule about 5 years
      Can you please share the result of what you accomplished?
    • Dominik Roszkowski
      Dominik Roszkowski over 4 years
      Thanks for asking this question! The list provided by you is really helpful in understanding what's possible with slivers :)