letsar / flutter_sticky_header

Flutter implementation of sticky headers for sliver
MIT License
907 stars 175 forks source link

Reverse list support #11

Closed wwwdata closed 2 years ago

wwwdata commented 6 years ago

When using a CustomScrollView with the reverse property set to true it would make sense to reverse the SliverStickyHeader as well. In a reversed list, the header currently is at the bottom of the list. I suggest to add a reverse option to SliverStickyHeader as well, so it renders after the other sliver widgets, instead of before.

letsar commented 6 years ago

Hi @wwwdata, I'm not sure to understand, if the header is rendered after its child, it will not be sticky, isn't it? 😕 Can you attach a drawing or something like that to help me to understand?

wwwdata commented 6 years ago

Imagine a chat app like WhatsApp. You always have the days as headlines and you start at the bottom of the list and scroll up. So I always want to have the headline at the top of the sliver instead of the bottom, which is the case when it is just reversed.

I started looking at the code and trying to make it work on my own, I will maybe look tomorrow or monday again and see if I can make it work on my own. But if its super easy and you get it done in like a couple of minutes I would appreciate your help!

letsar commented 6 years ago

Ok I think I got it. I started something on a feature branch: https://github.com/letsar/flutter_sticky_header/tree/feature/reverse. Can you test it and tell me if I go in the right direction?

wwwdata commented 6 years ago

@letsar yes you are heading in the right direction! was also doing these exact changes when I started. Now the fadeout/move-away-animation of the header also needs to happen at the top and not bottom and that's it I guess.

qiaolin-pan commented 5 years ago

This would be really useful

ghost commented 5 years ago

@letsar did you plan to finish this feature?

smm13344331 commented 5 years ago

Hi,

I am implementing a chat widget and using your library to separate messages by date, you can see the same behavior on Whatsapp and Telegram.

I am building several slivers that each contain messages on that date and a header which is basically a text showing the relevant date. I pass the slivers to a CustomScrollView with reverse property set to true.

Everything works fine except that the headers are pinned to the bottom of the screen (I need them to stick to the top). How can I fix this? Any pointers would be appreciated.

.
.
.
   return CustomScrollView(
      slivers: _buildListItem(context, sectionIndex),
      reverse: true,
      controller: listScrollController,
    );
.
.
.
  List<Widget> _buildListItem(
    BuildContext context,
    List<ChatSection> chatSections,
  ) {
    List<Widget> slivers = List();
    chatSections.forEach((chatSection) {
      slivers.add(_buildChatSliver(
          context, chatSection.messages, chatSection.datetime));
    });

    return slivers;
  }
  SliverStickyHeaderBuilder _buildChatSliver(BuildContext context,
      List<ChatMessage> chatMessages, DateTime dateTimeHeader) {
    return SliverStickyHeaderBuilder(
      overlapsContent: false,
      builder: (context, state) {
        DateTime now = DateTime.now();
        String headerText = now.year != dateTimeHeader.year
            ? DateFormat("YYYY MMM dd").format(dateTimeHeader)
            : DateFormat("MMM dd").format(dateTimeHeader);
        return Container(
          padding: EdgeInsets.fromLTRB(5, 0, 15, 5),
          child: Center(
            child: Container(
              padding: EdgeInsets.fromLTRB(5, 5, 5, 5),
              decoration: BoxDecoration(
                  color: Color.fromARGB(200, 150, 150, 150),
                  borderRadius: BorderRadius.all(Radius.circular(5))),
              child: Text(
                headerText,
                style: TextStyle(
                    color: Colors.white70,
                    fontSize: 12.0,
                    fontStyle: FontStyle.italic),
              ),
            ),
          ),
        );
      },
      sliver: SliverList(
          delegate: SliverChildBuilderDelegate(
              (context, i) => _buildItem(chatMessages[i]),
              childCount: chatMessages.length)),
    );
  }
ghost commented 5 years ago

Hi,

I am implementing a chat widget and using your library to separate messages by date, you can see the same behavior on Whatsapp and Telegram.

I am building several slivers that each contain messages on that date and a header which is basically a text showing the relevant date. I pass the slivers to a CustomScrollView with reverse property set to true.

Everything works fine except that the headers are pinned to the bottom of the screen (I need them to stick to the top). How can I fix this? Any pointers would be appreciated.

.
.
.
   return CustomScrollView(
      slivers: _buildListItem(context, sectionIndex),
      reverse: true,
      controller: listScrollController,
    );
.
.
.
  List<Widget> _buildListItem(
    BuildContext context,
    List<ChatSection> chatSections,
  ) {
    List<Widget> slivers = List();
    chatSections.forEach((chatSection) {
      slivers.add(_buildChatSliver(
          context, chatSection.messages, chatSection.datetime));
    });

    return slivers;
  }
  SliverStickyHeaderBuilder _buildChatSliver(BuildContext context,
      List<ChatMessage> chatMessages, DateTime dateTimeHeader) {
    return SliverStickyHeaderBuilder(
      overlapsContent: false,
      builder: (context, state) {
        DateTime now = DateTime.now();
        String headerText = now.year != dateTimeHeader.year
            ? DateFormat("YYYY MMM dd").format(dateTimeHeader)
            : DateFormat("MMM dd").format(dateTimeHeader);
        return Container(
          padding: EdgeInsets.fromLTRB(5, 0, 15, 5),
          child: Center(
            child: Container(
              padding: EdgeInsets.fromLTRB(5, 5, 5, 5),
              decoration: BoxDecoration(
                  color: Color.fromARGB(200, 150, 150, 150),
                  borderRadius: BorderRadius.all(Radius.circular(5))),
              child: Text(
                headerText,
                style: TextStyle(
                    color: Colors.white70,
                    fontSize: 12.0,
                    fontStyle: FontStyle.italic),
              ),
            ),
          ),
        );
      },
      sliver: SliverList(
          delegate: SliverChildBuilderDelegate(
              (context, i) => _buildItem(chatMessages[i]),
              childCount: chatMessages.length)),
    );
  }

try using different library (https://pub.dev/packages/sticky_headers).

return StickyHeader(
  header: _sectionHeader(dateText),
  content: Column(
    children: <Widget>[
      if (isLast) _topWidget(),
      for (var i in list.reversed)
        ListTile(
          title: ChatMessageWidget(
            message: i,
            senderUuid: widget.senderUuid,
            key: Key('chatmsg$key$i'),
            onLongPress: _onMsgPressed,
            onTap: _onMsgPressed,
          )
        )
    ]
  )
);

and then in build function:

return CustomScrollView(
  key: Key('MessagesList'),
  controller: _scrollController,
  slivers: <Widget>[
    SliverList(delegate: SliverChildBuilderDelegate((context, index) {
      if (index == 0) {
        return _bottomView();
      } else {
        final key = keys[index - 1];
        return _buildSection(context, key, mapped[key], index == keys.length);
      }
    },
    childCount: keys.length + 1)),
  ],
  reverse: true,
);

works well for me!

pkitatta commented 4 years ago

Hi @letsar , any plans to pull this off? Nice plugin though. @wwwdata did you get a workaround you can share?

@pavelmeerkat Thanks for the insight. I was actually using that plugin but I was looking for something that would help me a implement scroll-to-item in the grouped list. I need to be able to scroll to individual items on a certain click. I am not yet a master in flutter but from your code sample above it looks like your use of keys here gives you more control on the position of the list item. The other thing I don't like about that plugin is that the sticky header is wobbly on scroll - doesn't look very cool.

wwwdata commented 4 years ago

@pkitatta no sorry I didn't look any further into this. For the App that I developed back then we found another good UI solution that worked out. And then I didn't look into this issue anymore.

pkitatta commented 4 years ago

@pkitatta no sorry I didn't look any further into this. For the App that I developed back then we found another good UI solution that worked out. And then I didn't look into this issue anymore.

@wwwdata thanks, I will probably build my own solution. I have been chatting a maker of a similar plugin, and he discouraged me from using the current sticky header plugins available (including his) for my app for performance reasons. I am building a chat app and need to group messages by date.

So, would gladly welcome any insight you can give on how you went about your solution - if you didn't patent it... :wink:

progid commented 4 years ago

I'm really need this feature. Trying to write myself.

frogman1189 commented 3 years ago

Is there any progress on this feature? Would be really useful

I've attempted to get it to work, currently I'm finding that all my attempts are pretty much the equivalent of just adding axisDirection = flipAxisDirection(axisDirection) which works, except that the header over scrolls by what appears to be the header height, and I can't seem to be able to fix this. I feel like its to do with it incorrectly pinning, but I don't really know

mrtnetwork commented 2 years ago

@pkitatta no sorry I didn't look any further into this. For the App that I developed back then we found another good UI solution that worked out. And then I didn't look into this issue anymore.

@wwwdata thanks, I will probably build my own solution. I have been chatting a maker of a similar plugin, and he discouraged me from using the current sticky header plugins available (including his) for my app for performance reasons. I am building a chat app and need to group messages by date.

So, would gladly welcome any insight you can give on how you went about your solution - if you didn't patent it... 😉

i solved this problem with visibility_detector package and notificationListener like this

view

Stack(
                                          fit: StackFit.expand,
                                          children: [
                                            NotificationListener(
                                              onNotification: (v) {
                                                if (v
                                                    is ScrollStartNotification) {
                                                  if (!chat.showHeader) {
                                                    chat.showHeader = true;
                                                    setState(() {});
                                                  }
                                                } else if (v
                                                    is UserScrollNotification) {
                                                  chat.handleShowHeader(() {
                                                    setState(() {});
                                                  });
                                                }
                                                return true;
                                              },
                                              child: CustomScrollView(
                                                reverse: true,
                                                cacheExtent: 1000,
                                                slivers: [
                                                  SliverList(
                                                      delegate: SliverChildBuilderDelegate(
                                                          (context, index) {
                                                    return VisibilityDetector(
                                                      key: ValueKey(chat
                                                          .messagesList[index]
                                                          .id),
                                                      onVisibilityChanged: (v) {
                                                        if (v.visibleFraction >
                                                            0.5) {
                                                          final bool _changed =
                                                              chat.changeDateTime(chat
                                                                  .messagesList[
                                                                      index]
                                                                  .time);
                                                          if (_changed) {
                                                             setState(() {});
                                                          }
                                                        }
                                                      },
                                                      child: Container(
                                                          child: Message(
                                                              chat.messagesList[
                                                                  index])),
                                                    );
                                                  },
                                                          addAutomaticKeepAlives:
                                                              false,
                                                          addRepaintBoundaries:
                                                              false,
                                                          childCount: chat
                                                              .messagesList
                                                              .length))
                                                ],
                                              ),
                                            ),
                                            Align(
                                              alignment: Alignment.topCenter,
                                              child: AnimatedSwitcher(
                                                duration: const Duration(
                                                    milliseconds: 200),
                                                switchOutCurve: Curves.linear,
                                                switchInCurve:
                                                    Curves.easeOutBack,
                                                child: !chat.showHeader ||
                                                        chat.nowMessageView ==
                                                            null
                                                    ? null
                                                    : Container(
                                                        width: 90,
                                                        alignment:
                                                            Alignment.topCenter,
                                                        child: Card(
                                                          elevation: 2,
                                                          color: Colors.black26,
                                                          child: Padding(
                                                            padding:
                                                                const EdgeInsets
                                                                        .symmetric(
                                                                    horizontal:
                                                                        10,
                                                                    vertical:
                                                                        2.5),
                                                            child: Text(
                                                              chat.nowMessageView!
                                                                  .differenceToWorld,
                                                              style:
                                                                  overMessageStyle,
                                                              textAlign:
                                                                  TextAlign
                                                                      .center,
                                                            ),
                                                          ),
                                                        ),
                                                      ),
                                              ),
                                            )
                                          ],
                                        ),

Methods

 handleShowHeader(Function callBack) {
    timer?.cancel();
    if (!showHeader) return;
    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      showHeader = false;
      callBack.call();
    });
  }

  DateTime? nowMessageView;
  bool changeDateTime(DateTime newTime) {
    if (nowMessageView?.day == newTime.day) {
      return false;
    }
    nowMessageView = newTime;
    return true;
  }
charlesRmajor commented 2 years ago

I forked this repo and got reverse working for my use case if that helps anyone else. I haven't had a chance to test it for the other uses: https://github.com/charlesRmajor/flutter_sticky_header

Sent a PR with the updates to the feature/reverse branch if you'd like to have it, @letsar

jqp4 commented 2 years ago

@charlesRmajor

I forked this repo and got reverse working for my use case if that helps anyone else. I haven't had a chance to test it for the other uses: https://github.com/charlesRmajor/flutter_sticky_header

unfortunately this doesn't work for me.

all rendering areas have moved up, although the rendering itself looks normal. because of this, the optimizer breaks, hiding widgets that have gone beyond the screen. that is, the drawing area has disappeared, but their image is still there and flutter simply turns it off. also because of this, working out pressing the widget does not work.

charlesRmajor commented 2 years ago

Yeah ... I only got it working for my particular situation. I added a reverse2 example that was working to my fork. If it helps, here's how I'm using it currently:

CustomScrollView(
  shrinkWrap: true, 
  reverse: true,
  physics: ClampingScrollPhysics(),
  slivers: [
       SliverStickyHeader.builder(
                  reverse: true, 
                  builder: (context, constraints) {...},
                  sliver:
                    SliverList(...
jqp4 commented 2 years ago

Yes, that's what I do. the SliverStickyHeaderState contains incorrect information, and the widget click area inside the SliverList still slides up if the header height is not zero.

JChPal commented 1 year ago

Yeah ... I only got it working for my particular situation. I added a reverse2 example that was working to my fork. If it helps, here's how I'm using it currently:

CustomScrollView(
  shrinkWrap: true, 
  reverse: true,
  physics: ClampingScrollPhysics(),
  slivers: [
       SliverStickyHeader.builder(
                  reverse: true, 
                  builder: (context, constraints) {...},
                  sliver:
                    SliverList(...

@charlesRmajor your fork works for me, but constraints from the builder SliverStickyHeaderWidgetBuilder has wrong a value for isPinned in the SliverStickyHeaderState. Above the CustomScrollView center Key isPinned is alway false even when it's pinned.

I also found another package that support reverse parameter (https://pub.dev/packages/slyverin), but I need to know which Widget is pinned. I tried with VisibilityDetector but it's ugly and pretty slow.