Files
PiliPlus/lib/common/widgets/sliver/sliver_floating_header.dart
dom 887c3525a2 fix #1917
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:38:18 +08:00

230 lines
6.5 KiB
Dart

/*
* This file is part of PiliPlus
*
* PiliPlus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PiliPlus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
show RenderSliverSingleBoxAdapter, SliverGeometry, ScrollDirection;
/// ref [SliverFloatingHeader]
class SliverFloatingHeaderWidget extends StatelessWidget {
const SliverFloatingHeaderWidget({
super.key,
required this.child,
required this.backgroundColor,
});
final Widget child;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return _SliverFloatingHeaderWidget(
backgroundColor: backgroundColor,
child: _SliverFloatingHeaderScroll(child: child),
);
}
}
class _SliverFloatingHeaderScroll extends StatefulWidget {
const _SliverFloatingHeaderScroll({required this.child});
final Widget child;
@override
State<_SliverFloatingHeaderScroll> createState() =>
_SliverFloatingHeaderScrollState();
}
class _SliverFloatingHeaderScrollState
extends State<_SliverFloatingHeaderScroll> {
ScrollPosition? _position;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_position != null) {
_position!.isScrollingNotifier.removeListener(_isScrollingListener);
}
_position = Scrollable.maybeOf(context)?.position;
if (_position != null) {
_position!.isScrollingNotifier.addListener(_isScrollingListener);
}
}
@override
void dispose() {
if (_position != null) {
_position!.isScrollingNotifier.removeListener(_isScrollingListener);
}
super.dispose();
}
void _isScrollingListener() {
assert(_position != null);
final RenderSliverFloatingHeader? renderer = context
.findAncestorRenderObjectOfType<RenderSliverFloatingHeader>();
if (_position!.isScrollingNotifier.value) {
renderer?.updateScrollStartDirection(_position!.userScrollDirection);
}
}
@override
Widget build(BuildContext context) => widget.child;
}
class _SliverFloatingHeaderWidget extends SingleChildRenderObjectWidget {
const _SliverFloatingHeaderWidget({
required Widget super.child,
required this.backgroundColor,
});
final Color backgroundColor;
@override
RenderObject createRenderObject(BuildContext context) =>
RenderSliverFloatingHeader(backgroundColor: backgroundColor);
@override
void updateRenderObject(
BuildContext context,
RenderSliverFloatingHeader renderObject,
) {
renderObject.backgroundColor = backgroundColor;
}
}
class RenderSliverFloatingHeader extends RenderSliverSingleBoxAdapter {
RenderSliverFloatingHeader({
required Color backgroundColor,
}) : _backgroundColor = backgroundColor;
Color _backgroundColor;
set backgroundColor(Color value) {
if (_backgroundColor == value) return;
_backgroundColor = value;
markNeedsPaint();
}
double? _childPosition;
double? lastScrollOffset;
late double effectiveScrollOffset;
bool get floatingHeaderNeedsToBeUpdated {
return lastScrollOffset != null &&
(constraints.scrollOffset < lastScrollOffset! ||
effectiveScrollOffset < child!.size.height);
}
ScrollDirection? _lastStartedScrollDirection;
void updateScrollStartDirection(ScrollDirection direction) {
_lastStartedScrollDirection = direction;
}
@override
void performLayout() {
if (!floatingHeaderNeedsToBeUpdated) {
effectiveScrollOffset = constraints.scrollOffset;
} else {
double delta =
lastScrollOffset! -
constraints.scrollOffset; // > 0 when the header is growing
if (constraints.userScrollDirection == .forward ||
_lastStartedScrollDirection == .forward) {
final childExtent = child!.size.height;
if (effectiveScrollOffset > childExtent) {
effectiveScrollOffset =
childExtent; // The header is now just above the start edge of viewport.
}
} else {
// delta > 0 and scrolling forward is a contradiction. Assume that it's noise (set delta to 0).
delta = clampDouble(delta, -double.infinity, 0);
}
effectiveScrollOffset = clampDouble(
effectiveScrollOffset - delta,
0.0,
constraints.scrollOffset,
);
}
child?.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final childExtent = child!.size.height;
final double paintExtent = childExtent - effectiveScrollOffset;
final double layoutExtent = childExtent - constraints.scrollOffset;
geometry = SliverGeometry(
paintOrigin: math.min(constraints.overlap, 0.0),
scrollExtent: childExtent,
paintExtent: clampDouble(
paintExtent,
0.0,
constraints.remainingPaintExtent,
),
layoutExtent: clampDouble(
layoutExtent,
0.0,
constraints.remainingPaintExtent,
),
maxPaintExtent: childExtent,
hasVisualOverflow: false,
);
_childPosition = math.min(0.0, paintExtent - childExtent);
lastScrollOffset = constraints.scrollOffset;
}
@override
double childMainAxisPosition(covariant RenderObject child) {
return _childPosition ?? 0;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child == this.child);
applyPaintTransformForBoxChild(child as RenderBox, transform);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
offset += Offset(0.0, childMainAxisPosition(child!));
final size = child!.size;
context.canvas.drawRect(
Rect.fromLTWH(
offset.dx,
offset.dy - 2,
size.width,
size.height + 2,
),
Paint()..color = _backgroundColor,
);
context.paintChild(child!, offset);
}
}
@override
bool hitTestSelf({
required double mainAxisPosition,
required double crossAxisPosition,
}) => true;
}