Files
plezy/lib/utils/semantics_tree_gate.dart
T

85 lines
3.7 KiB
Dart
Raw Normal View History

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();
}