Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-07 11:54:28 +08:00
parent cae5befa63
commit 1946218df3
21 changed files with 446 additions and 334 deletions

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
class ExpandablePanel extends StatelessWidget {
final bool expand;
final Widget collapsed;
final Widget expanded;
const ExpandablePanel({
super.key,
required this.expand,
required this.collapsed,
required this.expanded,
});
@override
Widget build(BuildContext context) {
return _AnimatedCross(
alignment: .topLeft,
firstChild: collapsed,
secondChild: expanded,
sizeCurve: Curves.linear,
crossFadeState: expand ? .showSecond : .showFirst,
duration: const Duration(milliseconds: 300),
);
}
}
/// ref [AnimatedCrossFade]
class _AnimatedCross extends StatefulWidget {
const _AnimatedCross({
required this.firstChild,
required this.secondChild,
this.sizeCurve = Curves.linear,
this.alignment = Alignment.topCenter,
required this.crossFadeState,
required this.duration,
});
final Widget firstChild;
final Widget secondChild;
final CrossFadeState crossFadeState;
final Duration duration;
final Curve sizeCurve;
final AlignmentGeometry alignment;
@override
State<_AnimatedCross> createState() => _AnimatedCrossState();
}
class _AnimatedCrossState extends State<_AnimatedCross> {
late bool _showFirst;
AnimationStatus? _status;
@override
void initState() {
super.initState();
switch (widget.crossFadeState) {
case .showFirst:
_showFirst = true;
case .showSecond:
_showFirst = false;
}
}
@override
void didUpdateWidget(_AnimatedCross oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.crossFadeState != oldWidget.crossFadeState) {
switch (widget.crossFadeState) {
case .showFirst:
_status = .reverse;
case .showSecond:
_status = .forward;
}
_showFirst = false;
}
}
void _onEnd() {
if (_status == .reverse) {
_showFirst = true;
}
setState(() {});
}
Widget get firstChild => _widgetBuilder(_showFirst, widget.firstChild);
Widget get secondChild => _widgetBuilder(!_showFirst, widget.secondChild);
static Widget _widgetBuilder(bool visible, Widget child) =>
Opacity(opacity: visible ? 1.0 : 0.0, child: child);
@override
Widget build(BuildContext context) {
const Key kFirstChildKey = ValueKey<CrossFadeState>(.showFirst);
const Key kSecondChildKey = ValueKey<CrossFadeState>(.showSecond);
final Key topKey;
Widget topChild;
final Key bottomKey;
Widget bottomChild;
switch (widget.crossFadeState) {
case .showFirst:
topKey = kFirstChildKey;
topChild = firstChild;
bottomKey = kSecondChildKey;
bottomChild = secondChild;
case .showSecond:
topKey = kSecondChildKey;
topChild = secondChild;
bottomKey = kFirstChildKey;
bottomChild = firstChild;
}
return ClipRect(
child: AnimatedSize(
clipBehavior: .none,
alignment: widget.alignment,
duration: widget.duration,
curve: widget.sizeCurve,
onEnd: _onEnd,
child: AnimatedCrossFade.defaultLayoutBuilder(
topChild,
topKey,
bottomChild,
bottomKey,
),
),
);
}
}

View File

@@ -430,49 +430,51 @@ class _PageViewState<T extends HorizontalDragGestureRecognizer>
widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
widget.onPageChanged != null &&
notification is ScrollUpdateNotification) {
final metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
final child = Scrollable<T>(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: _controller,
physics: physics,
restorationId: widget.restorationId,
hitTestBehavior: widget.hitTestBehavior,
scrollBehavior:
widget.scrollBehavior ??
ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
scrollCacheExtent: widget.scrollCacheExtent,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
allowImplicitScrolling: widget.allowImplicitScrolling,
),
],
);
},
child: Scrollable<T>(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: _controller,
physics: physics,
restorationId: widget.restorationId,
hitTestBehavior: widget.hitTestBehavior,
scrollBehavior:
widget.scrollBehavior ??
ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
scrollCacheExtent: widget.scrollCacheExtent,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
allowImplicitScrolling: widget.allowImplicitScrolling,
),
],
);
},
horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
),
horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
);
if (widget.onPageChanged != null) {
return NotificationListener<ScrollEndNotification>(
onNotification: (notification) {
if (notification.depth == 0) {
final metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: child,
);
}
return child;
}
@override

View File

@@ -78,7 +78,7 @@ class RenderSliverToBoxWithVisibilityAdapter extends RenderSliverToBoxAdapter {
if (_visible != visible) {
_visible = visible;
WidgetsBinding.instance.addPostFrameCallback(
(_) => onVisibilityChanged(visible),
(_) => onVisibilityChanged(!visible),
);
}
}