refa persistent header & dynamic sliver appbar

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-03-02 16:16:17 +08:00
parent 1dbc54f063
commit 9c7b18710c
30 changed files with 1691 additions and 883 deletions

View File

@@ -1,98 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show RenderProxyBox;
class CustomSliverPersistentHeaderDelegate
extends SliverPersistentHeaderDelegate {
const CustomSliverPersistentHeaderDelegate({
required this.child,
this.bgColor,
this.extent = 45,
this.needRebuild = false,
});
final double extent;
final Widget child;
final Color? bgColor;
final bool needRebuild;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
//创建child子组件
//shrinkOffsetchild偏移值minExtent~maxExtent
//overlapsContentSliverPersistentHeader覆盖其他子组件返回true否则返回false
return _DecoratedBox(color: bgColor, child: child);
}
//SliverPersistentHeader最大高度
@override
double get maxExtent => extent;
//SliverPersistentHeader最小高度
@override
double get minExtent => extent;
@override
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate.bgColor != bgColor ||
(needRebuild && oldDelegate.child != child);
}
}
class _DecoratedBox extends SingleChildRenderObjectWidget {
const _DecoratedBox({
this.color,
super.child,
});
final Color? color;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderDecoratedBox(color: color);
}
@override
void updateRenderObject(
BuildContext context,
_RenderDecoratedBox renderObject,
) {
renderObject.color = color;
}
}
class _RenderDecoratedBox extends RenderProxyBox {
_RenderDecoratedBox({
Color? color,
}) : _color = color;
Color? _color;
Color? get color => _color;
set color(Color? value) {
if (_color == value) return;
_color = value;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
if (_color case final color?) {
final size = this.size;
context.canvas.drawRect(
Rect.fromLTWH(
offset.dx,
offset.dy - 2,
size.width,
size.height + 2,
),
Paint()..color = color,
);
}
super.paint(context, offset);
}
@override
bool hitTestSelf(Offset position) => true;
}

View File

@@ -0,0 +1,357 @@
/*
* 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:PiliPlus/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
import 'package:PiliPlus/common/widgets/only_layout_widget.dart'
show LayoutCallback;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
import 'package:flutter/services.dart';
/// ref [SliverAppBar]
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
required this.leading,
required this.automaticallyImplyLeading,
required this.title,
required this.actions,
required this.automaticallyImplyActions,
required this.flexibleSpace,
required this.bottom,
required this.elevation,
required this.scrolledUnderElevation,
required this.shadowColor,
required this.surfaceTintColor,
required this.forceElevated,
required this.backgroundColor,
required this.foregroundColor,
required this.iconTheme,
required this.actionsIconTheme,
required this.primary,
required this.centerTitle,
required this.excludeHeaderSemantics,
required this.titleSpacing,
required this.collapsedHeight,
required this.topPadding,
required this.shape,
required this.toolbarHeight,
required this.leadingWidth,
required this.toolbarTextStyle,
required this.titleTextStyle,
required this.systemOverlayStyle,
required this.forceMaterialTransparency,
required this.useDefaultSemanticsOrder,
required this.clipBehavior,
required this.accessibleNavigation,
required this.actionsPadding,
}) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize.height ?? 0.0;
final Widget? leading;
final bool automaticallyImplyLeading;
final Widget title;
final List<Widget>? actions;
final bool automaticallyImplyActions;
final Widget flexibleSpace;
final PreferredSizeWidget? bottom;
final double? elevation;
final double? scrolledUnderElevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final bool forceElevated;
final Color? backgroundColor;
final Color? foregroundColor;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
final double? titleSpacing;
final double collapsedHeight;
final double topPadding;
final ShapeBorder? shape;
final double? toolbarHeight;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final double _bottomHeight;
final bool forceMaterialTransparency;
final bool useDefaultSemanticsOrder;
final Clip? clipBehavior;
final bool accessibleNavigation;
final EdgeInsetsGeometry? actionsPadding;
@override
double get minExtent => collapsedHeight;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
) {
maxExtent ??= double.infinity;
final bool isScrolledUnder =
overlapsContent ||
forceElevated ||
(shrinkOffset > maxExtent - minExtent);
final effectiveTitle = AnimatedOpacity(
opacity: isScrolledUnder ? 1 : 0,
duration: const Duration(milliseconds: 500),
curve: const Cubic(0.2, 0.0, 0.0, 1.0),
child: title,
);
return FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
isScrolledUnder: isScrolledUnder,
hasLeading: leading != null || automaticallyImplyLeading,
child: AppBar(
clipBehavior: clipBehavior,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
title: effectiveTitle,
actions: actions,
automaticallyImplyActions: automaticallyImplyActions,
flexibleSpace: maxExtent == .infinity
? flexibleSpace
: FlexibleSpaceBar(background: flexibleSpace),
bottom: bottom,
elevation: isScrolledUnder ? elevation : 0.0,
scrolledUnderElevation: scrolledUnderElevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
primary: primary,
centerTitle: centerTitle,
excludeHeaderSemantics: excludeHeaderSemantics,
titleSpacing: titleSpacing,
shape: shape,
toolbarHeight: toolbarHeight,
leadingWidth: leadingWidth,
toolbarTextStyle: toolbarTextStyle,
titleTextStyle: titleTextStyle,
systemOverlayStyle: systemOverlayStyle,
forceMaterialTransparency: forceMaterialTransparency,
useDefaultSemanticsOrder: useDefaultSemanticsOrder,
actionsPadding: actionsPadding,
),
);
}
@override
bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
return leading != oldDelegate.leading ||
automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading ||
title != oldDelegate.title ||
actions != oldDelegate.actions ||
automaticallyImplyActions != oldDelegate.automaticallyImplyActions ||
flexibleSpace != oldDelegate.flexibleSpace ||
bottom != oldDelegate.bottom ||
_bottomHeight != oldDelegate._bottomHeight ||
elevation != oldDelegate.elevation ||
shadowColor != oldDelegate.shadowColor ||
backgroundColor != oldDelegate.backgroundColor ||
foregroundColor != oldDelegate.foregroundColor ||
iconTheme != oldDelegate.iconTheme ||
actionsIconTheme != oldDelegate.actionsIconTheme ||
primary != oldDelegate.primary ||
centerTitle != oldDelegate.centerTitle ||
titleSpacing != oldDelegate.titleSpacing ||
topPadding != oldDelegate.topPadding ||
forceElevated != oldDelegate.forceElevated ||
toolbarHeight != oldDelegate.toolbarHeight ||
leadingWidth != oldDelegate.leadingWidth ||
toolbarTextStyle != oldDelegate.toolbarTextStyle ||
titleTextStyle != oldDelegate.titleTextStyle ||
systemOverlayStyle != oldDelegate.systemOverlayStyle ||
forceMaterialTransparency != oldDelegate.forceMaterialTransparency ||
useDefaultSemanticsOrder != oldDelegate.useDefaultSemanticsOrder ||
accessibleNavigation != oldDelegate.accessibleNavigation ||
actionsPadding != oldDelegate.actionsPadding;
}
@override
String toString() {
return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
}
}
class DynamicSliverAppBar extends StatefulWidget {
const DynamicSliverAppBar.medium({
super.key,
this.leading,
this.automaticallyImplyLeading = true,
required this.title,
this.actions,
this.automaticallyImplyActions = true,
required this.flexibleSpace,
this.bottom,
this.elevation,
this.scrolledUnderElevation,
this.shadowColor,
this.surfaceTintColor,
this.forceElevated = false,
this.backgroundColor,
this.foregroundColor,
this.iconTheme,
this.actionsIconTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
this.titleSpacing,
this.shape,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.useDefaultSemanticsOrder = true,
this.clipBehavior,
this.actionsPadding,
this.onPerformLayout,
});
final LayoutCallback? onPerformLayout;
final Widget? leading;
final bool automaticallyImplyLeading;
final Widget title;
final List<Widget>? actions;
final bool automaticallyImplyActions;
final Widget flexibleSpace;
final PreferredSizeWidget? bottom;
final double? elevation;
final double? scrolledUnderElevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final bool forceElevated;
final Color? backgroundColor;
final Color? foregroundColor;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
final double? titleSpacing;
final ShapeBorder? shape;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final bool forceMaterialTransparency;
final bool useDefaultSemanticsOrder;
final Clip? clipBehavior;
final EdgeInsetsGeometry? actionsPadding;
@override
State<DynamicSliverAppBar> createState() => _DynamicSliverAppBarState();
}
class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
@override
Widget build(BuildContext context) {
final double bottomHeight = widget.bottom?.preferredSize.height ?? 0.0;
final double topPadding = widget.primary
? MediaQuery.viewPaddingOf(context).top
: 0.0;
final double effectiveCollapsedHeight =
topPadding + kToolbarHeight + bottomHeight + 1;
return MediaQuery.removePadding(
context: context,
removeBottom: true,
child: SliverPinnedHeader(
onPerformLayout: widget.onPerformLayout,
delegate: _SliverAppBarDelegate(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title,
actions: widget.actions,
automaticallyImplyActions: widget.automaticallyImplyActions,
flexibleSpace: widget.flexibleSpace,
bottom: widget.bottom,
elevation: widget.elevation,
scrolledUnderElevation: widget.scrolledUnderElevation,
shadowColor: widget.shadowColor,
surfaceTintColor: widget.surfaceTintColor,
forceElevated: widget.forceElevated,
backgroundColor: widget.backgroundColor,
foregroundColor: widget.foregroundColor,
iconTheme: widget.iconTheme,
actionsIconTheme: widget.actionsIconTheme,
primary: widget.primary,
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing,
collapsedHeight: effectiveCollapsedHeight,
topPadding: topPadding,
shape: widget.shape,
toolbarHeight: kToolbarHeight,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,
systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency,
useDefaultSemanticsOrder: widget.useDefaultSemanticsOrder,
clipBehavior: widget.clipBehavior,
accessibleNavigation: MediaQuery.of(context).accessibleNavigation,
actionsPadding: widget.actionsPadding,
),
),
);
}
}

View File

@@ -0,0 +1,285 @@
/*
* 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:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart' hide LayoutCallback;
import 'package:flutter/widgets.dart'
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
/// ref [SliverPersistentHeader]
Rect? _trim(
Rect? original, {
double top = -double.infinity,
double right = double.infinity,
double bottom = double.infinity,
double left = -double.infinity,
}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));
abstract class RenderSliverPersistentHeader extends RenderSliver
with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
RenderSliverPersistentHeader({RenderBox? child}) {
this.child = child;
}
SliverPersistentHeaderElement? element;
double get minExtent =>
(element!.widget as SliverPinnedHeader).delegate.minExtent;
bool _needsUpdateChild = true;
double get lastShrinkOffset => _lastShrinkOffset;
double _lastShrinkOffset = 0.0;
bool get lastOverlapsContent => _lastOverlapsContent;
bool _lastOverlapsContent = false;
@protected
void updateChild(
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
) {
assert(element != null);
element!.build(shrinkOffset, overlapsContent, maxExtent);
}
@override
void markNeedsLayout() {
_needsUpdateChild = true;
super.markNeedsLayout();
}
@protected
void updateChildIfNeeded(
double scrollOffset,
double? maxExtent, {
bool overlapsContent = false,
}) {
final double shrinkOffset = maxExtent == null
? scrollOffset
: math.min(scrollOffset, maxExtent);
if (_needsUpdateChild ||
_lastShrinkOffset != shrinkOffset ||
_lastOverlapsContent != overlapsContent) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
updateChild(shrinkOffset, overlapsContent, maxExtent);
});
_lastShrinkOffset = shrinkOffset;
_lastOverlapsContent = overlapsContent;
_needsUpdateChild = false;
}
}
@override
double childMainAxisPosition(covariant RenderObject child) =>
super.childMainAxisPosition(child);
@override
bool hitTestChildren(
SliverHitTestResult result, {
required double mainAxisPosition,
required double crossAxisPosition,
}) {
assert(geometry!.hitTestExtent > 0.0);
if (child != null) {
return hitTestBoxChild(
BoxHitTestResult.wrap(result),
child!,
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition,
);
}
return false;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child == this.child);
applyPaintTransformForBoxChild(child as RenderBox, transform);
}
void triggerRebuild() {
markNeedsLayout();
}
}
class SliverPinnedHeader extends RenderObjectWidget {
const SliverPinnedHeader({
super.key,
required this.delegate,
this.onPerformLayout,
});
final SliverPersistentHeaderDelegate delegate;
final LayoutCallback? onPerformLayout;
@override
SliverPersistentHeaderElement createElement() =>
SliverPersistentHeaderElement(this);
@override
RenderSliverPinnedHeader createRenderObject(BuildContext context) {
return RenderSliverPinnedHeader(onPerformLayout: onPerformLayout);
}
@override
void updateRenderObject(
BuildContext context,
RenderSliverPinnedHeader renderObject,
) {
renderObject.onPerformLayout = onPerformLayout;
}
}
class RenderSliverPinnedHeader extends RenderSliverPersistentHeader {
RenderSliverPinnedHeader({
super.child,
this.onPerformLayout,
});
LayoutCallback? onPerformLayout;
({double crossAxisExtent, double maxExtent})? _maxExtent;
double? get maxExtent => _maxExtent?.maxExtent;
void _rawLayout() {
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
_maxExtent = (
crossAxisExtent: constraints.crossAxisExtent,
maxExtent: child!.size.height,
);
onPerformLayout?.call(child!.size);
}
void _layout() {
final double shrinkOffset = math.min(
constraints.scrollOffset,
_maxExtent!.maxExtent,
);
child!.layout(
constraints.asBoxConstraints(
maxExtent: math.max(minExtent, _maxExtent!.maxExtent - shrinkOffset),
),
parentUsesSize: true,
);
}
@override
void performLayout() {
final constraints = this.constraints;
final bool overlapsContent = constraints.overlap > 0.0;
if (_maxExtent == null) {
updateChildIfNeeded(
constraints.scrollOffset,
_maxExtent?.maxExtent,
overlapsContent: overlapsContent,
);
_rawLayout();
} else {
if (_maxExtent!.crossAxisExtent == constraints.crossAxisExtent) {
updateChildIfNeeded(
constraints.scrollOffset,
_maxExtent?.maxExtent,
overlapsContent: overlapsContent,
);
_layout();
} else {
_needsUpdateChild = true;
updateChildIfNeeded(
constraints.scrollOffset,
null,
overlapsContent: overlapsContent,
);
_rawLayout();
if (constraints.scrollOffset > 0.0) {
_needsUpdateChild = true;
updateChildIfNeeded(
constraints.scrollOffset,
_maxExtent?.maxExtent,
overlapsContent: overlapsContent,
);
_layout();
}
}
}
final childExtent = child!.size.height;
final maxExtent = _maxExtent!.maxExtent;
final double effectiveRemainingPaintExtent = math.max(
0,
constraints.remainingPaintExtent - constraints.overlap,
);
final double layoutExtent = clampDouble(
maxExtent - constraints.scrollOffset,
0.0,
effectiveRemainingPaintExtent,
);
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: constraints.overlap,
paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
layoutExtent: layoutExtent,
maxPaintExtent: maxExtent,
maxScrollObstructionExtent: minExtent,
cacheExtent: layoutExtent > 0.0
? -constraints.cacheOrigin + layoutExtent
: layoutExtent,
hasVisualOverflow: false,
);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
context.paintChild(child!, offset);
}
}
@override
double childMainAxisPosition(RenderBox child) => 0.0;
@override
void showOnScreen({
RenderObject? descendant,
Rect? rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
final Rect? localBounds = descendant != null
? MatrixUtils.transformRect(
descendant.getTransformTo(this),
rect ?? descendant.paintBounds,
)
: rect;
final Rect? newRect = _trim(localBounds, top: 0);
super.showOnScreen(
descendant: this,
rect: newRect,
duration: duration,
curve: curve,
);
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart';
import 'package:flutter/widgets.dart';
/// ref [SliverPersistentHeader]
abstract class SliverPersistentHeaderDelegate {
const SliverPersistentHeaderDelegate();
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
);
double get minExtent;
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
}
class SliverPersistentHeaderElement extends RenderObjectElement {
SliverPersistentHeaderElement(
SliverPinnedHeader super.widget,
);
@override
RenderSliverPinnedHeader get renderObject =>
super.renderObject as RenderSliverPinnedHeader;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
renderObject.element = this;
}
@override
void unmount() {
renderObject.element = null;
super.unmount();
}
@override
void update(SliverPinnedHeader newWidget) {
final oldWidget = widget as SliverPinnedHeader;
super.update(newWidget);
final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate;
final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate;
if (newDelegate != oldDelegate &&
(newDelegate.runtimeType != oldDelegate.runtimeType ||
newDelegate.shouldRebuild(oldDelegate))) {
final RenderSliverPinnedHeader renderObject = this.renderObject;
_updateChild(
newDelegate,
renderObject.lastShrinkOffset,
renderObject.lastOverlapsContent,
renderObject.maxExtent,
);
renderObject.triggerRebuild();
}
}
@override
void performRebuild() {
super.performRebuild();
renderObject.triggerRebuild();
}
Element? child;
void _updateChild(
SliverPersistentHeaderDelegate delegate,
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
) {
final Widget newWidget = delegate.build(
this,
shrinkOffset,
overlapsContent,
maxExtent,
);
child = updateChild(child, newWidget, null);
}
void build(double shrinkOffset, bool overlapsContent, double? maxExtent) {
owner!.buildScope(this, () {
final sliverPersistentHeaderRenderObjectWidget =
widget as SliverPinnedHeader;
_updateChild(
sliverPersistentHeaderRenderObjectWidget.delegate,
shrinkOffset,
overlapsContent,
maxExtent,
);
});
}
@override
void forgetChild(Element child) {
assert(child == this.child);
this.child = null;
super.forgetChild(child);
}
@override
void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
assert(renderObject.debugValidateChild(child));
renderObject.child = child;
}
@override
void moveRenderObjectChild(
covariant RenderObject child,
Object? oldSlot,
Object? newSlot,
) {
assert(false);
}
@override
void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
renderObject.child = null;
}
@override
void visitChildren(ElementVisitor visitor) {
if (child != null) {
visitor(child!);
}
}
}

View File

@@ -1,171 +0,0 @@
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class DynamicSliverAppBarMedium extends StatefulWidget {
const DynamicSliverAppBarMedium({
this.flexibleSpace,
super.key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.bottom,
this.elevation,
this.scrolledUnderElevation,
this.shadowColor,
this.surfaceTintColor,
this.forceElevated = false,
this.backgroundColor,
this.backgroundGradient,
this.foregroundColor,
this.iconTheme,
this.actionsIconTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
this.titleSpacing,
this.collapsedHeight,
this.expandedHeight,
this.floating = false,
this.pinned = false,
this.snap = false,
this.stretch = false,
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
this.shape,
this.toolbarHeight = kToolbarHeight,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.clipBehavior,
this.appBarClipper,
this.onPerformLayout,
});
final ValueChanged<double>? onPerformLayout;
final Widget? flexibleSpace;
final Widget? leading;
final bool automaticallyImplyLeading;
final Widget? title;
final List<Widget>? actions;
final PreferredSizeWidget? bottom;
final double? elevation;
final double? scrolledUnderElevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final bool forceElevated;
final Color? backgroundColor;
/// If backgroundGradient is non null, backgroundColor will be ignored
final LinearGradient? backgroundGradient;
final Color? foregroundColor;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
final double? titleSpacing;
final double? expandedHeight;
final double? collapsedHeight;
final bool floating;
final bool pinned;
final ShapeBorder? shape;
final double toolbarHeight;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final bool forceMaterialTransparency;
final Clip? clipBehavior;
final bool snap;
final bool stretch;
final double stretchTriggerOffset;
final AsyncCallback? onStretchTrigger;
final CustomClipper<Path>? appBarClipper;
@override
State<DynamicSliverAppBarMedium> createState() =>
_DynamicSliverAppBarMediumState();
}
class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
double? _height;
double? _width;
late double _topPadding;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_topPadding = MediaQuery.viewPaddingOf(context).top;
final width = MediaQuery.widthOf(context);
if (_width != width) {
_width = width;
_height = null;
}
}
@override
Widget build(BuildContext context) {
if (_height == null) {
return SliverToBoxAdapter(
child: OnlyLayoutWidget(
onPerformLayout: (Size size) {
if (!mounted) return;
_height = size.height;
widget.onPerformLayout?.call(_height!);
setState(() {});
},
child: UnconstrainedBox(
alignment: Alignment.topLeft,
child: SizedBox(
width: _width,
child: widget.flexibleSpace,
),
),
),
);
}
return SliverAppBar.medium(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title,
actions: widget.actions,
bottom: widget.bottom,
elevation: widget.elevation,
scrolledUnderElevation: widget.scrolledUnderElevation,
shadowColor: widget.shadowColor,
surfaceTintColor: widget.surfaceTintColor,
forceElevated: widget.forceElevated,
backgroundColor: widget.backgroundColor,
foregroundColor: widget.foregroundColor,
iconTheme: widget.iconTheme,
actionsIconTheme: widget.actionsIconTheme,
primary: widget.primary,
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing,
floating: widget.floating,
pinned: widget.pinned,
snap: widget.snap,
stretch: widget.stretch,
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: kToolbarHeight,
collapsedHeight: kToolbarHeight + _topPadding + 1,
expandedHeight: _height! - _topPadding,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,
systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency,
clipBehavior: widget.clipBehavior,
flexibleSpace: FlexibleSpaceBar(background: widget.flexibleSpace),
);
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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;
/// ref [SliverFloatingHeader]
class SliverFloatingHeaderWidget extends SingleChildRenderObjectWidget {
const SliverFloatingHeaderWidget({
super.key,
required Widget super.child,
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);
}
@override
void performLayout() {
if (!floatingHeaderNeedsToBeUpdated) {
effectiveScrollOffset = constraints.scrollOffset;
} else {
double delta =
lastScrollOffset! -
constraints.scrollOffset; // > 0 when the header is growing
if (constraints.userScrollDirection == .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!));
if (_backgroundColor != null) {
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);
}
}
}

View File

@@ -0,0 +1,123 @@
/*
* 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/rendering.dart'
show RenderSliverSingleBoxAdapter, SliverConstraints, SliverGeometry;
import 'package:flutter/widgets.dart';
/// ref [SliverPersistentHeader]
class SliverPinnedDynamicHeader extends SingleChildRenderObjectWidget {
const SliverPinnedDynamicHeader({
super.key,
required Widget super.child,
required this.minExtent,
required this.maxExtent,
});
final double minExtent;
final double maxExtent;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSliverPinnedDynamicHeader(
minExtent: minExtent,
maxExtent: maxExtent,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderSliverPinnedDynamicHeader renderObject,
) {
renderObject
..minExtent = minExtent
..maxExtent = maxExtent;
}
}
class RenderSliverPinnedDynamicHeader extends RenderSliverSingleBoxAdapter {
RenderSliverPinnedDynamicHeader({
required double minExtent,
required double maxExtent,
}) : _minExtent = minExtent,
_maxExtent = maxExtent;
double _minExtent;
double get minExtent => _minExtent;
set minExtent(double value) {
if (_minExtent == value) return;
_minExtent = value;
markNeedsLayout();
}
double _maxExtent;
double get maxExtent => _maxExtent;
set maxExtent(double value) {
// removed
// if (_maxExtent == value) return;
_maxExtent = value;
markNeedsLayout();
}
@override
void performLayout() {
final SliverConstraints constraints = this.constraints;
final double shrinkOffset = math.min(constraints.scrollOffset, maxExtent);
child!.layout(
constraints.asBoxConstraints(
maxExtent: math.max(minExtent, maxExtent - shrinkOffset),
),
parentUsesSize: true,
);
final double childExtent = child!.size.height;
final double effectiveRemainingPaintExtent = math.max(
0,
constraints.remainingPaintExtent - constraints.overlap,
);
final double layoutExtent = clampDouble(
maxExtent - constraints.scrollOffset,
0.0,
effectiveRemainingPaintExtent,
);
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: constraints.overlap,
paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
layoutExtent: layoutExtent,
maxPaintExtent: maxExtent,
maxScrollObstructionExtent: minExtent,
cacheExtent: layoutExtent > 0.0
? -constraints.cacheOrigin + layoutExtent
: layoutExtent,
hasVisualOverflow: false,
);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
context.paintChild(child!, offset);
}
}
@override
double childMainAxisPosition(RenderBox child) => 0.0;
}

View File

@@ -0,0 +1,118 @@
/*
* 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/rendering.dart'
show RenderSliverSingleBoxAdapter, SliverGeometry;
import 'package:flutter/widgets.dart';
/// ref [SliverPersistentHeader]
class SliverPinnedHeader extends SingleChildRenderObjectWidget {
const SliverPinnedHeader({
super.key,
required Widget super.child,
this.backgroundColor,
});
final Color? backgroundColor;
@override
RenderObject createRenderObject(BuildContext context) =>
RenderSliverPinnedHeader(backgroundColor: backgroundColor);
@override
void updateRenderObject(
BuildContext context,
RenderSliverPinnedHeader renderObject,
) {
renderObject.backgroundColor = backgroundColor;
}
}
class RenderSliverPinnedHeader extends RenderSliverSingleBoxAdapter {
RenderSliverPinnedHeader({
required Color? backgroundColor,
}) : _backgroundColor = backgroundColor;
Color? _backgroundColor;
set backgroundColor(Color? value) {
if (_backgroundColor == value) return;
_backgroundColor = value;
if (_isPinned) markNeedsPaint();
}
bool _isPinned = false;
@override
void performLayout() {
final constraints = this.constraints;
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double childExtent = child!.size.height;
final double effectiveRemainingPaintExtent = math.max(
0,
constraints.remainingPaintExtent - constraints.overlap,
);
final double layoutExtent = clampDouble(
childExtent - constraints.scrollOffset,
0.0,
effectiveRemainingPaintExtent,
);
_isPinned = constraints.overlap > 0.0 || constraints.scrollOffset > 0.0;
geometry = SliverGeometry(
scrollExtent: childExtent,
paintOrigin: constraints.overlap,
paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
layoutExtent: layoutExtent,
maxPaintExtent: childExtent,
maxScrollObstructionExtent: childExtent,
cacheExtent: layoutExtent > 0.0
? -constraints.cacheOrigin + layoutExtent
: layoutExtent,
hasVisualOverflow: false,
);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
if (_isPinned && _backgroundColor != null) {
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
double childMainAxisPosition(RenderBox child) => 0.0;
@override
bool hitTestSelf({
required double mainAxisPosition,
required double crossAxisPosition,
}) => true;
}