show member collection top

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-03-10 18:21:36 +08:00
parent 9fef3284db
commit b8098fe067
9 changed files with 446 additions and 77 deletions

View File

@@ -1,15 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show RenderProxyBox;
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
class CustomHeightWidget extends SingleChildRenderObjectWidget {
const CustomHeightWidget({
super.key,
required this.height,
this.height,
this.offset = .zero,
required super.child,
required Widget super.child,
});
final double height;
final double? height;
final Offset offset;
@@ -34,14 +34,14 @@ class CustomHeightWidget extends SingleChildRenderObjectWidget {
class RenderCustomHeightWidget extends RenderProxyBox {
RenderCustomHeightWidget({
required double height,
double? height,
required Offset offset,
}) : _height = height,
_offset = offset;
double _height;
double get height => _height;
set height(double value) {
double? _height;
double? get height => _height;
set height(double? value) {
if (_height == value) return;
_height = value;
markNeedsLayout();
@@ -57,12 +57,40 @@ class RenderCustomHeightWidget extends RenderProxyBox {
@override
void performLayout() {
child!.layout(constraints);
size = constraints.constrainDimensions(constraints.maxWidth, height);
if (height != null) {
child!.layout(constraints.copyWith(maxHeight: .infinity));
size = constraints.constrainDimensions(constraints.maxWidth, height!);
} else {
child!.layout(
constraints.copyWith(maxHeight: .infinity),
parentUsesSize: true,
);
size = constraints.constrainDimensions(
constraints.maxWidth,
child!.size.height,
);
}
}
@override
void paint(PaintingContext context, Offset offset) {
context.paintChild(child!, offset + _offset);
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
return result.addWithPaintOffset(
offset: _offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - _offset);
return child!.hitTest(result, position: transformed);
},
);
}
@override
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
transform.translateByDouble(_offset.dx, _offset.dy, 0.0, 1.0);
}
}

View File

@@ -17,6 +17,7 @@
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/custom_height_widget.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/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'
@@ -24,6 +25,7 @@ import 'package:PiliPlus/common/widgets/only_layout_widget.dart'
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
import 'package:flutter/rendering.dart' show RenderOpacity, OpacityLayer;
import 'package:flutter/services.dart';
/// ref [SliverAppBar]
@@ -133,12 +135,10 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
title: effectiveTitle,
actions: actions,
automaticallyImplyActions: automaticallyImplyActions,
flexibleSpace: maxExtent == .infinity
? flexibleSpace
: IgnorePointer(
ignoring: isScrolledUnder,
child: FlexibleSpaceBar(background: flexibleSpace),
),
flexibleSpace: IgnorePointer(
ignoring: isScrolledUnder,
child: DynamicFlexibleSpaceBar(background: flexibleSpace),
),
bottom: bottom,
elevation: isScrolledUnder ? elevation : 0.0,
scrolledUnderElevation: scrolledUnderElevation,
@@ -350,3 +350,148 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
);
}
}
/// ref [FlexibleSpaceBar]
class DynamicFlexibleSpaceBar extends StatefulWidget {
const DynamicFlexibleSpaceBar({
super.key,
required this.background,
this.collapseMode = CollapseMode.parallax,
});
final Widget background;
final CollapseMode collapseMode;
@override
State<DynamicFlexibleSpaceBar> createState() =>
_DynamicFlexibleSpaceBarState();
}
class _DynamicFlexibleSpaceBarState extends State<DynamicFlexibleSpaceBar> {
double _getCollapsePadding(double t, FlexibleSpaceBarSettings settings) {
switch (widget.collapseMode) {
case CollapseMode.pin:
return -(settings.maxExtent - settings.currentExtent);
case CollapseMode.none:
return 0.0;
case CollapseMode.parallax:
final double deltaExtent = settings.maxExtent - settings.minExtent;
return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);
}
}
@override
Widget build(BuildContext context) {
final FlexibleSpaceBarSettings settings = context
.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
double? height;
final double opacity;
final double topPadding;
if (settings.maxExtent == .infinity) {
opacity = 1.0;
topPadding = 0.0;
} else {
final double deltaExtent = settings.maxExtent - settings.minExtent;
// 0.0 -> Expanded
// 1.0 -> Collapsed to toolbar
final double t = clampDouble(
1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent,
0.0,
1.0,
);
final double fadeStart = math.max(
0.0,
1.0 - kToolbarHeight / deltaExtent,
);
const fadeEnd = 1.0;
assert(fadeStart <= fadeEnd);
// If the min and max extent are the same, the app bar cannot collapse
// and the content should be visible, so opacity = 1.
opacity = settings.maxExtent == settings.minExtent
? 1.0
: 1.0 - Interval(fadeStart, fadeEnd).transform(t);
topPadding = _getCollapsePadding(t, settings);
}
return ClipRect(
child: CustomHeightWidget(
height: height,
offset: Offset(0.0, topPadding),
child: _FlexibleSpaceHeaderOpacity(
// IOS is relying on this semantics node to correctly traverse
// through the app bar when it is collapsed.
alwaysIncludeSemantics: true,
opacity: opacity,
child: widget.background,
),
),
);
}
}
/// [_FlexibleSpaceHeaderOpacity]
class _FlexibleSpaceHeaderOpacity extends SingleChildRenderObjectWidget {
const _FlexibleSpaceHeaderOpacity({
required this.opacity,
required super.child,
required this.alwaysIncludeSemantics,
});
final double opacity;
final bool alwaysIncludeSemantics;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderFlexibleSpaceHeaderOpacity(
opacity: opacity,
alwaysIncludeSemantics: alwaysIncludeSemantics,
);
}
@override
void updateRenderObject(
BuildContext context,
covariant _RenderFlexibleSpaceHeaderOpacity renderObject,
) {
renderObject
..alwaysIncludeSemantics = alwaysIncludeSemantics
..opacity = opacity;
}
}
class _RenderFlexibleSpaceHeaderOpacity extends RenderOpacity {
_RenderFlexibleSpaceHeaderOpacity({
super.opacity,
super.alwaysIncludeSemantics,
});
@override
bool get isRepaintBoundary => false;
@override
void paint(PaintingContext context, Offset offset) {
if (child == null) {
return;
}
if ((opacity * 255).roundToDouble() <= 0) {
layer = null;
return;
}
assert(needsCompositing);
layer = context.pushOpacity(
offset,
(opacity * 255).round(),
super.paint,
oldLayer: layer as OpacityLayer?,
);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
}
}

View File

@@ -54,6 +54,7 @@ class GalleryViewer extends StatefulWidget {
required this.quality,
required this.sources,
this.initIndex = 0,
this.onPageChanged,
});
final double minScale;
@@ -61,6 +62,7 @@ class GalleryViewer extends StatefulWidget {
final int quality;
final List<SourceModel> sources;
final int initIndex;
final ValueChanged<int>? onPageChanged;
@override
State<GalleryViewer> createState() => _GalleryViewerState();
@@ -346,6 +348,7 @@ class _GalleryViewerState extends State<GalleryViewer>
_player?.pause();
_playIfNeeded(widget.sources[index]);
_currIndex.value = index;
widget.onPageChanged?.call(index);
}
late final ValueChanged<int>? _onChangePage = widget.sources.length == 1

View File

@@ -15,11 +15,13 @@ class PendantAvatar extends StatelessWidget {
final String? garbPendantImage;
final int? roomId;
final VoidCallback? onTap;
final bool isMemberAvatar;
const PendantAvatar({
super.key,
required this.avatar,
this.size = 80,
required this.size,
this.isMemberAvatar = false,
double? badgeSize,
bool isVip = false,
int? officialType,
@@ -42,13 +44,12 @@ class PendantAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isMemberAvatar = size == 80;
Widget? pendant;
if (showDynDecorate && !garbPendantImage.isNullOrEmpty) {
final pendantSize = size * 1.75;
pendant = Positioned(
// -(size * 1.75 - size) / 2
top: -0.375 * size + (size == 80 ? 2 : 0),
top: -0.375 * size + (isMemberAvatar ? 2 : 0),
child: IgnorePointer(
child: NetworkImgLayer(
type: .emote,