85 lines
3.7 KiB
Dart
85 lines
3.7 KiB
Dart
import 'package:flutter/foundation.dart';
|
|||
|
|
import 'package:flutter/semantics.dart';
|
||
|
|
|
||
|
|
/// Lets the app decline to build a semantics tree while the platform has
|
||
|
|
/// accessibility switched on but nothing is going to read the tree.
|
||
|
|
///
|
||
|
|
/// Android turns Flutter semantics on whenever *any* accessibility service is
|
||
|
|
/// bound, and on TV that is routinely a utility with no interest in app
|
||
|
|
/// content — a launcher's "current app" hook, a key remapper. The framework
|
||
|
|
/// then compiles and ships a full semantics tree every frame that touches a
|
||
|
|
/// node: measured on an Android 14 TV box with the Projectivy Launcher service
|
||
|
|
/// bound, 2 ms of every frame and ~20 ms on each spotlight swap, plus the
|
||
|
|
/// allocation churn behind it. A screen reader must see everything, so the
|
||
|
|
/// decision is made per enabled service by `AssistiveTechnologyService`; this
|
||
|
|
/// mixin is only the switch.
|
||
|
|
///
|
||
|
|
/// The switch sits under [SemanticsBinding.semanticsEnabled], the single value
|
||
|
|
/// the pipeline owner and the engine handshake consult, so turning it off tears
|
||
|
|
/// the semantics owner down exactly as if the platform had disabled
|
||
|
|
/// accessibility: no tree is compiled, no update is sent, and the engine is
|
||
|
|
/// told through `setSemanticsTreeEnabled` to drop its copy. Defaults to wanted.
|
||
|
|
///
|
||
|
|
/// An explicit [ensureSemantics] client (the debug-build handle kept for UI
|
||
|
|
/// automation) always wins: the gate only ever suppresses the platform's own
|
||
|
|
/// request. Disposing such a client is not observable here, so a closed gate
|
||
|
|
/// takes effect again at the next platform or gate change rather than at once.
|
||
|
|
mixin SemanticsTreeGate on SemanticsBinding {
|
||
|
|
final _GateNotifier _gate = _GateNotifier();
|
||
|
|
|
||
|
|
/// Whether a platform request for semantics should produce a tree.
|
||
|
|
bool get semanticsTreeWanted => _gate.wanted;
|
||
|
|
set semanticsTreeWanted(bool value) {
|
||
|
|
if (_gate.wanted == value) return;
|
||
|
|
_gate.wanted = value;
|
||
|
|
_gate.notify();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether the platform (or an explicit [ensureSemantics] client) asked for
|
||
|
|
/// semantics, before this gate is applied.
|
||
|
|
bool get platformSemanticsRequested => super.semanticsEnabled;
|
||
|
|
|
||
|
|
/// The platform holds at most one handle; anything beyond it is an explicit
|
||
|
|
/// client that must keep receiving a tree.
|
||
|
|
bool get _hasExplicitClient => debugOutstandingSemanticsHandles > (platformDispatcher.semanticsEnabled ? 1 : 0);
|
||
|
|
|
||
|
|
@override
|
||
|
|
bool get semanticsEnabled => super.semanticsEnabled && (_gate.wanted || _hasExplicitClient);
|
||
|
|
|
||
|
|
@override
|
||
|
|
SemanticsHandle ensureSemantics() {
|
||
|
|
final handle = super.ensureSemantics();
|
||
|
|
// A new explicit client may have just re-enabled a gated tree; the base
|
||
|
|
// notifier only fires on the false→true edge, which the platform's own
|
||
|
|
// handle has already consumed.
|
||
|
|
if (!_gate.wanted && _hasExplicitClient) _gate.notify();
|
||
|
|
return handle;
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void addSemanticsEnabledListener(VoidCallback listener) {
|
||
|
|
super.addSemanticsEnabledListener(listener);
|
||
|
|
_gate.addListener(listener);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void removeSemanticsEnabledListener(VoidCallback listener) {
|
||
|
|
super.removeSemanticsEnabledListener(listener);
|
||
|
|
_gate.removeListener(listener);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Listens to [platformSemanticsRequested] only; unlike
|
||
|
|
/// [addSemanticsEnabledListener] it does not fire when the gate flips, so the
|
||
|
|
/// service driving the gate can react to platform changes without re-entering
|
||
|
|
/// its own decision.
|
||
|
|
void addPlatformSemanticsListener(VoidCallback listener) => super.addSemanticsEnabledListener(listener);
|
||
|
|
|
||
|
|
void removePlatformSemanticsListener(VoidCallback listener) => super.removeSemanticsEnabledListener(listener);
|
||
|
|
}
|
||
|
|
|
||
|
|
class _GateNotifier extends ChangeNotifier {
|
||
|
|
bool wanted = true;
|
||
|
|
|
||
|
|
void notify() => notifyListeners();
|
||
|
|
}
|