Open volser opened 3 years ago
Can you elaborate on how this widget should work?
the same as header, but pin at the bottom next header in the list
actually, I have a reversed list and need to implement pinned header, but for reversed list it works like footer :-)
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
Hmm ok And what if there are multiple? I guess only 1 should be visible?
yes, only 1, the same as header, but reversed logic, when scroll up, prev (I mean footer above) footer should shift next (footer below)
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
@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.
@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
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).
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)
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?
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),
],
),
@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 :)
@Kavantix Thank you so much for this update. I just tested it and it is working perfectly fine. I have one suggestion:
Great to hear it works! But can you elaborate a bit on the question?
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.)
You can try wrapping the multisliver with the sliverwithpinnedfooter
If I am using MultiSliver correctly, it seems like it is not working for SliverWithPinnedFooter.
Well I meant try using it like this:
SliverWithPinnedFooter(
sliver: MultiSliver(
pushPinnedChildren: true,
children: [...],
),
footer: FooterWidget(),
)
If I use MultiSliver inside SliverWithPinnedFooter then the footer is behaving quite differently. If you need I will attach a video as well.
video would be helpful
If you notice at the bottom. Footer appears before the header.
Hmmm, supporting this might require the widget to be changed to SliverWithPinnedFooterAndHeader
I'll have to think about it a bit more
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
.
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.
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
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 SliverBox
es.
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,
);
}
}
Is it possible to implement SliverPinnedFooter?