mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-09-09 19:28:56 +02:00
fix a rare crash around blurring in newer versions of flutter - bit of a workaround
This commit is contained in:
@@ -594,10 +594,13 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
|
||||
],
|
||||
),
|
||||
|
||||
// Tutorial overlay - show only if tutorial should be shown and we're done checking
|
||||
if (!_isCheckingTutorial && _showTutorial)
|
||||
// Tutorial overlay - always mounted once the initial check
|
||||
// completes (never conditionally inserted/removed - see
|
||||
// PositioningTutorialOverlay's class doc for why), fades via
|
||||
// its own `visible` flag instead.
|
||||
if (!_isCheckingTutorial)
|
||||
Positioned.fill(
|
||||
child: PositioningTutorialOverlay(),
|
||||
child: PositioningTutorialOverlay(visible: _showTutorial),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -627,10 +627,13 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
],
|
||||
),
|
||||
|
||||
// Tutorial overlay - show only if tutorial should be shown and we're done checking
|
||||
if (!_isCheckingTutorial && _showTutorial)
|
||||
// Tutorial overlay - always mounted once the initial check
|
||||
// completes (never conditionally inserted/removed - see
|
||||
// PositioningTutorialOverlay's class doc for why), fades via
|
||||
// its own `visible` flag instead.
|
||||
if (!_isCheckingTutorial)
|
||||
Positioned.fill(
|
||||
child: PositioningTutorialOverlay(),
|
||||
child: PositioningTutorialOverlay(visible: _showTutorial),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -4,89 +4,164 @@ import 'package:flutter/material.dart';
|
||||
import '../dev_config.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
/// Overlay that appears over add/edit node sheets to guide users through
|
||||
/// Overlay that appears over add/edit node sheets to guide users through
|
||||
/// the positioning tutorial. Shows a blurred background with tutorial text.
|
||||
class PositioningTutorialOverlay extends StatelessWidget {
|
||||
///
|
||||
/// Stability note: on Android's Impeller (Vulkan) renderer, BackdropFilter
|
||||
/// snapshots the render pass's "back texture" mid-frame. If that snapshot
|
||||
/// races against the surface being invalidated - which happens constantly
|
||||
/// right where this overlay lives, since the enclosing bottom sheet resizes
|
||||
/// on open/keyboard/rotation - Impeller can hit a null texture and hard-abort
|
||||
/// the whole process natively (not a catchable Dart error). Two things keep
|
||||
/// that race from being triggered here:
|
||||
/// - [RepaintBoundary] isolates the filter onto its own stable composited
|
||||
/// layer instead of sharing one with the actively-resizing sheet.
|
||||
/// - The widget is always kept mounted by the parent sheet and toggles
|
||||
/// visibility via a fade (see [visible]) rather than being abruptly
|
||||
/// inserted/removed from the tree, which is believed to be the other half
|
||||
/// of what triggers the crash.
|
||||
class PositioningTutorialOverlay extends StatefulWidget {
|
||||
const PositioningTutorialOverlay({
|
||||
super.key,
|
||||
required this.visible,
|
||||
this.onFadeOutComplete,
|
||||
});
|
||||
|
||||
/// Called when the fade-out animation completes (if animated)
|
||||
/// Whether the tutorial should currently be showing. Toggling this fades
|
||||
/// the overlay in/out - see class doc for why it's never removed from
|
||||
/// the tree instead.
|
||||
final bool visible;
|
||||
|
||||
/// Called when the fade-out animation completes (i.e. the overlay has
|
||||
/// become fully transparent after [visible] turned false).
|
||||
final VoidCallback? onFadeOutComplete;
|
||||
|
||||
@override
|
||||
State<PositioningTutorialOverlay> createState() => _PositioningTutorialOverlayState();
|
||||
}
|
||||
|
||||
class _PositioningTutorialOverlayState extends State<PositioningTutorialOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
value: widget.visible ? 1.0 : 0.0,
|
||||
);
|
||||
_controller.addStatusListener(_onStatusChanged);
|
||||
}
|
||||
|
||||
void _onStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.dismissed) {
|
||||
widget.onFadeOutComplete?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PositioningTutorialOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.visible != oldWidget.visible) {
|
||||
if (widget.visible) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeStatusListener(_onStatusChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: LocalizationService.instance,
|
||||
builder: (context, child) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: kPositioningTutorialBlurSigma,
|
||||
sigmaY: kPositioningTutorialBlurSigma,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.3), // Semi-transparent overlay
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Tutorial icon
|
||||
Icon(
|
||||
Icons.pan_tool_outlined,
|
||||
size: 48,
|
||||
color: Colors.white,
|
||||
// Ignore touches when faded out so the invisible-but-still-mounted
|
||||
// overlay doesn't block interaction with the sheet underneath.
|
||||
return IgnorePointer(
|
||||
ignoring: !widget.visible,
|
||||
child: FadeTransition(
|
||||
opacity: _controller,
|
||||
child: AnimatedBuilder(
|
||||
animation: LocalizationService.instance,
|
||||
builder: (context, child) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return ClipRect(
|
||||
child: RepaintBoundary(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: kPositioningTutorialBlurSigma,
|
||||
sigmaY: kPositioningTutorialBlurSigma,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.3), // Semi-transparent overlay
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Tutorial icon
|
||||
Icon(
|
||||
Icons.pan_tool_outlined,
|
||||
size: 48,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tutorial title
|
||||
Text(
|
||||
locService.t('positioningTutorial.title'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Tutorial instructions
|
||||
Text(
|
||||
locService.t('positioningTutorial.instructions'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Additional hint
|
||||
Text(
|
||||
locService.t('positioningTutorial.hint'),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: 14,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tutorial title
|
||||
Text(
|
||||
locService.t('positioningTutorial.title'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Tutorial instructions
|
||||
Text(
|
||||
locService.t('positioningTutorial.instructions'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Additional hint
|
||||
Text(
|
||||
locService.t('positioningTutorial.hint'),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: 14,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user