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

@@ -16,6 +16,13 @@ abstract final class StyleString {
maxWidth: 420, maxWidth: 420,
); );
static const topBarHeight = 52.0; static const topBarHeight = 52.0;
static const buttonStyle = ButtonStyle(
visualDensity: VisualDensity(
horizontal: -2,
vertical: -1.25,
),
tapTargetSize: .shrinkWrap,
);
} }
abstract final class Constants { abstract final class Constants {

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;
}

View File

@@ -1,8 +1,9 @@
import 'dart:math' show pi; import 'dart:math' show pi;
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.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/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/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
@@ -103,44 +104,33 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
Widget buildReplyHeader(ThemeData theme) { Widget buildReplyHeader(ThemeData theme) {
final secondary = theme.colorScheme.secondary; final secondary = theme.colorScheme.secondary;
return SliverPersistentHeader( return SliverPinnedHeader(
pinned: true, backgroundColor: theme.colorScheme.surface,
delegate: CustomSliverPersistentHeaderDelegate( child: Padding(
extent: 45, padding: const .fromLTRB(12, 2.5, 6, 2.5),
bgColor: theme.colorScheme.surface, child: Row(
child: Container( mainAxisAlignment: MainAxisAlignment.spaceBetween,
height: 45, children: [
padding: const EdgeInsets.only(left: 12, right: 6), Obx(
child: Row( () {
mainAxisAlignment: MainAxisAlignment.spaceBetween, final count = controller.count.value;
children: [ return Text(
Obx( '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
() { );
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),
SizedBox( label: Obx(
height: 35, () => Text(
child: TextButton.icon( controller.sortType.value.label,
onPressed: controller.queryBySort, style: TextStyle(fontSize: 13, color: secondary),
icon: Icon(
Icons.sort,
size: 16,
color: secondary,
),
label: Obx(
() => Text(
controller.sortType.value.label,
style: TextStyle(fontSize: 13, color: secondary),
),
),
), ),
), ),
], ),
), ],
), ),
), ),
); );

View File

@@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; 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' import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart'
as topic_sheet; as topic_sheet;
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/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/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart';
import 'package:PiliPlus/pages/dynamics_mention/controller.dart'; import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
@@ -247,18 +247,14 @@ class _DynMentionPanelState
} }
return SliverMainAxisGroup( return SliverMainAxisGroup(
slivers: [ slivers: [
SliverPersistentHeader( SliverPinnedHeader(
pinned: true, backgroundColor: theme.colorScheme.surface,
delegate: CustomSliverPersistentHeaderDelegate( child: Padding(
extent: 40, padding: const .symmetric(
needRebuild: true, horizontal: 16,
bgColor: theme.colorScheme.surface, vertical: 10,
child: Container(
height: 40,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(group.groupName!),
), ),
child: Text(group.groupName!),
), ),
), ),
SliverList.builder( SliverList.builder(

View File

@@ -1,10 +1,10 @@
import 'package:PiliPlus/common/widgets/custom_icon.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/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/pair.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/http/loading_state.dart';
import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart';
@@ -78,56 +78,49 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
Obx(() { Obx(() {
final allSortBy = _controller.topicSortByConf.value?.allSortBy; final allSortBy = _controller.topicSortByConf.value?.allSortBy;
if (allSortBy != null && allSortBy.isNotEmpty) { if (allSortBy != null && allSortBy.isNotEmpty) {
return SliverPersistentHeader( return SliverPinnedHeader(
pinned: true, backgroundColor: theme.colorScheme.surface,
delegate: CustomSliverPersistentHeaderDelegate( child: Padding(
extent: 36, padding: EdgeInsets.only(
needRebuild: true, left: 12 + padding.left,
bgColor: theme.colorScheme.surface, top: 6,
child: Container( bottom: 6,
height: 36, ),
padding: EdgeInsets.only( child: Builder(
left: 12 + padding.left, builder: (context) {
top: 6, return ToggleButtons(
bottom: 6, fillColor: theme.colorScheme.secondaryContainer,
), selectedColor: theme.colorScheme.onSecondaryContainer,
child: Builder( constraints: const BoxConstraints(
builder: (context) { minWidth: 54,
return ToggleButtons( minHeight: 24,
fillColor: theme.colorScheme.secondaryContainer, ),
selectedColor: tapTargetSize: MaterialTapTargetSize.shrinkWrap,
theme.colorScheme.onSecondaryContainer, borderRadius: const .all(.circular(25)),
constraints: const BoxConstraints( onPressed: (index) {
minWidth: 54, _controller.onSort(allSortBy[index].sortBy!);
minHeight: 24, (context as Element).markNeedsBuild();
), },
tapTargetSize: MaterialTapTargetSize.shrinkWrap, isSelected: allSortBy
borderRadius: const .all(.circular(25)), .map((e) => e.sortBy == _controller.sortBy)
onPressed: (index) { .toList(),
_controller.onSort(allSortBy[index].sortBy!); children: allSortBy.map((e) {
(context as Element).markNeedsBuild(); return Text(
}, e.sortName!,
isSelected: allSortBy style: const TextStyle(
.map((e) => e.sortBy == _controller.sortBy) fontSize: 13,
.toList(), height: 1,
children: allSortBy.map((e) { ),
return Text( strutStyle: const StrutStyle(
e.sortName!, height: 1,
style: const TextStyle( leading: 0,
fontSize: 13, fontSize: 13,
height: 1, ),
), textScaler: TextScaler.noScaling,
strutStyle: const StrutStyle( );
height: 1, }).toList(),
leading: 0, );
fontSize: 13, },
),
textScaler: TextScaler.noScaling,
);
}).toList(),
);
},
),
), ),
), ),
); );
@@ -157,10 +150,9 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
) { ) {
return switch (topState) { return switch (topState) {
Loading() => const SliverAppBar(), Loading() => const SliverAppBar(),
Success(:final response) when response != null => DynamicSliverAppBarMedium( Success(:final response) when response != null => DynamicSliverAppBar.medium(
pinned: true, onPerformLayout: (value) => _controller.appbarOffset =
onPerformLayout: (value) => value.height - kToolbarHeight - padding.top,
_controller.appbarOffset = value - kToolbarHeight - padding.top,
title: IgnorePointer(child: Text(response.topicItem!.name)), title: IgnorePointer(child: Text(response.topicItem!.name)),
flexibleSpace: Container( flexibleSpace: Container(
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -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/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.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/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.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/common/live/live_dm_silent_type.dart';
import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart'; import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart';
import 'package:PiliPlus/pages/live_dm_block/controller.dart'; import 'package:PiliPlus/pages/live_dm_block/controller.dart';
@@ -102,13 +102,7 @@ class _LiveDmBlockPageState extends State<LiveDmBlockPage> {
ExtendedNestedScrollView.sliverOverlapAbsorberHandleFor( ExtendedNestedScrollView.sliverOverlapAbsorberHandleFor(
context, context,
), ),
sliver: SliverPersistentHeader( sliver: SliverPinnedHeader(child: tabBar),
pinned: true,
delegate: CustomSliverPersistentHeaderDelegate(
extent: 48,
child: tabBar,
),
),
), ),
]; ];
}, },

View File

@@ -1,7 +1,8 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.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/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
@@ -15,7 +16,6 @@ import 'package:PiliPlus/utils/num_utils.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:get/get.dart'; import 'package:get/get.dart';
class MainReplyPage extends StatefulWidget { class MainReplyPage extends StatefulWidget {
@@ -61,9 +61,9 @@ class _MainReplyPageState extends State<MainReplyPage> {
body: NotificationListener<UserScrollNotification>( body: NotificationListener<UserScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
final direction = notification.direction; final direction = notification.direction;
if (direction == ScrollDirection.forward) { if (direction == .forward) {
_controller.showFab(); _controller.showFab();
} else if (direction == ScrollDirection.reverse) { } else if (direction == .reverse) {
_controller.hideFab(); _controller.hideFab();
} }
return false; return false;
@@ -172,44 +172,33 @@ class _MainReplyPageState extends State<MainReplyPage> {
Widget buildReplyHeader(ColorScheme colorScheme) { Widget buildReplyHeader(ColorScheme colorScheme) {
final secondary = colorScheme.secondary; final secondary = colorScheme.secondary;
return SliverPersistentHeader( return SliverFloatingHeaderWidget(
floating: true, backgroundColor: colorScheme.surface,
delegate: CustomSliverPersistentHeaderDelegate( child: Padding(
extent: 45, padding: const .fromLTRB(12, 2.5, 6, 2.5),
bgColor: colorScheme.surface, child: Row(
child: Container( mainAxisAlignment: MainAxisAlignment.spaceBetween,
height: 45, children: [
padding: const EdgeInsets.only(left: 12, right: 6), Obx(
child: Row( () {
mainAxisAlignment: MainAxisAlignment.spaceBetween, final count = _controller.count.value;
children: [ return Text(
Obx( '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
() { );
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),
SizedBox( label: Obx(
height: 35, () => Text(
child: TextButton.icon( _controller.sortType.value.label,
onPressed: _controller.queryBySort, style: TextStyle(fontSize: 13, color: secondary),
icon: Icon(
Icons.sort,
size: 16,
color: secondary,
),
label: Obx(
() => Text(
_controller.sortType.value.label,
style: TextStyle(fontSize: 13, color: secondary),
),
),
), ),
), ),
], ),
), ],
), ),
), ),
); );

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/widgets/dialog/report_member.dart'; 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/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
@@ -328,8 +328,7 @@ class _MemberPageState extends State<MemberPage> {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
case Success<SpaceData?>(:final response): case Success<SpaceData?>(:final response):
if (response != null) { if (response != null) {
return DynamicSliverAppBarMedium( return DynamicSliverAppBar.medium(
pinned: true,
actions: _actions(theme), actions: _actions(theme),
title: Text(_userController.username ?? ''), title: Text(_userController.username ?? ''),
flexibleSpace: Obx( flexibleSpace: Obx(

View File

@@ -1,8 +1,8 @@
import 'package:PiliPlus/common/constants.dart'; 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/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/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/http/loading_state.dart';
import 'package:PiliPlus/models_new/space/space_audio/item.dart'; import 'package:PiliPlus/models_new/space/space_audio/item.dart';
import 'package:PiliPlus/pages/member_audio/controller.dart'; import 'package:PiliPlus/pages/member_audio/controller.dart';
@@ -80,44 +80,36 @@ class _MemberAudioState extends State<MemberAudio>
response != null && response.isNotEmpty response != null && response.isNotEmpty
? SliverMainAxisGroup( ? SliverMainAxisGroup(
slivers: [ slivers: [
SliverPersistentHeader( SliverFloatingHeaderWidget(
floating: true, backgroundColor: colorScheme.surface,
delegate: CustomSliverPersistentHeaderDelegate( child: Padding(
extent: 40, padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
bgColor: colorScheme.surface, child: Row(
child: SizedBox( children: [
height: 40, Text(
child: Row( '${_controller.totalSize ?? 0}',
children: [ style: const TextStyle(fontSize: 13),
const SizedBox(width: 8), ),
Padding( Padding(
padding: const EdgeInsets.only(left: 6), padding: const EdgeInsets.only(left: 6),
child: Text( child: TextButton.icon(
'${_controller.totalSize ?? 0}', style: StyleString.buttonStyle,
style: const TextStyle(fontSize: 13), onPressed: _controller.toViewPlayAll,
icon: Icon(
Icons.play_circle_outline_rounded,
size: 16,
color: colorScheme.secondary,
), ),
), label: Text(
Container( '播放全部',
height: 35, style: TextStyle(
padding: const EdgeInsets.only(left: 6), fontSize: 13,
child: TextButton.icon(
onPressed: _controller.toViewPlayAll,
icon: Icon(
Icons.play_circle_outline_rounded,
size: 16,
color: colorScheme.secondary, color: colorScheme.secondary,
), ),
label: Text(
'播放全部',
style: TextStyle(
fontSize: 13,
color: colorScheme.secondary,
),
),
), ),
), ),
], ),
), ],
), ),
), ),
), ),

View File

@@ -97,13 +97,14 @@ class _MemberContributeState extends State<MemberContribute>
} }
Widget _getPageFromType(SpaceTab2Item item) { Widget _getPageFromType(SpaceTab2Item item) {
final isSingle = _controller.tabs == null;
return switch (item.param) { return switch (item.param) {
'video' => MemberVideo( 'video' => MemberVideo(
type: ContributeType.video, type: ContributeType.video,
heroTag: widget.heroTag, heroTag: widget.heroTag,
mid: widget.mid, mid: widget.mid,
title: item.title, title: item.title,
isSingle: _controller.tabs == null, isSingle: isSingle,
), ),
'charging_video' => MemberVideo( 'charging_video' => MemberVideo(
type: ContributeType.charging, type: ContributeType.charging,
@@ -116,9 +117,9 @@ class _MemberContributeState extends State<MemberContribute>
mid: widget.mid, mid: widget.mid,
), ),
'opus' => MemberOpus( 'opus' => MemberOpus(
isSingle: _controller.tabs == null,
heroTag: widget.heroTag, heroTag: widget.heroTag,
mid: widget.mid, mid: widget.mid,
isSingle: isSingle,
), ),
'audio' => MemberAudio( 'audio' => MemberAudio(
heroTag: widget.heroTag, heroTag: widget.heroTag,

View File

@@ -1,7 +1,7 @@
import 'package:PiliPlus/common/skeleton/video_card_h.dart'; 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/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/http/loading_state.dart';
import 'package:PiliPlus/models_new/space/space_fav/data.dart'; import 'package:PiliPlus/models_new/space/space_fav/data.dart';
import 'package:PiliPlus/pages/member_favorite/controller.dart'; import 'package:PiliPlus/pages/member_favorite/controller.dart';
@@ -109,56 +109,51 @@ class _MemberFavoriteState extends State<MemberFavorite>
}) { }) {
return SliverMainAxisGroup( return SliverMainAxisGroup(
slivers: [ slivers: [
SliverPersistentHeader( SliverPinnedHeader(
pinned: true, child: Material(
delegate: CustomSliverPersistentHeaderDelegate( color: theme.colorScheme.surface,
child: Material( child: Builder(
color: theme.colorScheme.surface, builder: (context) {
child: Builder( return InkWell(
builder: (context) { onTap: () {
return InkWell( _controller.setExpand(isFav);
onTap: () { (context as Element).markNeedsBuild();
_controller.setExpand(isFav); data.refresh();
(context as Element).markNeedsBuild(); if (!isEnd.value) {
data.refresh(); isEnd.refresh();
if (!isEnd.value) { }
isEnd.refresh(); },
} child: Padding(
}, padding: const .symmetric(horizontal: 12, vertical: 10),
child: Container( child: Text.rich(
height: 45, TextSpan(
alignment: .centerLeft, children: [
padding: const .only(left: 12), WidgetSpan(
child: Text.rich( alignment: .middle,
TextSpan( child: Icon(
children: [ _controller.isExpand(isFav)
WidgetSpan( ? Icons.expand_less
alignment: .middle, : Icons.expand_more,
child: Icon( color: theme.colorScheme.outline,
_controller.isExpand(isFav)
? Icons.expand_less
: Icons.expand_more,
color: theme.colorScheme.outline,
),
), ),
TextSpan( ),
text: ' ${data.value.name}', TextSpan(
style: const TextStyle(fontSize: 14), 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,
),
),
],
),
), ),
), ),
); ),
}, );
), },
), ),
), ),
), ),

View File

@@ -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/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.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/http/loading_state.dart';
import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart';
import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart';
@@ -173,91 +174,78 @@ class _MemberVideoState extends State<MemberVideo>
response != null && response.isNotEmpty response != null && response.isNotEmpty
? SliverMainAxisGroup( ? SliverMainAxisGroup(
slivers: [ slivers: [
SliverPersistentHeader( SliverFloatingHeaderWidget(
pinned: false, backgroundColor: theme.colorScheme.surface,
floating: true, child: Padding(
delegate: CustomSliverPersistentHeaderDelegate( padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
extent: 40, child: Row(
bgColor: theme.colorScheme.surface, children: [
child: SizedBox( Obx(
height: 40, () {
child: Row( final count = _controller.count.value;
children: [ return Text(
const SizedBox(width: 8), count != -1 ? '$count视频' : '',
Padding( style: const TextStyle(fontSize: 13),
padding: const EdgeInsets.only(left: 6), );
child: Obx( },
() { ),
final count = _controller.count.value; Obx(
return Text( () {
count != -1 ? '$count视频' : '', final episodicButton =
style: const TextStyle(fontSize: 13), _controller.episodicButton.value;
); return episodicButton.uri?.isNotEmpty == true
}, ? Padding(
), padding: EdgeInsets.only(
), left: _controller.count.value != -1
Obx( ? 6
() { : 0,
final episodicButton = ),
_controller.episodicButton.value; child: TextButton.icon(
return episodicButton.uri?.isNotEmpty == true style: StyleString.buttonStyle,
? Container( onPressed: _controller.toViewPlayAll,
height: 35, icon: Icon(
padding: EdgeInsets.only( Icons.play_circle_outline_rounded,
left: _controller.count.value != -1 size: 16,
? 6 color: theme.colorScheme.secondary,
: 0,
), ),
child: TextButton.icon( label: Text(
onPressed: _controller.toViewPlayAll, episodicButton.text ?? '播放全部',
icon: Icon( style: TextStyle(
Icons.play_circle_outline_rounded, fontSize: 13,
size: 16,
color: theme.colorScheme.secondary, 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(), label: Obx(
SizedBox( () => Text(
height: 35, widget.type == ContributeType.video
child: TextButton.icon( ? _controller.order.value == 'pubdate'
onPressed: _controller.queryBySort, ? '最新发布'
icon: Icon( : '最多播放'
Icons.sort, : _controller.sort.value == 'desc'
size: 16, ? '默认'
: '倒序',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary, 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), ),
], ],
),
), ),
), ),
), ),

View File

@@ -1,11 +1,12 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/custom_icon.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/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/flutter/selectable_text/selectable_text.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.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/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/http/loading_state.dart';
import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models/common/pgc_review_type.dart'; import 'package:PiliPlus/models/common/pgc_review_type.dart';
@@ -383,51 +384,43 @@ class _PgcReviewChildPageState extends State<PgcReviewChildPage>
); );
} }
Widget _buildHeader(ThemeData theme) => SliverPersistentHeader( Widget _buildHeader(ThemeData theme) => SliverFloatingHeaderWidget(
pinned: false, backgroundColor: theme.colorScheme.surface,
floating: true, child: Padding(
delegate: CustomSliverPersistentHeaderDelegate( padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5),
extent: 40, child: Row(
bgColor: theme.colorScheme.surface, mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Container( children: [
height: 40, Obx(
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), () {
child: Row( final count = _controller.count.value;
mainAxisAlignment: MainAxisAlignment.spaceBetween, return count == null
children: [ ? const SizedBox.shrink()
Obx( : Text(
() { '${NumUtils.numFormat(count)}条点评',
final count = _controller.count.value; style: const TextStyle(fontSize: 13),
return count == null );
? const SizedBox.shrink() },
: Text( ),
'${NumUtils.numFormat(count)}条点评', TextButton.icon(
style: const TextStyle(fontSize: 13), style: StyleString.buttonStyle,
); onPressed: _controller.queryBySort,
}, icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
), ),
SizedBox( label: Obx(
height: 35, () => Text(
child: TextButton.icon( _controller.sortType.value.label,
onPressed: _controller.queryBySort, style: TextStyle(
icon: Icon( fontSize: 13,
Icons.sort,
size: 16,
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
), ),
label: Obx(
() => Text(
_controller.sortType.value.label,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
), ),
), ),
], ),
), ],
), ),
), ),
); );

View File

@@ -1,9 +1,9 @@
import 'dart:math'; 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/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.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/video_card/video_card_h.dart';
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
@@ -201,17 +201,11 @@ class _PopularSeriesPageState extends State<PopularSeriesPage> with GridMixin {
], ],
); );
} }
final height = MediaQuery.textScalerOf(context).scale(27); return SliverFloatingHeaderWidget(
return SliverPersistentHeader( backgroundColor: colorScheme.surface,
floating: true, child: Padding(
delegate: CustomSliverPersistentHeaderDelegate( padding: const .only(left: 14, bottom: 7),
extent: height, child: child,
child: Container(
height: height,
padding: const EdgeInsets.only(left: 14, bottom: 7),
child: child,
),
bgColor: colorScheme.surface,
), ),
); );
} }

View File

@@ -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/models/search/result.dart';
import 'package:PiliPlus/pages/search_panel/article/controller.dart'; import 'package:PiliPlus/pages/search_panel/article/controller.dart';
import 'package:PiliPlus/pages/search_panel/article/widgets/item.dart'; import 'package:PiliPlus/pages/search_panel/article/widgets/item.dart';
@@ -45,51 +45,45 @@ class _SearchArticlePanelState
@override @override
Widget buildHeader(ThemeData theme) { Widget buildHeader(ThemeData theme) {
return SliverPersistentHeader( return SliverFloatingHeaderWidget(
pinned: false, backgroundColor: theme.colorScheme.surface,
floating: true, child: Padding(
delegate: CustomSliverPersistentHeaderDelegate( padding: const .fromLTRB(25, 0, 12, 4),
extent: 40, child: Row(
bgColor: theme.colorScheme.surface, children: [
child: Container( Obx(
height: 40, () => Text(
padding: const EdgeInsets.only(left: 25, right: 12), '排序: ${controller.articleOrderType.value.label}',
child: Row( maxLines: 1,
children: [ style: TextStyle(color: theme.colorScheme.outline),
Obx( ),
() => Text( ),
'排序: ${controller.articleOrderType.value.label}', const Spacer(),
maxLines: 1, Obx(
style: TextStyle(color: theme.colorScheme.outline), () => 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,
),
),
),
],
),
), ),
), ),
); );

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; 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/models/search/result.dart';
import 'package:PiliPlus/pages/search_panel/user/controller.dart'; import 'package:PiliPlus/pages/search_panel/user/controller.dart';
import 'package:PiliPlus/pages/search_panel/user/widgets/item.dart'; import 'package:PiliPlus/pages/search_panel/user/widgets/item.dart';
@@ -46,51 +46,45 @@ class _SearchUserPanelState
@override @override
Widget buildHeader(ThemeData theme) { Widget buildHeader(ThemeData theme) {
return SliverPersistentHeader( return SliverFloatingHeaderWidget(
pinned: false, backgroundColor: theme.colorScheme.surface,
floating: true, child: Padding(
delegate: CustomSliverPersistentHeaderDelegate( padding: const .fromLTRB(25, 0, 12, 4),
extent: 40, child: Row(
bgColor: theme.colorScheme.surface, children: [
child: Container( Obx(
height: 40, () => Text(
padding: const EdgeInsets.only(left: 25, right: 12), '排序: ${controller.userOrderType!.value.label}',
child: Row( maxLines: 1,
children: [ style: TextStyle(color: theme.colorScheme.outline),
Obx( ),
() => Text( ),
'排序: ${controller.userOrderType!.value.label}', const Spacer(),
maxLines: 1, Obx(
style: TextStyle(color: theme.colorScheme.outline), () => 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,
),
),
),
],
),
), ),
), ),
); );

View File

@@ -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/common/widgets/video_card/video_card_h.dart';
import 'package:PiliPlus/models/common/search/video_search_type.dart'; import 'package:PiliPlus/models/common/search/video_search_type.dart';
import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/models/search/result.dart';
@@ -47,61 +47,55 @@ class _SearchVideoPanelState
@override @override
Widget buildHeader(ThemeData theme) { Widget buildHeader(ThemeData theme) {
return SliverPersistentHeader( return SliverFloatingHeaderWidget(
pinned: false, backgroundColor: theme.colorScheme.surface,
floating: true, child: Padding(
delegate: CustomSliverPersistentHeaderDelegate( padding: const .fromLTRB(12, 0, 12, 4),
extent: 34, child: Row(
bgColor: theme.colorScheme.surface, children: [
child: Container( Expanded(
height: 34, child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 12), scrollDirection: Axis.horizontal,
child: Row( child: Wrap(
children: [ children: [
Expanded( for (final e in ArchiveFilterType.values)
child: SingleChildScrollView( Obx(
scrollDirection: Axis.horizontal, () => SearchText(
child: Wrap( fontSize: 13,
children: [ text: e.desc,
for (final e in ArchiveFilterType.values) bgColor: Colors.transparent,
Obx( textColor: controller.selectedType.value == e
() => SearchText( ? theme.colorScheme.primary
fontSize: 13, : theme.colorScheme.outline,
text: e.desc, onTap: (_) => controller
bgColor: Colors.transparent, ..order = e.name
textColor: controller.selectedType.value == e ..selectedType.value = e
? theme.colorScheme.primary ..onSortSearch(getBack: false),
: theme.colorScheme.outline,
onTap: (_) => controller
..order = e.name
..selectedType.value = e
..onSortSearch(getBack: false),
),
), ),
], ),
), ],
), ),
), ),
const VerticalDivider(indent: 7, endIndent: 8), ),
const SizedBox(width: 3), const VerticalDivider(indent: 7, endIndent: 8),
SizedBox( const SizedBox(width: 3),
width: 32, SizedBox(
height: 32, width: 32,
child: IconButton( height: 32,
tooltip: '筛选', child: IconButton(
style: const ButtonStyle( tooltip: '筛选',
padding: WidgetStatePropertyAll(EdgeInsets.zero), style: const ButtonStyle(
), padding: WidgetStatePropertyAll(EdgeInsets.zero),
onPressed: () => controller.onShowFilterDialog(context), ),
icon: Icon( onPressed: () => controller.onShowFilterDialog(context),
Icons.filter_list_outlined, icon: Icon(
size: 18, Icons.filter_list_outlined,
color: theme.colorScheme.primary, size: 18,
), color: theme.colorScheme.primary,
), ),
), ),
], ),
), ],
), ),
), ),
); );

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/image/network_img_layer.dart';
@@ -136,24 +137,22 @@ class _HorizontalMemberPageState extends State<HorizontalMemberPage> {
); );
}, },
), ),
SizedBox( TextButton.icon(
height: 35, style: StyleString.buttonStyle,
child: TextButton.icon( onPressed: () => _controller
onPressed: () => _controller ..lastAid = widget.videoDetailController.aid.toString()
..lastAid = widget.videoDetailController.aid.toString() ..queryBySort(),
..queryBySort(), icon: Icon(
icon: Icon( Icons.sort,
Icons.sort, size: 16,
size: 16, color: theme.colorScheme.secondary,
color: theme.colorScheme.secondary, ),
), label: Obx(
label: Obx( () => Text(
() => Text( _controller.order.value == 'pubdate' ? '最新发布' : '最多播放',
_controller.order.value == 'pubdate' ? '最新发布' : '最多播放', style: TextStyle(
style: TextStyle( fontSize: 13,
fontSize: 13, color: theme.colorScheme.secondary,
color: theme.colorScheme.secondary,
),
), ),
), ),
), ),

View File

@@ -1,7 +1,8 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.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/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
@@ -83,47 +84,39 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
: _videoReplyController.scrollController, : _videoReplyController.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
key: const PageStorageKey(_VideoReplyPanelState), key: const PageStorageKey(_VideoReplyPanelState),
slivers: <Widget>[ slivers: [
SliverPersistentHeader( SliverFloatingHeaderWidget(
pinned: false, backgroundColor: theme.colorScheme.surface,
floating: true, child: Padding(
delegate: CustomSliverPersistentHeaderDelegate( padding: const .fromLTRB(12, 2.5, 6, 2.5),
extent: 40, child: Row(
bgColor: theme.colorScheme.surface, mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Container( children: [
height: 40, Obx(
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), () => Text(
child: Row( _videoReplyController.sortType.value.title,
mainAxisAlignment: MainAxisAlignment.spaceBetween, style: const TextStyle(fontSize: 13),
children: [
Obx(
() => Text(
_videoReplyController.sortType.value.title,
style: const TextStyle(fontSize: 13),
),
), ),
SizedBox( ),
height: 35, TextButton.icon(
child: TextButton.icon( style: StyleString.buttonStyle,
onPressed: _videoReplyController.queryBySort, onPressed: _videoReplyController.queryBySort,
icon: Icon( icon: Icon(
Icons.sort, Icons.sort,
size: 16, size: 16,
color: theme.colorScheme.secondary,
),
label: Obx(
() => Text(
_videoReplyController.sortType.value.label,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
), ),
label: Obx(
() => Text(
_videoReplyController.sortType.value.label,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
), ),
), ),
], ),
), ],
), ),
), ),
), ),

View File

@@ -1,8 +1,9 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/colored_box_transition.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/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo, Mode; show ReplyInfo, Mode;
@@ -240,52 +241,43 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
} }
Widget _sortWidget(ThemeData theme) { Widget _sortWidget(ThemeData theme) {
return SliverPersistentHeader( return SliverPinnedHeader(
pinned: true, backgroundColor: theme.colorScheme.surface,
delegate: CustomSliverPersistentHeaderDelegate( child: Padding(
extent: 40, padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5),
bgColor: theme.colorScheme.surface, child: Row(
child: Container( mainAxisAlignment: MainAxisAlignment.spaceBetween,
height: 40, children: [
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), Obx(
child: Row( () {
mainAxisAlignment: MainAxisAlignment.spaceBetween, final count = _controller.count.value;
children: [ return count != -1
Obx( ? Text(
() { '相关回复共${NumUtils.numFormat(count)}',
final count = _controller.count.value; style: const TextStyle(fontSize: 13),
return count != -1 )
? Text( : const SizedBox.shrink();
'相关回复共${NumUtils.numFormat(count)}', },
style: const TextStyle(fontSize: 13), ),
) TextButton.icon(
: const SizedBox.shrink(); style: StyleString.buttonStyle,
}, onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
), ),
SizedBox( label: Obx(
height: 35, () => Text(
child: TextButton.icon( _controller.mode.value == Mode.MAIN_LIST_HOT ? '按热度' : '按时间',
onPressed: _controller.queryBySort, style: TextStyle(
icon: Icon( fontSize: 13,
Icons.sort,
size: 16,
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
), ),
label: Obx(
() => Text(
_controller.mode.value == Mode.MAIN_LIST_HOT
? '按热度'
: '按时间',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
), ),
), ),
], ),
), ],
), ),
), ),
); );

View File

@@ -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/image_viewer/hero_dialog_route.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.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/http/loading_state.dart';
import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/main.dart';
import 'package:PiliPlus/models/common/episode_panel_type.dart'; import 'package:PiliPlus/models/common/episode_panel_type.dart';
@@ -648,14 +649,10 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
? animHeight ? animHeight
: videoDetailController.videoHeight; : videoDetailController.videoHeight;
return [ return [
SliverAppBar( SliverPinnedDynamicHeader(
elevation: 0, minExtent: kToolbarHeight,
scrolledUnderElevation: 0, maxExtent: height,
primary: false, child: Stack(
automaticallyImplyLeading: false,
pinned: true,
expandedHeight: height,
flexibleSpace: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
SizedBox( SizedBox(

View File

@@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:io' show File; 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/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/flutter/text_field/text_field.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';