Kavantix / sliver_tools

A set of useful sliver tools that are missing from the flutter framework
MIT License
652 stars 64 forks source link

SliverPinnedFooter #20

Open volser opened 3 years ago

volser commented 3 years ago

Is it possible to implement SliverPinnedFooter?

Kavantix commented 3 years ago

Can you elaborate on how this widget should work?

volser commented 3 years ago

the same as header, but pin at the bottom next header in the list

volser commented 3 years ago

actually, I have a reversed list and need to implement pinned header, but for reversed list it works like footer :-)

volser commented 3 years ago

in this demo https://raw.githubusercontent.com/Kavantix/sliver_tools/master/gifs/demo2.gif "Next button" should be pinned to the bottom if list is longer than screen

Kavantix commented 3 years ago

Hmm ok And what if there are multiple? I guess only 1 should be visible?

volser commented 3 years ago

yes, only 1, the same as header, but reversed logic, when scroll up, prev (I mean footer above) footer should shift next (footer below)

Kavantix commented 3 years ago

That does complicate things somewhat since they would need to communicate with each other in order to achieve this within the current flutter sliver implementation. Possibly it would even need a parent inherited widget above the viewport to make this work.

Thanks for the suggestion though, I'll put it on the project but I won't have time to work on this in the near future, feel free to make a PR

saibotma commented 2 years ago

@Kavantix Would be great if you could elaborate on why the whole thing would get complicated. I want to build the same thing, and I am struggling with it, but I am also new to slivers. Any tips & trick or hints are appreciated.

Kavantix commented 2 years ago

@saibotma could you elaborate a bit more on the exact usecase you are trying to implement because only a subset of usecases would be as complicated as I mentioned

saibotma commented 2 years ago

I am actually trying to have a bottom (tab) bar for navigation, where scrollable content scrolls beneath it. I want to make the bottom bar opaque in order to add a blur filter and create an iOS style frosted glass effect. Currently, I have implemented it using a Stack. The bottom bar is above the scrolled content and aligned to the bottom. The scrolled content has padding bottom as high as the bottom bar. However, this feels very hacky, and you always have to know the height of the bottom bar (which can change in my case).

Kavantix commented 2 years ago

Ah I see, that usecase is indeed quite a bit simpler than what I was talking about since you only have 1 tab bar and thus don't need any logic for having multiple.

However, the part you mention where it should draw on top of the other content makes it rather difficult. The thing with slivers is that they are painted in the reverse order, this is why a SliverPinnedHeadere is painted on top of the other widgets. This means that in order to have a footer draw over content it needs to be earlier in the list of slivers which would remove the ability to fill up space in the bottom.

So making this a Sliver is probably not the best option. Just keep using the Stack. To solve the problem of not knowing the size of the tab bar you can put an invisible widget at the end of the CustomScrollView that sizes in the same way as the tab bar does (you could even start by actually putting the tab bar there but invisible)

koiralapankaj7 commented 2 years ago

This might be another use case where SliverPinnedFooter makes sense. I want to pin the Total row at the bottom but as of now without using shrinkWrap I am not being able to do so. Is there any better way to achieve this behaviour?

image

    Column(
         mainAxisSize: MainAxisSize.min,
         children: [
            // Header
            const _Header(),

            // Body
            Flexible(
                  child: CustomScrollView(
                    // Shrink wrapping the content of the scroll view is
                    // significantly more expensive than expanding to the
                    // maximum allowed size because the content can expand
                    // and contract during scrolling, which means the size
                    // of the scroll view needs to be recomputed whenever
                    // the scroll position changes.
                    shrinkWrap: provider.body.rows.length < 30,
                    physics: const ClampingScrollPhysics(),
                    slivers: const [_Body()],
                  ),
             ),

            // Footer
            const _Header(header: false),
       ],
  ),
Kavantix commented 2 years ago

@volser @koiralapankaj7 I just opened a draft PR #59 that should serve your needs. I would like some feedback if you have time to test it :)

koiralapankaj7 commented 2 years ago

@Kavantix Thank you so much for this update. I just tested it and it is working perfectly fine. I have one suggestion:

  1. Footer should push pinned children if there are any.
Kavantix commented 2 years ago

Great to hear it works! But can you elaborate a bit on the question?

koiralapankaj7 commented 2 years ago

https://user-images.githubusercontent.com/19282888/188480667-6792e01c-6f90-4503-9baf-0238ddddde70.mov

In this video, the footer is going below the header while scrolling. What I meant was can we push the header when the footer reaches there? (header in this context is SliverPinnedHeader.)

Kavantix commented 2 years ago

You can try wrapping the multisliver with the sliverwithpinnedfooter

koiralapankaj7 commented 2 years ago

image

If I am using MultiSliver correctly, it seems like it is not working for SliverWithPinnedFooter.

Kavantix commented 2 years ago

Well I meant try using it like this:

SliverWithPinnedFooter(
  sliver: MultiSliver(
    pushPinnedChildren: true,
    children: [...],
  ),
  footer: FooterWidget(),
)
koiralapankaj7 commented 2 years ago

image

If I use MultiSliver inside SliverWithPinnedFooter then the footer is behaving quite differently. If you need I will attach a video as well.

Kavantix commented 2 years ago

video would be helpful

koiralapankaj7 commented 2 years ago

https://user-images.githubusercontent.com/19282888/188484086-f29e68f3-3530-4a7a-bf6a-cd20bf61c7aa.mov

If you notice at the bottom. Footer appears before the header.

Kavantix commented 2 years ago

Hmmm, supporting this might require the widget to be changed to SliverWithPinnedFooterAndHeader I'll have to think about it a bit more

folaoluwafemi commented 1 year ago

Hello, please where are we with this feature?

I need it for a similarly described usecase in 2 of my apps; both for pinning a "Enter comment" text field to the bottom of the screen while the user scrolls.

Also I can't find the SliverWithPinnedFooter class in my current version of sliver_tools.

Kavantix commented 5 months ago

If anyone would like to contribute that would be welcome but I do not have the need or time for this sliver at the moment.

lukas-pierce commented 2 months ago

I realized this using boxy package.

The main idea wrap your original sliver with SliverContainer and use its foreground property for realize sliver footer.

Code structure:

/// SliverContainer - is a special widget from "boxy" package
/// which allows put any foreground widget.
///
/// As foreground put footer widget, it should be constant height
/// sized and bottom aligned.
///
/// For prevent footer overflow bottom items in the list
/// add empty SizedBox after the list
SliverContainer(
  // body
  sliver: SliverMainAxisGroup( // multiple slivers wrapper
    slivers: [
      // your original sliver
      SliverList(...),

      // widget with same height as footer
      const SliverToBoxAdapter(
        child: SizedBox(height: 80),
      ),
    ],
  ),

  // footer
  foreground: Align(
    alignment: Alignment.bottomCenter,
    child: ListFooter(
      title: '$name Footer',
      height: 80,
    ),
  ),
),

https://github.com/user-attachments/assets/f9cb5e31-73c8-40c6-98a4-86cb328653e0

Full source code: https://gist.github.com/lukas-pierce/7ca38a847774968b46435f6ea7d52083

Krushiler commented 5 days ago

I've created my own sliver. You can wrap MultiSliver or SliverMainAxisGroup in SliverFooter (It's better should be named SliverWithFooter). It works similar to code provided by @lukas-pierce. Meaning, footer is not a sliver. It's a box that affects extent of the sliver. Use it in case you don't want to add a library just because of single sliver.

Comments are welcome. I'm not a pro in custom SliverBoxes.

import 'dart:math';

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

enum SliverFooterSlot { footer, sliver }

class SliverFooter extends SlottedMultiChildRenderObjectWidget<SliverFooterSlot, RenderObject> {
  final Widget footer;
  final Widget sliver;
  final bool fillRemaining;

  const SliverFooter({super.key, required this.footer, required this.sliver, this.fillRemaining = false});

  @override
  Widget? childForSlot(slot) => switch (slot) {
        SliverFooterSlot.footer => footer,
        SliverFooterSlot.sliver => sliver,
      };

  @override
  SlottedContainerRenderObjectMixin<SliverFooterSlot, RenderObject> createRenderObject(BuildContext context) {
    return SliverFooterRenderObject()..fillRemaining = fillRemaining;
  }

  @override
  void updateRenderObject(BuildContext context, covariant SliverFooterRenderObject renderObject) {
    renderObject.fillRemaining = fillRemaining;
  }

  @override
  Iterable<SliverFooterSlot> get slots => SliverFooterSlot.values;
}

class SliverFooterRenderObject extends RenderSliver
    with RenderSliverHelpers, SlottedContainerRenderObjectMixin<SliverFooterSlot, RenderObject> {
  bool _fillRemaining = false;

  bool get fillRemaining => _fillRemaining;

  set fillRemaining(bool value) {
    if (_fillRemaining != value) {
      _fillRemaining = value;
      markNeedsLayout();
    }
  }

  RenderSliver get sliver => childForSlot(SliverFooterSlot.sliver)! as RenderSliver;

  RenderBox get footer => childForSlot(SliverFooterSlot.footer)! as RenderBox;

  double get footerPosition => fillRemaining
      ? constraints.viewportMainAxisExtent - footer.size.height
      : (constraints.remainingPaintExtent - footer.size.height)
          .clamp(0.0, sliver.geometry!.scrollExtent - constraints.scrollOffset);

  @override
  void performLayout() {
    sliver.layout(constraints, parentUsesSize: true);
    footer.layout(constraints.asBoxConstraints().tighten(width: constraints.crossAxisExtent), parentUsesSize: true);

    final childHeight = sliver.geometry!.scrollExtent + footer.size.height;

    final double extent = fillRemaining ? max(constraints.viewportMainAxisExtent, childHeight) : childHeight;

    final paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent);
    final cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: extent);

    geometry = SliverGeometry(
      scrollExtent: extent,
      paintExtent: paintedChildSize,
      maxPaintExtent: paintedChildSize,
      hasVisualOverflow: extent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
      cacheExtent: cacheExtent,
    );
  }

  @override
  double childMainAxisPosition(covariant RenderObject child) {
    if (child == sliver) {
      return 0.0;
    }
    if (child == footer) {
      return footerPosition;
    }
    return 0;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    context.paintChild(sliver, offset);
    context.paintChild(footer, offset + Offset(0, footerPosition));
  }

  @override
  void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
    applyPaintTransformForBoxChild(footer, transform);
  }

  @override
  bool hitTestChildren(
    SliverHitTestResult result, {
    double? mainAxisPosition,
    double? crossAxisPosition,
  }) {
    if (mainAxisPosition == null || crossAxisPosition == null) {
      return false;
    }
    return hitTestBoxChild(
          BoxHitTestResult.wrap(result),
          footer,
          mainAxisPosition: mainAxisPosition,
          crossAxisPosition: crossAxisPosition,
        ) ||
        sliver.hitTest(
          result,
          mainAxisPosition: mainAxisPosition,
          crossAxisPosition: crossAxisPosition,
        );
  }
}