tp7309 / flutter_sticky_and_expandable_list

粘性头部与分组列表Sliver实现 Build a grouped list, which support expand/collapse section and sticky headers, support use it with sliver widget.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

NestedScrollView SliverAppBar and sticky header overlap

aaqibismail opened this issue · comments

This package has been really great for creating complex applications but I've had trouble now implementing the SliverExpandableList with a NestedScrollView. Specifically, the sticky header overlaps with the SliverAppBar I have in the headerSliver section of the NestedScrollView when the pin option is set to true. If I set pin to false there will no longer be any overlap but for the purposes of my application I need the appbar to be pinned.

I saw in another issue someone mentioned something similar and they used a SafeArea widget around the CustomScrollView to circumvent this, however this only fixes this issue when pin is set to false. Similarly, using a regular appbar within the scaffold with the toolbarheight set to 0 has the same effect as using the SafeArea widget. I tested with the example application listed to find a solution to no avail. This is what it looks with a SafeArea widget around the CustomScrollView vs without:

As you can see while the SafeArea widget does fix the header from being trapped in the statusbar, it is now stuck within the sliverappbar.
After reading the NestedScrollView documentation here are the series of steps I've tried:

  1. According to the NestedScrollView documentation, SliverOverlapAbsorber and SliverOverlapInjector should stop the body from scrolling underneath the sliverappbar, however this does not seem to be the case. It still refuses to respect the sliverappbar.

  2. Setting floating to true instead of pinned does not work as intended either. The sliverappbar never appears when scrolling upwards even if I set "floatHeaderSlivers" of NestedScrollView to true.

  3. Setting floating and snapped to true also does not resolve this issue. While it does show the sliverappbar when I scroll up now, it completely overlaps the body. Additionally, setting "floatHeaderSlivers" of NestedScrollView to true with these options does not change this behavior.
    image

Any ideas on how to solve this issue? It would be greatly appreciated. The code below is based off the code from the example in this package.

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

class _MyAppState extends State<MyApp> {
  var sectionList = MockData.getExampleSections();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        // appBar: AppBar(
        //   toolbarHeight: 0,
        // ),
        body: NestedScrollView(
          // floatHeaderSlivers: true,
          headerSliverBuilder: (context, innerBoxIsScrolled) => [
            // SliverOverlapAbsorber(
            //   handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
            //   sliver:
            SliverAppBar(
              backgroundColor: Colors.transparent,
              // pinned: true,
              floating: true,
              snap: true,
              expandedHeight: 200,
              flexibleSpace: FlexibleSpaceBar(
                title: Text("Sliver Example"),
              ),
            ),
            // ),
          ],
          body: SafeArea(
            child: Builder(
              builder: (context) => CustomScrollView(
                slivers: <Widget>[
                  // SliverOverlapInjector(
                  //   handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
                  //       context),
                  // ),
                  SliverExpandableList(
                    builder:
                        SliverExpandableChildDelegate<String, ExampleSection>(
                      sectionList: sectionList,
                      headerBuilder: _buildHeader,
                      itemBuilder: (context, sectionIndex, itemIndex, index) {
                        String item =
                            sectionList[sectionIndex].items[itemIndex];
                        return ListTile(
                          leading: CircleAvatar(
                            child: Text("$index"),
                          ),
                          title: Text(item),
                        );
                      },
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
[√] Flutter (Channel stable, 1.22.2, on Microsoft Windows [Version 10.0.19041.610], locale en-US)
    • Flutter version 1.22.2 at C:\flutter
    • Framework revision 84f3d28555 (3 weeks ago), 2020-10-15 16:26:19 -0700
    • Engine revision b8752bbfff
    • Dart version 2.10.2

 
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at C:\Users\aaqib\AppData\Local\Android\sdk
    • Platform android-30, build-tools 30.0.2
    • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)
    • All Android licenses accepted.

[!] Android Studio (version 4.1.0)
    • Android Studio at C:\Program Files\Android\Android Studio
    X Flutter plugin not installed; this adds Flutter specific functionality.
    X Dart plugin not installed; this adds Dart specific functionality.
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)

[√] VS Code (version 1.50.1)
    • VS Code at C:\Users\aaqib\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.16.0

[√] Connected device (1 available)
    • sdk gphone x86 (mobile) • emulator-5554 • android-x86 • Android 11 (API 30) (emulator)

! Doctor found issues in 1 category.

Option 1:
put SliverAppBar into CustomScrollView.
Option 2:
try extended_nested_scroll_view, maybe work.

Option 1:
put SliverAppBar into CustomScrollView.
Option 2:
try extended_nested_scroll_view, maybe work.

Option 1 won't help too much in my case because then I don't need the NestedScrollView anyways. Option 2 also didn't change this behavior, both the regular and extended NestedScrollViews have the same issue.

add NestedScrollView Example, maybe it can help you.

Thanks for the example. I tried adapting the example for my use-case and while it technically worked, but from a UX standpoint the transition between the SliverAppbars wasn't very fluid or smooth; it felt a little distracting. I tried using an animated switcher to switch the height of the 2nd SliverAppbar when it was supposed to be pinned but it didn't help much. Is there any way to use a this package with a NestedScrollView without 2 SliverAppbars? Here's a screen recording of what I am referring to. Thank you for the help.

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

import 'mock_data.dart';

class NestedScrollViewTest extends StatelessWidget {
  const NestedScrollViewTest({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter sticky and expandable list',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ExampleNestedScrollView(),
    );
  }
}

class ExampleNestedScrollView extends StatefulWidget {
  const ExampleNestedScrollView({Key key}) : super(key: key);

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

class _ExampleNestedScrollViewState extends State<ExampleNestedScrollView>
    with TickerProviderStateMixin {
  List<ExampleSection> sectionList = MockData.getExampleSections();
  final GlobalKey<NestedScrollViewState> nestedScrollKey = GlobalKey();
  final double _expandedHeight = 200;
  bool _isPinnedTitleShown = false;

  @override
  void initState() {
    super.initState();
    var headerContentHeight = _expandedHeight;
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      outerController.addListener(() {
        var pinned = outerController.offset >= headerContentHeight;
        if (_isPinnedTitleShown != pinned) {
          setState(() {
            _isPinnedTitleShown = pinned;
          });
        }
        print("outerController position: $outerController $kToolbarHeight");
      });
    });
  }

  ScrollController get outerController {
    return nestedScrollKey.currentState.outerController;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        key: nestedScrollKey,
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              // backgroundColor: Colors.white,
              expandedHeight: _expandedHeight,
              leading: IconButton(
                icon: const Icon(Icons.menu),
                onPressed: () {},
              ),
              title: Container(
                alignment: Alignment.center,
                margin: const EdgeInsets.only(
                  left: 24,
                  right: 8,
                ),
                height: 30,
                decoration: BoxDecoration(
                  color: const Color.fromRGBO(220, 244, 243, .63),
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Text(
                  'Search Tasks',
                  textAlign: TextAlign.center,
                  style: Theme.of(context)
                      .primaryTextTheme
                      .bodyText1
                      .copyWith(color: Colors.white70, fontSize: 16),
                ),
              ),
              flexibleSpace: FlexibleSpaceBar(
                background: Stack(
                  children: [
                    Align(
                      child: Container(
                        height: 56 + 0.4 * MediaQuery.of(context).size.height,
                        width: MediaQuery.of(context).size.width,
                        decoration: const BoxDecoration(
                          gradient: LinearGradient(
                            begin: Alignment.topCenter,
                            end: Alignment.bottomCenter,
                            colors: [Color(0xFF6EE2F5), Color(0xFF6E96F5)],
                          ),
                        ),
                      ),
                    ),
                    Align(
                      alignment: const Alignment(0, 0.5),
                      child: Container(
                        padding: const EdgeInsets.all(4),
                        decoration: BoxDecoration(
                          gradient: const LinearGradient(
                            begin: Alignment.topLeft,
                            end: Alignment.bottomRight,
                            colors: [Color(0xFFFFEEF4), Color(0xFFE3FCFF)],
                          ),
                          borderRadius: BorderRadius.circular(100),
                          boxShadow: const [
                            BoxShadow(color: Color.fromRGBO(0, 0, 0, 0.16))
                          ],
                        ),
                        child: Container(
                          alignment: Alignment.center,
                          child: RichText(
                            textAlign: TextAlign.center,
                            text: TextSpan(
                              children: <TextSpan>[
                                TextSpan(
                                  text: '${(1 * 100).round()}%\n',
                                  style: TextStyle(
                                    color: const Color(0xFF5C74BC),
                                    fontSize:
                                        (1 * 100).round() == 100 ? 28 : 32,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                                const TextSpan(
                                  text: 'Complete',
                                  style: TextStyle(
                                    color: Color(0xFF5C74BC),
                                    fontSize: 13,
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            // ),
          ];
        },
        body: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              // backgroundColor: Colors.white,
              toolbarHeight: _isPinnedTitleShown ? kToolbarHeight : 0,
              pinned: true,
              elevation: 0,
              leading: IconButton(
                icon: const Icon(Icons.menu),
                onPressed: () {},
              ),
              title: Container(
                alignment: Alignment.center,
                margin: const EdgeInsets.only(
                  left: 24,
                  right: 8,
                ),
                height: 30,
                decoration: BoxDecoration(
                  color: const Color.fromRGBO(220, 244, 243, .63),
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Text(
                  'Search Tasks',
                  textAlign: TextAlign.center,
                  style: Theme.of(context)
                      .primaryTextTheme
                      .bodyText1
                      .copyWith(color: Colors.white70, fontSize: 16),
                ),
              ),
            ),
            SliverExpandableList(
              builder: SliverExpandableChildDelegate<String, ExampleSection>(
                sectionList: sectionList,
                headerBuilder: _buildHeader,
                itemBuilder: (context, sectionIndex, itemIndex, index) {
                  final item = sectionList[sectionIndex].items[itemIndex];
                  return ListTile(
                    leading: CircleAvatar(
                      child: Text("$index"),
                    ),
                    title: Text(item),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildHeader(BuildContext context, int sectionIndex, int index) {
    final ExampleSection section = sectionList[sectionIndex];
    return InkWell(
      onTap: () {
        //toggle section expand state
        setState(() {
          section.setSectionExpanded(!section.isSectionExpanded());
        });
      },
      child: Container(
        color: Colors.lightBlue,
        height: 48,
        padding: const EdgeInsets.only(left: 20),
        alignment: Alignment.centerLeft,
        child: Text(
          "Header #$sectionIndex",
          style: const TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}

current solution must remain second SliverAppBar size, for smooth:

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

import 'mock_data.dart';


class ExampleSliver extends StatefulWidget {
  const ExampleSliver({Key key}) : super(key: key);

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

class _ExampleNestedScrollViewState extends State<ExampleSliver>
    with TickerProviderStateMixin {
  List<ExampleSection> sectionList = MockData.getExampleSections();
  final GlobalKey<NestedScrollViewState> nestedScrollKey = GlobalKey();
  final double _expandedHeight = 200;
  final double _toolbarHeight = 30;
  bool _isPinnedTitleShown = false;

  @override
  void initState() {
    super.initState();
    var headerContentHeight = _expandedHeight - _toolbarHeight;
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      outerController.addListener(() {
        var pinned = outerController.offset >= headerContentHeight;
        if (_isPinnedTitleShown != pinned) {
          setState(() {
            _isPinnedTitleShown = pinned;
          });
        }
        print("outerController position: $outerController $kToolbarHeight $_isPinnedTitleShown");
      });
    });
  }

  ScrollController get outerController {
    return nestedScrollKey.currentState.outerController;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        key: nestedScrollKey,
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              // backgroundColor: Colors.white,
              expandedHeight: _expandedHeight - kToolbarHeight,
              leading: IconButton(
                icon: const Icon(Icons.menu),
                onPressed: () {},
              ),
              title: Container(
                alignment: Alignment.center,
                margin: const EdgeInsets.only(
                  left: 24,
                  right: 8,
                ),
                height: 30,
                decoration: BoxDecoration(
                  color: const Color.fromRGBO(220, 244, 243, .63),
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Text(
                  'Search Tasks',
                  textAlign: TextAlign.center,
                  style: Theme.of(context)
                      .primaryTextTheme
                      .bodyText1
                      .copyWith(color: Colors.white70, fontSize: 16),
                ),
              ),
              flexibleSpace: FlexibleSpaceBar(
                background: Stack(
                  children: [
                    Align(
                      child: Container(
                        height: 56 + 0.4 * MediaQuery.of(context).size.height,
                        width: MediaQuery.of(context).size.width,
                        decoration: const BoxDecoration(
                          gradient: LinearGradient(
                            begin: Alignment.topCenter,
                            end: Alignment.bottomCenter,
                            colors: [Color(0xFF6EE2F5), Color(0xFF6E96F5)],
                          ),
                        ),
                      ),
                    ),
                    Align(
                      alignment: const Alignment(0, 0.5),
                      child: Container(
                        padding: const EdgeInsets.all(4),
                        decoration: BoxDecoration(
                          gradient: const LinearGradient(
                            begin: Alignment.topLeft,
                            end: Alignment.bottomRight,
                            colors: [Color(0xFFFFEEF4), Color(0xFFE3FCFF)],
                          ),
                          borderRadius: BorderRadius.circular(100),
                          boxShadow: const [
                            BoxShadow(color: Color.fromRGBO(0, 0, 0, 0.16))
                          ],
                        ),
                        child: Container(
                          alignment: Alignment.center,
                          child: RichText(
                            textAlign: TextAlign.center,
                            text: TextSpan(
                              children: <TextSpan>[
                                TextSpan(
                                  text: '${(1 * 100).round()}%\n',
                                  style: TextStyle(
                                    color: const Color(0xFF5C74BC),
                                    fontSize:
                                    (1 * 100).round() == 100 ? 28 : 32,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                                const TextSpan(
                                  text: 'Complete',
                                  style: TextStyle(
                                    color: Color(0xFF5C74BC),
                                    fontSize: 13,
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            // ),
          ];
        },
        body: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              pinned: true,
              elevation: 0,
              leading: Visibility(
                visible: _isPinnedTitleShown,
                child: IconButton(
                  icon: const Icon(Icons.menu),
                  onPressed: () {},
                ),
              ),
              title: Visibility(
                visible: _isPinnedTitleShown,
                child: Container(
                  alignment: Alignment.center,
                  margin: const EdgeInsets.only(
                    left: 24,
                    right: 8,
                  ),
                  height: _toolbarHeight,
                  decoration: BoxDecoration(
                    color: const Color.fromRGBO(220, 244, 243, .63),
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: Text(
                    'Search Tasks',
                    textAlign: TextAlign.center,
                    style: Theme.of(context)
                        .primaryTextTheme
                        .bodyText1
                        .copyWith(color: Colors.white70, fontSize: 16),
                  ),
                ),
              ),
            ),
            SliverExpandableList(
              builder: SliverExpandableChildDelegate<String, ExampleSection>(
                sectionList: sectionList,
                headerBuilder: _buildHeader,
                itemBuilder: (context, sectionIndex, itemIndex, index) {
                  final item = sectionList[sectionIndex].items[itemIndex];
                  return ListTile(
                    leading: CircleAvatar(
                      child: Text("$index"),
                    ),
                    title: Text(item),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildHeader(BuildContext context, int sectionIndex, int index) {
    final ExampleSection section = sectionList[sectionIndex];
    return InkWell(
      onTap: () {
        //toggle section expand state
        setState(() {
          section.setSectionExpanded(!section.isSectionExpanded());
        });
      },
      child: Container(
        color: Colors.lightBlue,
        height: 48,
        padding: const EdgeInsets.only(left: 20),
        alignment: Alignment.centerLeft,
        child: Text(
          "Header #$sectionIndex",
          style: const TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}```