diff --git a/lib/common/constants.dart b/lib/common/constants.dart index a72f65cbf..1ef09d1f2 100644 --- a/lib/common/constants.dart +++ b/lib/common/constants.dart @@ -16,6 +16,13 @@ abstract final class StyleString { maxWidth: 420, ); static const topBarHeight = 52.0; + static const buttonStyle = ButtonStyle( + visualDensity: VisualDensity( + horizontal: -2, + vertical: -1.25, + ), + tapTargetSize: .shrinkWrap, + ); } abstract final class Constants { diff --git a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart deleted file mode 100644 index bfc354861..000000000 --- a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart +++ /dev/null @@ -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子组件 - //shrinkOffset:child偏移值minExtent~maxExtent - //overlapsContent:SliverPersistentHeader覆盖其他子组件返回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; -} diff --git a/lib/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart b/lib/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart new file mode 100644 index 000000000..efa57d2b6 --- /dev/null +++ b/lib/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart @@ -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 . + */ + +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? 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? 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 createState() => _DynamicSliverAppBarState(); +} + +class _DynamicSliverAppBarState extends State { + @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, + ), + ), + ); + } +} diff --git a/lib/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart b/lib/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart new file mode 100644 index 000000000..d4f31aea6 --- /dev/null +++ b/lib/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart @@ -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 . + */ + +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, 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 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, + ); + } +} diff --git a/lib/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart b/lib/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart new file mode 100644 index 000000000..c9fb7753f --- /dev/null +++ b/lib/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart @@ -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 . + */ + +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!); + } + } +} diff --git a/lib/common/widgets/dynamic_sliver_appbar_medium.dart b/lib/common/widgets/dynamic_sliver_appbar_medium.dart deleted file mode 100644 index c29859ff6..000000000 --- a/lib/common/widgets/dynamic_sliver_appbar_medium.dart +++ /dev/null @@ -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? onPerformLayout; - final Widget? flexibleSpace; - final Widget? leading; - final bool automaticallyImplyLeading; - final Widget? title; - final List? 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? appBarClipper; - - @override - State createState() => - _DynamicSliverAppBarMediumState(); -} - -class _DynamicSliverAppBarMediumState extends State { - 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), - ); - } -} diff --git a/lib/common/widgets/chat_list_view.dart b/lib/common/widgets/flutter/chat_list_view.dart similarity index 100% rename from lib/common/widgets/chat_list_view.dart rename to lib/common/widgets/flutter/chat_list_view.dart diff --git a/lib/common/widgets/sliver/sliver_floating_header.dart b/lib/common/widgets/sliver/sliver_floating_header.dart new file mode 100644 index 000000000..30b7e9370 --- /dev/null +++ b/lib/common/widgets/sliver/sliver_floating_header.dart @@ -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 . + */ + +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); + } + } +} diff --git a/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart b/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart new file mode 100644 index 000000000..725e32d81 --- /dev/null +++ b/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart @@ -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 . + */ + +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; +} diff --git a/lib/common/widgets/sliver/sliver_pinned_header.dart b/lib/common/widgets/sliver/sliver_pinned_header.dart new file mode 100644 index 000000000..2a1df95a8 --- /dev/null +++ b/lib/common/widgets/sliver/sliver_pinned_header.dart @@ -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 . + */ + +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; +} diff --git a/lib/pages/common/dyn/common_dyn_page.dart b/lib/pages/common/dyn/common_dyn_page.dart index e96df0ed2..e8ad0b64b 100644 --- a/lib/pages/common/dyn/common_dyn_page.dart +++ b/lib/pages/common/dyn/common_dyn_page.dart @@ -1,8 +1,9 @@ import 'dart:math' show pi; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_reply.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -103,44 +104,33 @@ abstract class CommonDynPageState extends State Widget buildReplyHeader(ThemeData theme) { final secondary = theme.colorScheme.secondary; - return SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 45, - bgColor: theme.colorScheme.surface, - child: Container( - height: 45, - padding: const EdgeInsets.only(left: 12, right: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () { - final count = controller.count.value; - return Text( - '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复', - ); - }, - ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, - color: secondary, - ), - label: Obx( - () => Text( - controller.sortType.value.label, - style: TextStyle(fontSize: 13, color: secondary), - ), - ), + return SliverPinnedHeader( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const .fromLTRB(12, 2.5, 6, 2.5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () { + final count = controller.count.value; + return Text( + '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复', + ); + }, + ), + TextButton.icon( + style: StyleString.buttonStyle, + onPressed: controller.queryBySort, + icon: Icon(Icons.sort, size: 16, color: secondary), + label: Obx( + () => Text( + controller.sortType.value.label, + style: TextStyle(fontSize: 13, color: secondary), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/dynamics_mention/view.dart b/lib/pages/dynamics_mention/view.dart index 6b14f96fb..219659688 100644 --- a/lib/pages/dynamics_mention/view.dart +++ b/lib/pages/dynamics_mention/view.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:math'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart' as topic_sheet; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart'; import 'package:PiliPlus/pages/dynamics_mention/controller.dart'; @@ -247,18 +247,14 @@ class _DynMentionPanelState } return SliverMainAxisGroup( slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - needRebuild: true, - bgColor: theme.colorScheme.surface, - child: Container( - height: 40, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text(group.groupName!), + SliverPinnedHeader( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const .symmetric( + horizontal: 16, + vertical: 10, ), + child: Text(group.groupName!), ), ), SliverList.builder( diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart index edf9f5eee..03451d572 100644 --- a/lib/pages/dynamics_topic/view.dart +++ b/lib/pages/dynamics_topic/view.dart @@ -1,10 +1,10 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; -import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart'; +import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart'; @@ -78,56 +78,49 @@ class _DynTopicPageState extends State with DynMixin { Obx(() { final allSortBy = _controller.topicSortByConf.value?.allSortBy; if (allSortBy != null && allSortBy.isNotEmpty) { - return SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 36, - needRebuild: true, - bgColor: theme.colorScheme.surface, - child: Container( - height: 36, - padding: EdgeInsets.only( - left: 12 + padding.left, - top: 6, - bottom: 6, - ), - child: Builder( - builder: (context) { - return ToggleButtons( - fillColor: theme.colorScheme.secondaryContainer, - selectedColor: - theme.colorScheme.onSecondaryContainer, - constraints: const BoxConstraints( - minWidth: 54, - minHeight: 24, - ), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - borderRadius: const .all(.circular(25)), - onPressed: (index) { - _controller.onSort(allSortBy[index].sortBy!); - (context as Element).markNeedsBuild(); - }, - isSelected: allSortBy - .map((e) => e.sortBy == _controller.sortBy) - .toList(), - children: allSortBy.map((e) { - return Text( - e.sortName!, - style: const TextStyle( - fontSize: 13, - height: 1, - ), - strutStyle: const StrutStyle( - height: 1, - leading: 0, - fontSize: 13, - ), - textScaler: TextScaler.noScaling, - ); - }).toList(), - ); - }, - ), + return SliverPinnedHeader( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: EdgeInsets.only( + left: 12 + padding.left, + top: 6, + bottom: 6, + ), + child: Builder( + builder: (context) { + return ToggleButtons( + fillColor: theme.colorScheme.secondaryContainer, + selectedColor: theme.colorScheme.onSecondaryContainer, + constraints: const BoxConstraints( + minWidth: 54, + minHeight: 24, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + borderRadius: const .all(.circular(25)), + onPressed: (index) { + _controller.onSort(allSortBy[index].sortBy!); + (context as Element).markNeedsBuild(); + }, + isSelected: allSortBy + .map((e) => e.sortBy == _controller.sortBy) + .toList(), + children: allSortBy.map((e) { + return Text( + e.sortName!, + style: const TextStyle( + fontSize: 13, + height: 1, + ), + strutStyle: const StrutStyle( + height: 1, + leading: 0, + fontSize: 13, + ), + textScaler: TextScaler.noScaling, + ); + }).toList(), + ); + }, ), ), ); @@ -157,10 +150,9 @@ class _DynTopicPageState extends State with DynMixin { ) { return switch (topState) { Loading() => const SliverAppBar(), - Success(:final response) when response != null => DynamicSliverAppBarMedium( - pinned: true, - onPerformLayout: (value) => - _controller.appbarOffset = value - kToolbarHeight - padding.top, + Success(:final response) when response != null => DynamicSliverAppBar.medium( + onPerformLayout: (value) => _controller.appbarOffset = + value.height - kToolbarHeight - padding.top, title: IgnorePointer(child: Text(response.topicItem!.name)), flexibleSpace: Container( decoration: BoxDecoration( diff --git a/lib/pages/live_dm_block/view.dart b/lib/pages/live_dm_block/view.dart index f7f097d89..f181a1f35 100644 --- a/lib/pages/live_dm_block/view.dart +++ b/lib/pages/live_dm_block/view.dart @@ -1,8 +1,8 @@ -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/models/common/live/live_dm_silent_type.dart'; import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart'; import 'package:PiliPlus/pages/live_dm_block/controller.dart'; @@ -102,13 +102,7 @@ class _LiveDmBlockPageState extends State { ExtendedNestedScrollView.sliverOverlapAbsorberHandleFor( context, ), - sliver: SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 48, - child: tabBar, - ), - ), + sliver: SliverPinnedHeader(child: tabBar), ), ]; }, diff --git a/lib/pages/main_reply/view.dart b/lib/pages/main_reply/view.dart index e52aff774..097d91a88 100644 --- a/lib/pages/main_reply/view.dart +++ b/lib/pages/main_reply/view.dart @@ -1,7 +1,8 @@ +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_reply.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -15,7 +16,6 @@ import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' show ScrollDirection; import 'package:get/get.dart'; class MainReplyPage extends StatefulWidget { @@ -61,9 +61,9 @@ class _MainReplyPageState extends State { body: NotificationListener( onNotification: (notification) { final direction = notification.direction; - if (direction == ScrollDirection.forward) { + if (direction == .forward) { _controller.showFab(); - } else if (direction == ScrollDirection.reverse) { + } else if (direction == .reverse) { _controller.hideFab(); } return false; @@ -172,44 +172,33 @@ class _MainReplyPageState extends State { Widget buildReplyHeader(ColorScheme colorScheme) { final secondary = colorScheme.secondary; - return SliverPersistentHeader( - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 45, - bgColor: colorScheme.surface, - child: Container( - height: 45, - padding: const EdgeInsets.only(left: 12, right: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () { - final count = _controller.count.value; - return Text( - '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复', - ); - }, - ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: _controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, - color: secondary, - ), - label: Obx( - () => Text( - _controller.sortType.value.label, - style: TextStyle(fontSize: 13, color: secondary), - ), - ), + return SliverFloatingHeaderWidget( + backgroundColor: colorScheme.surface, + child: Padding( + padding: const .fromLTRB(12, 2.5, 6, 2.5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () { + final count = _controller.count.value; + return Text( + '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复', + ); + }, + ), + TextButton.icon( + style: StyleString.buttonStyle, + onPressed: _controller.queryBySort, + icon: Icon(Icons.sort, size: 16, color: secondary), + label: Obx( + () => Text( + _controller.sortType.value.label, + style: TextStyle(fontSize: 13, color: secondary), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 6f6140a0c..c7df772c7 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -1,5 +1,5 @@ import 'package:PiliPlus/common/widgets/dialog/report_member.dart'; -import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart'; +import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -328,8 +328,7 @@ class _MemberPageState extends State { return const CircularProgressIndicator(); case Success(:final response): if (response != null) { - return DynamicSliverAppBarMedium( - pinned: true, + return DynamicSliverAppBar.medium( actions: _actions(theme), title: Text(_userController.username ?? ''), flexibleSpace: Obx( diff --git a/lib/pages/member_audio/view.dart b/lib/pages/member_audio/view.dart index 3c53df03c..1e0f978f4 100644 --- a/lib/pages/member_audio/view.dart +++ b/lib/pages/member_audio/view.dart @@ -1,8 +1,8 @@ import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_audio/item.dart'; import 'package:PiliPlus/pages/member_audio/controller.dart'; @@ -80,44 +80,36 @@ class _MemberAudioState extends State response != null && response.isNotEmpty ? SliverMainAxisGroup( slivers: [ - SliverPersistentHeader( - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: colorScheme.surface, - child: SizedBox( - height: 40, - child: Row( - children: [ - const SizedBox(width: 8), - Padding( - padding: const EdgeInsets.only(left: 6), - child: Text( - '共${_controller.totalSize ?? 0}首', - style: const TextStyle(fontSize: 13), + SliverFloatingHeaderWidget( + backgroundColor: colorScheme.surface, + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5), + child: Row( + children: [ + Text( + '共${_controller.totalSize ?? 0}首', + style: const TextStyle(fontSize: 13), + ), + Padding( + padding: const EdgeInsets.only(left: 6), + child: TextButton.icon( + style: StyleString.buttonStyle, + onPressed: _controller.toViewPlayAll, + icon: Icon( + Icons.play_circle_outline_rounded, + size: 16, + color: colorScheme.secondary, ), - ), - Container( - height: 35, - padding: const EdgeInsets.only(left: 6), - child: TextButton.icon( - onPressed: _controller.toViewPlayAll, - icon: Icon( - Icons.play_circle_outline_rounded, - size: 16, + label: Text( + '播放全部', + style: TextStyle( + fontSize: 13, color: colorScheme.secondary, ), - label: Text( - '播放全部', - style: TextStyle( - fontSize: 13, - color: colorScheme.secondary, - ), - ), ), ), - ], - ), + ), + ], ), ), ), diff --git a/lib/pages/member_contribute/view.dart b/lib/pages/member_contribute/view.dart index 6cf17a3e1..06a6443a6 100644 --- a/lib/pages/member_contribute/view.dart +++ b/lib/pages/member_contribute/view.dart @@ -97,13 +97,14 @@ class _MemberContributeState extends State } Widget _getPageFromType(SpaceTab2Item item) { + final isSingle = _controller.tabs == null; return switch (item.param) { 'video' => MemberVideo( type: ContributeType.video, heroTag: widget.heroTag, mid: widget.mid, title: item.title, - isSingle: _controller.tabs == null, + isSingle: isSingle, ), 'charging_video' => MemberVideo( type: ContributeType.charging, @@ -116,9 +117,9 @@ class _MemberContributeState extends State mid: widget.mid, ), 'opus' => MemberOpus( - isSingle: _controller.tabs == null, heroTag: widget.heroTag, mid: widget.mid, + isSingle: isSingle, ), 'audio' => MemberAudio( heroTag: widget.heroTag, diff --git a/lib/pages/member_favorite/view.dart b/lib/pages/member_favorite/view.dart index 8962a092e..599f33eae 100644 --- a/lib/pages/member_favorite/view.dart +++ b/lib/pages/member_favorite/view.dart @@ -1,7 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_card_h.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_fav/data.dart'; import 'package:PiliPlus/pages/member_favorite/controller.dart'; @@ -109,56 +109,51 @@ class _MemberFavoriteState extends State }) { return SliverMainAxisGroup( slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - child: Material( - color: theme.colorScheme.surface, - child: Builder( - builder: (context) { - return InkWell( - onTap: () { - _controller.setExpand(isFav); - (context as Element).markNeedsBuild(); - data.refresh(); - if (!isEnd.value) { - isEnd.refresh(); - } - }, - child: Container( - height: 45, - alignment: .centerLeft, - padding: const .only(left: 12), - child: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: .middle, - child: Icon( - _controller.isExpand(isFav) - ? Icons.expand_less - : Icons.expand_more, - color: theme.colorScheme.outline, - ), + SliverPinnedHeader( + child: Material( + color: theme.colorScheme.surface, + child: Builder( + builder: (context) { + return InkWell( + onTap: () { + _controller.setExpand(isFav); + (context as Element).markNeedsBuild(); + data.refresh(); + if (!isEnd.value) { + isEnd.refresh(); + } + }, + child: Padding( + padding: const .symmetric(horizontal: 12, vertical: 10), + child: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: .middle, + child: Icon( + _controller.isExpand(isFav) + ? Icons.expand_less + : Icons.expand_more, + color: theme.colorScheme.outline, ), - TextSpan( - text: ' ${data.value.name}', - style: const TextStyle(fontSize: 14), + ), + TextSpan( + text: ' ${data.value.name}', + style: const TextStyle(fontSize: 14), + ), + TextSpan( + text: ' ${data.value.mediaListResponse?.count}', + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.outline, ), - TextSpan( - text: ' ${data.value.mediaListResponse?.count}', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.outline, - ), - ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }, ), ), ), diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index 5d2e27fd6..2a9096755 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -1,7 +1,8 @@ -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; @@ -173,91 +174,78 @@ class _MemberVideoState extends State response != null && response.isNotEmpty ? SliverMainAxisGroup( slivers: [ - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: SizedBox( - height: 40, - child: Row( - children: [ - const SizedBox(width: 8), - Padding( - padding: const EdgeInsets.only(left: 6), - child: Obx( - () { - final count = _controller.count.value; - return Text( - count != -1 ? '共$count视频' : '', - style: const TextStyle(fontSize: 13), - ); - }, - ), - ), - Obx( - () { - final episodicButton = - _controller.episodicButton.value; - return episodicButton.uri?.isNotEmpty == true - ? Container( - height: 35, - padding: EdgeInsets.only( - left: _controller.count.value != -1 - ? 6 - : 0, + SliverFloatingHeaderWidget( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5), + child: Row( + children: [ + Obx( + () { + final count = _controller.count.value; + return Text( + count != -1 ? '共$count视频' : '', + style: const TextStyle(fontSize: 13), + ); + }, + ), + Obx( + () { + final episodicButton = + _controller.episodicButton.value; + return episodicButton.uri?.isNotEmpty == true + ? Padding( + padding: EdgeInsets.only( + left: _controller.count.value != -1 + ? 6 + : 0, + ), + child: TextButton.icon( + style: StyleString.buttonStyle, + onPressed: _controller.toViewPlayAll, + icon: Icon( + Icons.play_circle_outline_rounded, + size: 16, + color: theme.colorScheme.secondary, ), - child: TextButton.icon( - onPressed: _controller.toViewPlayAll, - icon: Icon( - Icons.play_circle_outline_rounded, - size: 16, + label: Text( + episodicButton.text ?? '播放全部', + style: TextStyle( + fontSize: 13, color: theme.colorScheme.secondary, ), - label: Text( - episodicButton.text ?? '播放全部', - style: TextStyle( - fontSize: 13, - color: - theme.colorScheme.secondary, - ), - ), ), - ) - : const SizedBox.shrink(); - }, + ), + ) + : const SizedBox.shrink(); + }, + ), + const Spacer(), + TextButton.icon( + style: StyleString.buttonStyle, + onPressed: _controller.queryBySort, + icon: Icon( + Icons.sort, + size: 16, + color: theme.colorScheme.secondary, ), - const Spacer(), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: _controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, + label: Obx( + () => Text( + widget.type == ContributeType.video + ? _controller.order.value == 'pubdate' + ? '最新发布' + : '最多播放' + : _controller.sort.value == 'desc' + ? '默认' + : '倒序', + style: TextStyle( + fontSize: 13, color: theme.colorScheme.secondary, ), - label: Obx( - () => Text( - widget.type == ContributeType.video - ? _controller.order.value == 'pubdate' - ? '最新发布' - : '最多播放' - : _controller.sort.value == 'desc' - ? '默认' - : '倒序', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.secondary, - ), - ), - ), ), ), - const SizedBox(width: 8), - ], - ), + ), + ], ), ), ), diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index 84eea7084..c292b5d6d 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -1,11 +1,12 @@ +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/common/pgc_review_type.dart'; @@ -383,51 +384,43 @@ class _PgcReviewChildPageState extends State ); } - Widget _buildHeader(ThemeData theme) => SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () { - final count = _controller.count.value; - return count == null - ? const SizedBox.shrink() - : Text( - '${NumUtils.numFormat(count)}条点评', - style: const TextStyle(fontSize: 13), - ); - }, + Widget _buildHeader(ThemeData theme) => SliverFloatingHeaderWidget( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () { + final count = _controller.count.value; + return count == null + ? const SizedBox.shrink() + : Text( + '${NumUtils.numFormat(count)}条点评', + style: const TextStyle(fontSize: 13), + ); + }, + ), + TextButton.icon( + style: StyleString.buttonStyle, + onPressed: _controller.queryBySort, + icon: Icon( + Icons.sort, + size: 16, + color: theme.colorScheme.secondary, ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: _controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, + label: Obx( + () => Text( + _controller.sortType.value.label, + style: TextStyle( + fontSize: 13, color: theme.colorScheme.secondary, ), - label: Obx( - () => Text( - _controller.sortType.value.label, - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.secondary, - ), - ), - ), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/popular_series/view.dart b/lib/pages/popular_series/view.dart index 3b402773d..cb9b061ad 100644 --- a/lib/pages/popular_series/view.dart +++ b/lib/pages/popular_series/view.dart @@ -1,9 +1,9 @@ import 'dart:math'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -201,17 +201,11 @@ class _PopularSeriesPageState extends State with GridMixin { ], ); } - final height = MediaQuery.textScalerOf(context).scale(27); - return SliverPersistentHeader( - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: height, - child: Container( - height: height, - padding: const EdgeInsets.only(left: 14, bottom: 7), - child: child, - ), - bgColor: colorScheme.surface, + return SliverFloatingHeaderWidget( + backgroundColor: colorScheme.surface, + child: Padding( + padding: const .only(left: 14, bottom: 7), + child: child, ), ); } diff --git a/lib/pages/search_panel/article/view.dart b/lib/pages/search_panel/article/view.dart index c65086a93..69892c830 100644 --- a/lib/pages/search_panel/article/view.dart +++ b/lib/pages/search_panel/article/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/pages/search_panel/article/controller.dart'; import 'package:PiliPlus/pages/search_panel/article/widgets/item.dart'; @@ -45,51 +45,45 @@ class _SearchArticlePanelState @override Widget buildHeader(ThemeData theme) { - return SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.only(left: 25, right: 12), - child: Row( - children: [ - Obx( - () => Text( - '排序: ${controller.articleOrderType.value.label}', - maxLines: 1, - style: TextStyle(color: theme.colorScheme.outline), + return SliverFloatingHeaderWidget( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const .fromLTRB(25, 0, 12, 4), + child: Row( + children: [ + Obx( + () => Text( + '排序: ${controller.articleOrderType.value.label}', + maxLines: 1, + style: TextStyle(color: theme.colorScheme.outline), + ), + ), + const Spacer(), + Obx( + () => Text( + '分区: ${controller.articleZoneType!.value.label}', + maxLines: 1, + style: TextStyle(color: theme.colorScheme.outline), + ), + ), + const Spacer(), + SizedBox( + width: 32, + height: 32, + child: IconButton( + tooltip: '筛选', + style: const ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.zero), + ), + onPressed: () => controller.onShowFilterDialog(context), + icon: Icon( + Icons.filter_list_outlined, + size: 18, + color: theme.colorScheme.primary, ), ), - const Spacer(), - Obx( - () => Text( - '分区: ${controller.articleZoneType!.value.label}', - maxLines: 1, - style: TextStyle(color: theme.colorScheme.outline), - ), - ), - const Spacer(), - SizedBox( - width: 32, - height: 32, - child: IconButton( - tooltip: '筛选', - style: const ButtonStyle( - padding: WidgetStatePropertyAll(EdgeInsets.zero), - ), - onPressed: () => controller.onShowFilterDialog(context), - icon: Icon( - Icons.filter_list_outlined, - size: 18, - color: theme.colorScheme.primary, - ), - ), - ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/search_panel/user/view.dart b/lib/pages/search_panel/user/view.dart index 65c1aa8d4..dc1008bec 100644 --- a/lib/pages/search_panel/user/view.dart +++ b/lib/pages/search_panel/user/view.dart @@ -1,5 +1,5 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/pages/search_panel/user/controller.dart'; import 'package:PiliPlus/pages/search_panel/user/widgets/item.dart'; @@ -46,51 +46,45 @@ class _SearchUserPanelState @override Widget buildHeader(ThemeData theme) { - return SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.only(left: 25, right: 12), - child: Row( - children: [ - Obx( - () => Text( - '排序: ${controller.userOrderType!.value.label}', - maxLines: 1, - style: TextStyle(color: theme.colorScheme.outline), + return SliverFloatingHeaderWidget( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const .fromLTRB(25, 0, 12, 4), + child: Row( + children: [ + Obx( + () => Text( + '排序: ${controller.userOrderType!.value.label}', + maxLines: 1, + style: TextStyle(color: theme.colorScheme.outline), + ), + ), + const Spacer(), + Obx( + () => Text( + '用户类型: ${controller.userType!.value.label}', + maxLines: 1, + style: TextStyle(color: theme.colorScheme.outline), + ), + ), + const Spacer(), + SizedBox( + width: 32, + height: 32, + child: IconButton( + tooltip: '筛选', + style: const ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.zero), + ), + onPressed: () => controller.onShowFilterDialog(context), + icon: Icon( + Icons.filter_list_outlined, + size: 18, + color: theme.colorScheme.primary, ), ), - const Spacer(), - Obx( - () => Text( - '用户类型: ${controller.userType!.value.label}', - maxLines: 1, - style: TextStyle(color: theme.colorScheme.outline), - ), - ), - const Spacer(), - SizedBox( - width: 32, - height: 32, - child: IconButton( - tooltip: '筛选', - style: const ButtonStyle( - padding: WidgetStatePropertyAll(EdgeInsets.zero), - ), - onPressed: () => controller.onShowFilterDialog(context), - icon: Icon( - Icons.filter_list_outlined, - size: 18, - color: theme.colorScheme.primary, - ), - ), - ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/search_panel/video/view.dart b/lib/pages/search_panel/video/view.dart index 14265c5f8..d7b28050b 100644 --- a/lib/pages/search_panel/video/view.dart +++ b/lib/pages/search_panel/video/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/models/common/search/video_search_type.dart'; import 'package:PiliPlus/models/search/result.dart'; @@ -47,61 +47,55 @@ class _SearchVideoPanelState @override Widget buildHeader(ThemeData theme) { - return SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 34, - bgColor: theme.colorScheme.surface, - child: Container( - height: 34, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Wrap( - children: [ - for (final e in ArchiveFilterType.values) - Obx( - () => SearchText( - fontSize: 13, - text: e.desc, - bgColor: Colors.transparent, - textColor: controller.selectedType.value == e - ? theme.colorScheme.primary - : theme.colorScheme.outline, - onTap: (_) => controller - ..order = e.name - ..selectedType.value = e - ..onSortSearch(getBack: false), - ), + return SliverFloatingHeaderWidget( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const .fromLTRB(12, 0, 12, 4), + child: Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + children: [ + for (final e in ArchiveFilterType.values) + Obx( + () => SearchText( + fontSize: 13, + text: e.desc, + bgColor: Colors.transparent, + textColor: controller.selectedType.value == e + ? theme.colorScheme.primary + : theme.colorScheme.outline, + onTap: (_) => controller + ..order = e.name + ..selectedType.value = e + ..onSortSearch(getBack: false), ), - ], - ), + ), + ], ), ), - const VerticalDivider(indent: 7, endIndent: 8), - const SizedBox(width: 3), - SizedBox( - width: 32, - height: 32, - child: IconButton( - tooltip: '筛选', - style: const ButtonStyle( - padding: WidgetStatePropertyAll(EdgeInsets.zero), - ), - onPressed: () => controller.onShowFilterDialog(context), - icon: Icon( - Icons.filter_list_outlined, - size: 18, - color: theme.colorScheme.primary, - ), + ), + const VerticalDivider(indent: 7, endIndent: 8), + const SizedBox(width: 3), + SizedBox( + width: 32, + height: 32, + child: IconButton( + tooltip: '筛选', + style: const ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.zero), + ), + onPressed: () => controller.onShowFilterDialog(context), + icon: Icon( + Icons.filter_list_outlined, + size: 18, + color: theme.colorScheme.primary, ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/video/member/view.dart b/lib/pages/video/member/view.dart index 50366a670..ab8276cef 100644 --- a/lib/pages/video/member/view.dart +++ b/lib/pages/video/member/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; @@ -136,24 +137,22 @@ class _HorizontalMemberPageState extends State { ); }, ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: () => _controller - ..lastAid = widget.videoDetailController.aid.toString() - ..queryBySort(), - icon: Icon( - Icons.sort, - size: 16, - color: theme.colorScheme.secondary, - ), - label: Obx( - () => Text( - _controller.order.value == 'pubdate' ? '最新发布' : '最多播放', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.secondary, - ), + TextButton.icon( + style: StyleString.buttonStyle, + onPressed: () => _controller + ..lastAid = widget.videoDetailController.aid.toString() + ..queryBySort(), + icon: Icon( + Icons.sort, + size: 16, + color: theme.colorScheme.secondary, + ), + label: Obx( + () => Text( + _controller.order.value == 'pubdate' ? '最新发布' : '最多播放', + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.secondary, ), ), ), diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index 38841e6ee..e3be23813 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -1,7 +1,8 @@ +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_reply.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; @@ -83,47 +84,39 @@ class _VideoReplyPanelState extends State : _videoReplyController.scrollController, physics: const AlwaysScrollableScrollPhysics(), key: const PageStorageKey(_VideoReplyPanelState), - slivers: [ - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () => Text( - _videoReplyController.sortType.value.title, - style: const TextStyle(fontSize: 13), - ), + slivers: [ + SliverFloatingHeaderWidget( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const .fromLTRB(12, 2.5, 6, 2.5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () => Text( + _videoReplyController.sortType.value.title, + style: const TextStyle(fontSize: 13), ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: _videoReplyController.queryBySort, - icon: Icon( - Icons.sort, - size: 16, + ), + TextButton.icon( + style: StyleString.buttonStyle, + onPressed: _videoReplyController.queryBySort, + icon: Icon( + Icons.sort, + size: 16, + color: theme.colorScheme.secondary, + ), + label: Obx( + () => Text( + _videoReplyController.sortType.value.label, + style: TextStyle( + fontSize: 13, color: theme.colorScheme.secondary, ), - label: Obx( - () => Text( - _videoReplyController.sortType.value.label, - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.secondary, - ), - ), - ), ), ), - ], - ), + ), + ], ), ), ), diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart index 09ebf3c47..e73dc14dd 100644 --- a/lib/pages/video/reply_reply/view.dart +++ b/lib/pages/video/reply_reply/view.dart @@ -1,8 +1,9 @@ +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/widgets/colored_box_transition.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo, Mode; @@ -240,52 +241,43 @@ class _VideoReplyReplyPanelState extends State } Widget _sortWidget(ThemeData theme) { - return SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () { - final count = _controller.count.value; - return count != -1 - ? Text( - '相关回复共${NumUtils.numFormat(count)}条', - style: const TextStyle(fontSize: 13), - ) - : const SizedBox.shrink(); - }, + return SliverPinnedHeader( + backgroundColor: theme.colorScheme.surface, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () { + final count = _controller.count.value; + return count != -1 + ? Text( + '相关回复共${NumUtils.numFormat(count)}条', + style: const TextStyle(fontSize: 13), + ) + : const SizedBox.shrink(); + }, + ), + TextButton.icon( + style: StyleString.buttonStyle, + onPressed: _controller.queryBySort, + icon: Icon( + Icons.sort, + size: 16, + color: theme.colorScheme.secondary, ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: _controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, + label: Obx( + () => Text( + _controller.mode.value == Mode.MAIN_LIST_HOT ? '按热度' : '按时间', + style: TextStyle( + fontSize: 13, color: theme.colorScheme.secondary, ), - label: Obx( - () => Text( - _controller.mode.value == Mode.MAIN_LIST_HOT - ? '按热度' - : '按时间', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.secondary, - ), - ), - ), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index c3b8dd517..df3db77b3 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -9,6 +9,7 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero_dialog_route.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_dynamic_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/episode_panel_type.dart'; @@ -648,14 +649,10 @@ class _VideoDetailPageVState extends State ? animHeight : videoDetailController.videoHeight; return [ - SliverAppBar( - elevation: 0, - scrolledUnderElevation: 0, - primary: false, - automaticallyImplyLeading: false, - pinned: true, - expandedHeight: height, - flexibleSpace: Stack( + SliverPinnedDynamicHeader( + minExtent: kToolbarHeight, + maxExtent: height, + child: Stack( clipBehavior: Clip.none, children: [ SizedBox( diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 7cd2dbd52..ff57111ae 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:io' show File; -import 'package:PiliPlus/common/widgets/chat_list_view.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; +import 'package:PiliPlus/common/widgets/flutter/chat_list_view.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';