Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 59 additions & 39 deletions lib/features/earthquake/presentation/pages/report_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,10 @@ class _ReportSheetState extends State<_ReportSheet> {
final DraggableScrollableController _dragController =
DraggableScrollableController();

/// [_onNotification]
ScrollController? _scrollController;
bool _wasExpanded = false;

@override
void dispose() {
_extent.dispose();
Expand All @@ -437,11 +441,12 @@ class _ReportSheetState extends State<_ReportSheet> {
child: NotificationListener<DraggableScrollableNotification>(
onNotification: (notification) {
_extent.value = notification.extent;
// Runs from the scroll-notification dispatch, not mid-build, so
// it's safe to update the page-level notifier the floating back
// button listens to.
widget.expandedNotifier.value =
notification.extent >= _ReportSheet._switchThreshold;
final expanded = notification.extent >= _ReportSheet._switchThreshold;
widget.expandedNotifier.value = expanded;
if (_wasExpanded && !expanded) {
_scrollController?.jumpTo(0);
}
_wasExpanded = expanded;
Comment on lines +446 to +449

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[other · medium]
_scrollController 是在 DraggableScrollableSheetbuilder 執行時才被賦值,但在 onNotification 回調中會被使用。這存在生命週期上的風險:如果在 builder 尚未執行、或 Widget 正在銷毀/重建的階段,調用 _scrollController?.jumpTo(0),可能會因為 scrollController 為 null 或處於不穩定狀態而引發異常,或與 DraggableScrollableSheet 的內部滾動邏輯產生衝突。建議在調用前確保 _scrollController 已正確初始化且可控。

return false;
},
child: DraggableScrollableSheet(
Expand All @@ -451,6 +456,7 @@ class _ReportSheetState extends State<_ReportSheet> {
maxChildSize: _ReportSheet._expanded,
snap: true,
builder: (context, scrollController) {
_scrollController = scrollController;
return DecoratedBox(
decoration: BoxDecoration(
color: colors.surface,
Expand All @@ -469,17 +475,14 @@ class _ReportSheetState extends State<_ReportSheet> {
valueListenable: _extent,
builder: (context, extent, _) {
final expanded = extent >= _ReportSheet._switchThreshold;
// Flush with the screen top at full extent — the status
// bar/notch inset has to come back as padding on the
// expanded header, or it renders underneath it.
final atTop = extent >= _ReportSheet._expanded - 0.02;
final revealProgress =
((extent - _ReportSheet.peek) /
(_ReportSheet._expanded - _ReportSheet.peek))
.clamp(0.0, 1.0);
Comment on lines +479 to +482

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[other · low]
revealProgress 的計算公式 ((extent - _ReportSheet.peek) / (_ReportSheet._expanded - _ReportSheet.peek)) 若分母為零(即 _expanded 等於 peek),會導致除以零的數學錯誤。雖然目前常量定義中 _expanded (1.0) 與 peek (0.32) 不相等,但為了代碼的健壯性,建議增加分母檢查或使用更安全的計算方式。

Suggestion:

Suggested change
final revealProgress =
((extent - _ReportSheet.peek) /
(_ReportSheet._expanded - _ReportSheet.peek))
.clamp(0.0, 1.0);
final revealProgress = (
(_ReportSheet._expanded - _ReportSheet.peek) > 0
? (extent - _ReportSheet.peek) / (_ReportSheet._expanded - _ReportSheet.peek)
: 0.0
).clamp(0.0, 1.0);

final peekOpacity = 1 - revealProgress;
return Column(
children: [
// Pinned above the scroll body, not a scrolling item:
// the "地震報告" bar (back button + title) stays while
// the 詳細資訊 content scrolls underneath it. Keep the
// ListView's horizontal inset on the header too, so it
// sits where it did when it was the list's first child.
if (expanded)
Padding(
padding: const EdgeInsets.symmetric(
Expand All @@ -500,9 +503,28 @@ class _ReportSheetState extends State<_ReportSheet> {
AppSpacing.xl +
MediaQuery.paddingOf(context).bottom,
),
children: expanded
? _expandedContent(context, report)
: _peekContent(context, report),
children: [
Stack(
alignment: Alignment.topCenter,
children: [
Opacity(
opacity: revealProgress,
child: IgnorePointer(
ignoring: !expanded,
child: _expandedContent(context, report),
),
),
if (peekOpacity > 0)
Opacity(
opacity: peekOpacity,
child: IgnorePointer(
ignoring: expanded,
child: _peekContent(context, report),
),
),
],
),
Comment on lines +507 to +526

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[other · medium]
Stack 中同時渲染 _expandedContent_peekContent 並進行透明度過渡,若內容包含複雜組件(例如 _ReportImageCard 中的圖片),在拖動過程中可能會因為同時對兩個複雜的 Widget Tree 進行重繪與透明度計算,而造成高負載,導致動畫掉幀(jank)。建議評估組件複雜度,或考慮是否能優化渲染路徑。

],
),
),
],
Expand All @@ -517,30 +539,27 @@ class _ReportSheetState extends State<_ReportSheet> {
);
}

List<Widget> _peekContent(BuildContext context, EarthquakeReport report) => [
_ReportPeekSummary(report: report),
];
Widget _peekContent(BuildContext context, EarthquakeReport report) =>
_ReportPeekSummary(report: report);

List<Widget> _expandedContent(BuildContext context, EarthquakeReport report) {
Widget _expandedContent(BuildContext context, EarthquakeReport report) {
final l10n = AppLocalizations.of(context);
return [
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ReportHeader(report: report),
const SizedBox(height: AppSpacing.lg),
SectionHeader(l10n.reportDetailInfo),
_ReportInfoCard(report: report),
_LocalIntensitySection(report: report),
if (report.list.isNotEmpty) _AreaIntensitySection(report: report),
SectionHeader(l10n.reportDetailImage),
_ReportImageCard(report: report),
],
),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ReportHeader(report: report),
const SizedBox(height: AppSpacing.lg),
SectionHeader(l10n.reportDetailInfo),
_ReportInfoCard(report: report),
_LocalIntensitySection(report: report),
if (report.list.isNotEmpty) _AreaIntensitySection(report: report),
SectionHeader(l10n.reportDetailImage),
_ReportImageCard(report: report),
],
),
];
);
}
}

Expand Down Expand Up @@ -698,15 +717,16 @@ class _PeekStat extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
value,
style: theme.textTheme.bodyLarge?.copyWith(
Expand Down
4 changes: 2 additions & 2 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -850,10 +850,10 @@ packages:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
source: hosted
version: "6.5.0"
version: "6.5.2"
provider:
dependency: "direct main"
description:
Expand Down
Loading