insets: explicit-intent setSystemUiMode replaces always-on lerp
Animating every metric change baked in a boot race: XInsets._() ran
during App.production() (before runApp), and on Android cold starts
the activity hadn't been laid out yet — view.viewPadding read zero.
The next didChangeMetrics then lerped 0 → real over 220ms, leaving
the safe-area collapsed for the first ~13 frames. Visible as content
under the status bar + home indicator on launch.
- didChangeMetrics now snaps by default. _from / _start / _ticking
state only spins up when a caller explicitly opted in.
- XInsets.setSystemUiMode(mode, {animate}) wraps
SystemChrome.setEnabledSystemUIMode. The opt-in flag and the
trigger live together — no caller-side ordering risk, no
one-shot side-channel callers can misuse.
- Removed the public `duration` knob (tests no longer need to pin
it; nothing animates without an explicit setSystemUiMode call).
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui' show FlutterView;
|
||||
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Smoothly-animated view padding (safe-area insets).
|
||||
@@ -34,14 +35,26 @@ class XInsets with ChangeNotifier, WidgetsBindingObserver {
|
||||
EdgeInsets _from = EdgeInsets.zero;
|
||||
Duration _start = Duration.zero;
|
||||
bool _ticking = false;
|
||||
|
||||
/// Lerp duration. Set to [Duration.zero] in tests for deterministic
|
||||
/// frame-by-frame goldens.
|
||||
Duration duration = const Duration(milliseconds: 220);
|
||||
Duration? _nextAnimationDuration;
|
||||
|
||||
/// Lerp curve.
|
||||
Curve curve = Curves.easeOut;
|
||||
|
||||
/// Wraps [SystemChrome.setEnabledSystemUIMode]. The resulting metric
|
||||
/// change (Android relaying out the activity for / against immersive)
|
||||
/// animates `viewPadding` from the current value to the new system value
|
||||
/// over [animate]. Pass `null` (or omit) to snap. Use this rather than
|
||||
/// calling `SystemChrome.setEnabledSystemUIMode` directly when entering
|
||||
/// or exiting immersive — otherwise the underlying chrome (NavBar /
|
||||
/// BottomBar / composer) snaps into place by ~status-bar height.
|
||||
Future<void> setSystemUiMode(
|
||||
SystemUiMode mode, {
|
||||
Duration? animate,
|
||||
}) async {
|
||||
_nextAnimationDuration = animate;
|
||||
await SystemChrome.setEnabledSystemUIMode(mode);
|
||||
}
|
||||
|
||||
/// Smoothed view-padding in logical pixels.
|
||||
EdgeInsets get viewPadding => _current;
|
||||
|
||||
@@ -54,13 +67,16 @@ class XInsets with ChangeNotifier, WidgetsBindingObserver {
|
||||
final next = _read(WidgetsBinding.instance.platformDispatcher.implicitView);
|
||||
if (next == _system) return;
|
||||
_system = next;
|
||||
if (duration == Duration.zero) {
|
||||
final animDuration = _nextAnimationDuration;
|
||||
_nextAnimationDuration = null;
|
||||
if (animDuration == null || animDuration == Duration.zero) {
|
||||
_current = next;
|
||||
_ticking = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
_from = _current;
|
||||
_animDuration = animDuration;
|
||||
_start = SchedulerBinding.instance.currentSystemFrameTimeStamp;
|
||||
if (!_ticking) {
|
||||
_ticking = true;
|
||||
@@ -69,9 +85,11 @@ class XInsets with ChangeNotifier, WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
Duration _animDuration = Duration.zero;
|
||||
|
||||
void _tick(Duration ts) {
|
||||
if (!_ticking) return;
|
||||
final dt = (ts - _start).inMicroseconds / duration.inMicroseconds;
|
||||
final dt = (ts - _start).inMicroseconds / _animDuration.inMicroseconds;
|
||||
final t = curve.transform(dt.clamp(0.0, 1.0));
|
||||
_current = EdgeInsets.lerp(_from, _system, t)!;
|
||||
notifyListeners();
|
||||
|
||||
Reference in New Issue
Block a user