TatsuUkraine / flutter_sticky_infinite_list

Multi directional infinite list with Sticky headers for Flutter applications
BSD 2-Clause "Simplified" License
341 stars 31 forks source link

how to use it with NestedScrollView #29

Closed fangshengfy closed 4 years ago

fangshengfy commented 4 years ago

import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:sticky_infinite_list/sticky_infinite_list.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
      routes: {
        SingleChildScrollPage.ROUTE: (_) => SingleChildScrollPage(),
      },
    );
  }
}

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

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final StreamController<Settings> _streamController =
      StreamController<Settings>.broadcast();
  final ScrollController _scrollController = ScrollController();
  Settings _settings = Settings();

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                expandedHeight: 230.0,
                pinned: true,
                flexibleSpace: FlexibleSpaceBar(
                  title: Text('复仇者联盟'),
                  background: Image.network(
                    'http://img.haote.com/upload/20180918/2018091815372344164.jpg',
                    fit: BoxFit.fitHeight,
                  ),
                ),
              )
            ];
          },
          body: ScrollWidget(
            settings: _settings,
            scrollController: _scrollController,
            stream: _streamController.stream,
          ),
        ),
      );

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

    _streamController.close();
  }
}

class ScrollWidget extends StatelessWidget {
  final Stream<Settings> stream;
  final ScrollController scrollController;
  final Settings settings;

  const ScrollWidget({
    Key key,
    this.stream,
    this.scrollController,
    this.settings,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) => InfiniteList(
        /// when direction changes dynamically, Flutter
        /// won't rerender scroll completely,
        /// which means that gesture detector on scroll itself
        /// remains from original direction
        key: Key(settings.scrollDirection.toString()),
        scrollDirection: settings.scrollDirection,
        anchor: settings.anchor,
        controller: scrollController,
        direction: settings.multiDirection
            ? InfiniteListDirection.multi
            : InfiniteListDirection.single,
        negChildCount: settings.negCount,
        posChildCount: settings.posCount,
        physics: settings.physics,
        builder: (context, index) {
          final date = DateTime.now().add(Duration(
            days: index,
          ));

          if (settings.overlay) {
            return InfiniteListItem.overlay(
              mainAxisAlignment: settings.mainAxisAlignment,
              crossAxisAlignment: settings.crossAxisAlignment,
              headerStateBuilder: (context, state) => Padding(
                padding: const EdgeInsets.all(8.0),
                child: Container(
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.orange.withOpacity(1 - state.position),
                  ),
                  height: 70,
                  width: 70,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text(
                        date.day.toString(),
                        style: TextStyle(
                          fontSize: 21,
                          color: Colors.black87,
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                      Text(
                        DateFormat.MMM().format(date),
                        style: TextStyle(
                          height: .7,
                          fontSize: 17,
                          color: Colors.black87,
                          fontWeight: FontWeight.w400,
                        ),
                      )
                    ],
                  ),
                ),
              ),
              contentBuilder: (context) => Padding(
                padding: const EdgeInsets.all(8.0),
                child: Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(10),
                    color: Colors.blueAccent,
                  ),
                  height: settings.contentHeight,
                  width: settings.contentWidth,
                  child: Center(
                    child: Text(
                      DateFormat.yMMMMd().format(date),
                      style: TextStyle(fontSize: 18, color: Colors.white),
                    ),
                  ),
                ),
              ),
            );
          }

          return InfiniteListItem(
            mainAxisAlignment: settings.mainAxisAlignment,
            crossAxisAlignment: settings.crossAxisAlignment,
            positionAxis: settings.positionAxis,
            headerStateBuilder: (context, state) => Padding(
              padding: const EdgeInsets.all(8.0),
              child: Container(
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: Colors.orange.withOpacity(1 - state.position),
                ),
                height: 70,
                width: 70,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      date.day.toString(),
                      style: TextStyle(
                        fontSize: 21,
                        color: Colors.black87,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                    Text(
                      DateFormat.MMM().format(date),
                      style: TextStyle(
                        height: .7,
                        fontSize: 17,
                        color: Colors.black87,
                        fontWeight: FontWeight.w400,
                      ),
                    )
                  ],
                ),
              ),
            ),
            contentBuilder: (context) => Padding(
              padding: const EdgeInsets.all(8.0),
              child: Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  color: Colors.blueAccent,
                ),
                height: settings.contentHeight,
                width: settings.contentWidth,
                child: Center(
                  child: Text(
                    DateFormat.yMMMMd().format(date),
                    style: TextStyle(fontSize: 18, color: Colors.white),
                  ),
                ),
              ),
            ),
          );
        },
      );
}

enum ScrollPhysicsEnum {
  PLATFORM,
  IOS,
  ANDROID,
}

class Settings {
  int negCount;
  int posCount;
  bool multiDirection;
  HeaderMainAxisAlignment mainAxisAlignment;
  HeaderCrossAxisAlignment crossAxisAlignment;
  HeaderPositionAxis positionAxis;
  double anchor;
  Axis scrollDirection;
  ScrollPhysicsEnum physicsType;
  bool overlay;

  bool get scrollVertical => scrollDirection == Axis.vertical;

  ScrollPhysics get physics {
    switch (physicsType) {
      case ScrollPhysicsEnum.PLATFORM:
        return null;

      case ScrollPhysicsEnum.ANDROID:
        return ClampingScrollPhysics();

      case ScrollPhysicsEnum.IOS:
        return BouncingScrollPhysics();
    }

    return null;
  }

  double get contentHeight {
    if (scrollVertical) {
      return 300;
    }

    return double.infinity;
  }

  double get contentWidth {
    if (scrollVertical) {
      return double.infinity;
    }

    return 300;
  }

  Settings({
    this.negCount,
    this.posCount,
    this.mainAxisAlignment = HeaderMainAxisAlignment.start,
    this.crossAxisAlignment = HeaderCrossAxisAlignment.start,
    this.positionAxis = HeaderPositionAxis.mainAxis,
    this.multiDirection = false,
    this.anchor = 0,
    this.scrollDirection = Axis.vertical,
    this.physicsType = ScrollPhysicsEnum.PLATFORM,
    this.overlay = false,
  });
}

class SingleChildScrollPage extends StatefulWidget {
  static const String ROUTE = "/single-child";

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

class _SingleChildScrollPageState extends State<SingleChildScrollPage> {
  static const double _headerHeight = 50;

  final StreamController<StickyState<String>> _headerStream =
      StreamController<StickyState<String>>.broadcast();
  final StreamController<StickyState<String>> _headerOverlayStream =
      StreamController<StickyState<String>>.broadcast();

  @override
  Widget build(BuildContext context) {
    final height = MediaQuery.of(context).size.height;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Single element example'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            Container(
              height: height,
              color: Colors.lightBlueAccent,
              child: Placeholder(),
            ),
            StickyListItem<String>(
              streamSink: _headerStream.sink,
              header: Container(
                height: _headerHeight,
                width: double.infinity,
                color: Colors.orange,
                child: Center(
                  child: StreamBuilder<StickyState<String>>(
                    stream: _headerStream.stream,
                    builder: (_, snapshot) {
                      if (!snapshot.hasData) {
                        return Container();
                      }

                      final position = (snapshot.data.position * 100).round();

                      return Text('Positioned relative. Position: $position%');
                    },
                  ),
                ),
              ),
              content: Container(
                height: height,
                color: Colors.blueAccent,
                child: Placeholder(),
              ),
              itemIndex: "single-child",
            ),
            StickyListItem<String>.overlay(
              streamSink: _headerOverlayStream.sink,
              header: Container(
                height: _headerHeight,
                width: double.infinity,
                color: Colors.orange,
                child: Center(
                  child: StreamBuilder<StickyState<String>>(
                    stream: _headerOverlayStream.stream,
                    builder: (_, snapshot) {
                      if (!snapshot.hasData) {
                        return Container();
                      }

                      final position = (snapshot.data.position * 100).round();

                      return Text('Positioned overlay. Position: $position%');
                    },
                  ),
                ),
              ),
              content: Container(
                height: height,
                color: Colors.lightBlueAccent,
                child: Placeholder(),
              ),
              itemIndex: "single-overlayed-child",
            ),
            Container(
              height: height,
              color: Colors.cyan,
              child: Placeholder(),
            ),
          ],
        ),
      ),
    );
  }

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

    _headerStream.close();
    _headerOverlayStream.close();
  }
}
fangshengfy commented 4 years ago

maybe scroll has poblem

TatsuUkraine commented 4 years ago

What kind of issue you get with this example?

fangshengfy commented 4 years ago

NestedScrollView-》SliverAppBar There is a problem with folding

fangshengfy commented 4 years ago

跳转

TatsuUkraine commented 4 years ago

Flutter can't properly place sliverappbar over the multidirectional infinite scroll since it can't define edges for app bar placement calculation.

If you need to incorporate sticky header items with SliverAppBar, you need to use example from SingleChildScrollPage and example from Flutter docs. Since quite likely you need to add Overlap and Absorber slivers in scrollable area

TatsuUkraine commented 4 years ago

SingleChildScrollView in SingleChildScrollPage can be replaced with any other ScrollView that Flutter provides

TatsuUkraine commented 4 years ago

and StickyListItem can be rendered with SliverChildBuilderDelegate for example if you need infinite list

TatsuUkraine commented 4 years ago

but, if you need single directional scroll view with nested scroll - you no need actually NestedScrollView

TatsuUkraine commented 4 years ago

@fangshengfy here is an example of page scaffold (just define droadcast _headerStream controller)

    Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            expandedHeight: 230.0,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text('Title'),
              background: Image.network(
                'http://img.haote.com/upload/20180918/2018091815372344164.jpg',
                fit: BoxFit.fitHeight,
              ),
            ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                final date = DateTime.now().add(
                    Duration(
                      days: index,
                    )
                );

                return StickyListItem<int>(
                  itemIndex: index,
                  streamSink: _headerStream.sink,
                  positionAxis: HeaderPositionAxis.crossAxis,
                  header: StreamBuilder<StickyState<int>>(
                    initialData: StickyState<int>(index),
                    stream: _headerStream.stream.where((event) => event.index == index),
                    builder: (_, snapshot) {
                      return Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Container(
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            color: Colors.orange.withOpacity(1 - snapshot.data.position),
                          ),
                          height: 70,
                          width: 70,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              Text(
                                date.day.toString(),
                                style: TextStyle(
                                  fontSize: 21,
                                  color: Colors.black87,
                                  fontWeight: FontWeight.w600,
                                ),
                              ),
                              Text(
                                DateFormat.MMM().format(date),
                                style: TextStyle(
                                  height: .7,
                                  fontSize: 17,
                                  color: Colors.black87,
                                  fontWeight: FontWeight.w400,
                                ),
                              )
                            ],
                          ),
                        ),
                      );
                    },
                  ),
                  content: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(10),
                        color: Colors.blueAccent,
                      ),
                      height: 300,
                      width: double.infinity,
                      child: Center(
                        child: Text(
                          DateFormat.yMMMMd().format(date),
                          style: TextStyle(
                              fontSize: 18,
                              color: Colors.white
                          ),
                        ),
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      )
    );
TatsuUkraine commented 4 years ago

Btw, if you don't want to rebuild header on scroll, you can remove stream and StreamBuilder from prev example. Position will be recalculated in any case