Add semantics for Sliders (#4808)

Also, make SemanticsOwner into a real class and use it instead of a static in
several places.
This commit is contained in:
Adam Barth 2016-07-01 13:38:24 -07:00 committed by GitHub
parent f0671edfdb
commit 5ed8f1a1fd
19 changed files with 254 additions and 183 deletions

View file

@ -180,6 +180,8 @@ final Tween<double> _kLabelBalloonRadiusTween = new Tween<double>(begin: _kThumb
final Tween<double> _kLabelBalloonTipTween = new Tween<double>(begin: 0.0, end: -8.0);
final double _kLabelBalloonTipAttachmentRatio = math.sin(math.PI / 4.0);
const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider.
double _getAdditionalHeightForLabel(String label) {
return label == null ? 0.0 : _kLabelBalloonRadius * 2.0;
}
@ -191,7 +193,7 @@ BoxConstraints _getAdditionalConstraints(String label) {
);
}
class _RenderSlider extends RenderConstrainedBox {
class _RenderSlider extends RenderConstrainedBox implements SemanticActionHandler {
_RenderSlider({
double value,
int divisions,
@ -291,8 +293,10 @@ class _RenderSlider extends RenderConstrainedBox {
return dragValue;
}
bool get isInteractive => onChanged != null;
void _handleDragStart(DragStartDetails details) {
if (onChanged != null) {
if (isInteractive) {
_active = true;
_currentDragValue = (globalToLocal(details.globalPosition).x - _kReactionRadius) / _trackLength;
onChanged(_discretizedCurrentDragValue);
@ -302,7 +306,7 @@ class _RenderSlider extends RenderConstrainedBox {
}
void _handleDragUpdate(DragUpdateDetails details) {
if (onChanged != null) {
if (isInteractive) {
_currentDragValue += details.primaryDelta / _trackLength;
onChanged(_discretizedCurrentDragValue);
}
@ -322,7 +326,7 @@ class _RenderSlider extends RenderConstrainedBox {
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is PointerDownEvent && onChanged != null)
if (event is PointerDownEvent && isInteractive)
_drag.addPointer(event);
}
@ -331,7 +335,7 @@ class _RenderSlider extends RenderConstrainedBox {
final Canvas canvas = context.canvas;
final double trackLength = _trackLength;
final bool enabled = onChanged != null;
final bool enabled = isInteractive;
final double value = _position.value;
final double additionalHeightForLabel = _getAdditionalHeightForLabel(label);
@ -417,4 +421,32 @@ class _RenderSlider extends RenderConstrainedBox {
}
canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint);
}
@override
bool get hasSemantics => isInteractive;
@override
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode semantics) {
if (isInteractive)
semantics.addAdjustmentActions();
};
}
@override
void performAction(SemanticAction action) {
switch (action) {
case SemanticAction.increase:
if (isInteractive)
onChanged((value + _kAdjustmentUnit).clamp(0.0, 1.0));
break;
case SemanticAction.decrease:
if (isInteractive)
onChanged((value - _kAdjustmentUnit).clamp(0.0, 1.0));
break;
default:
assert(false);
break;
}
}
}

View file

@ -133,13 +133,19 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
///
/// Called automatically when the binding is created.
void initSemantics() {
SemanticsNode.onSemanticsEnabled = renderView.scheduleInitialSemantics;
shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) {
ensureSemantics();
mojom.SemanticsServerStub server = new mojom.SemanticsServerStub.fromEndpoint(endpoint);
server.impl = new SemanticsServer();
server.impl = new SemanticsServer(semanticsOwner: pipelineOwner.semanticsOwner);
});
}
void ensureSemantics() {
if (pipelineOwner.semanticsOwner == null)
renderView.scheduleInitialSemantics();
assert(pipelineOwner.semanticsOwner != null);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
beginFrame();
}
@ -153,10 +159,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
if (SemanticsNode.hasListeners) {
pipelineOwner.flushSemantics();
SemanticsNode.sendSemanticsTree();
}
pipelineOwner.flushSemantics();
}
@override

View file

@ -507,12 +507,12 @@ class _SemanticsGeometry {
abstract class _SemanticsFragment {
_SemanticsFragment({
RenderObject owner,
RenderObject renderObjectOwner,
Iterable<SemanticAnnotator> annotators,
List<_SemanticsFragment> children
}) {
assert(owner != null);
_ancestorChain = <RenderObject>[owner];
assert(renderObjectOwner != null);
_ancestorChain = <RenderObject>[renderObjectOwner];
if (annotators != null)
addAnnotators(annotators);
assert(() {
@ -531,7 +531,7 @@ abstract class _SemanticsFragment {
_ancestorChain.add(ancestor);
}
RenderObject get owner => _ancestorChain.first;
RenderObject get renderObjectOwner => _ancestorChain.first;
List<SemanticAnnotator> _annotators;
void addAnnotators(Iterable<SemanticAnnotator> moreAnnotators) {
@ -555,20 +555,20 @@ abstract class _SemanticsFragment {
/// that comes from the (dirty) ancestors.)
class _CleanSemanticsFragment extends _SemanticsFragment {
_CleanSemanticsFragment({
RenderObject owner
}) : super(owner: owner) {
assert(owner._semantics != null);
RenderObject renderObjectOwner
}) : super(renderObjectOwner: renderObjectOwner) {
assert(renderObjectOwner._semantics != null);
}
@override
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; });
SemanticsNode node = owner._semantics;
SemanticsNode node = renderObjectOwner._semantics;
assert(node != null);
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics);
} else {
assert(_ancestorChain.length == 1);
}
@ -578,10 +578,10 @@ class _CleanSemanticsFragment extends _SemanticsFragment {
abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
_InterestingSemanticsFragment({
RenderObject owner,
RenderObject renderObjectOwner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
}) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children);
bool get haveConcreteNode => true;
@ -593,7 +593,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
for (SemanticAnnotator annotator in _annotators)
annotator(node);
for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == owner);
assert(child._ancestorChain.last == renderObjectOwner);
node.addChildren(child.compile(
geometry: createSemanticsGeometryForChild(geometry),
currentSemantics: _children.length > 1 ? null : node,
@ -612,10 +612,10 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
class _RootSemanticsFragment extends _InterestingSemanticsFragment {
_RootSemanticsFragment({
RenderObject owner,
RenderObject renderObjectOwner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
}) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children);
@override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
@ -623,14 +623,14 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
assert(geometry == null);
assert(currentSemantics == null);
assert(parentSemantics == null);
owner._semantics ??= new SemanticsNode.root(
handler: owner is SemanticActionHandler ? owner as dynamic : null,
owner: owner.owner
renderObjectOwner._semantics ??= new SemanticsNode.root(
handler: renderObjectOwner is SemanticActionHandler ? renderObjectOwner as dynamic : null,
owner: renderObjectOwner.owner.semanticsOwner
);
SemanticsNode node = owner._semantics;
SemanticsNode node = renderObjectOwner._semantics;
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
assert(!node.wasAffectedByClip);
node.rect = owner.semanticBounds;
node.rect = renderObjectOwner.semanticBounds;
return node;
}
@ -642,20 +642,20 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
_ConcreteSemanticsFragment({
RenderObject owner,
RenderObject renderObjectOwner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
}) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children);
@override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
owner._semantics ??= new SemanticsNode(
handler: owner is SemanticActionHandler ? owner as dynamic : null
renderObjectOwner._semantics ??= new SemanticsNode(
handler: renderObjectOwner is SemanticActionHandler ? renderObjectOwner as dynamic : null
);
SemanticsNode node = owner._semantics;
SemanticsNode node = renderObjectOwner._semantics;
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics);
} else {
assert(_ancestorChain.length == 1);
}
@ -670,10 +670,10 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
_ImplicitSemanticsFragment({
RenderObject owner,
RenderObject renderObjectOwner,
Iterable<SemanticAnnotator> annotators,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, annotators: annotators, children: children);
}) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children);
@override
bool get haveConcreteNode => _haveConcreteNode;
@ -685,18 +685,18 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
assert(_haveConcreteNode == null);
_haveConcreteNode = currentSemantics == null && _annotators.isNotEmpty;
if (haveConcreteNode) {
owner._semantics ??= new SemanticsNode(
handler: owner is SemanticActionHandler ? owner as dynamic : null
renderObjectOwner._semantics ??= new SemanticsNode(
handler: renderObjectOwner is SemanticActionHandler ? renderObjectOwner as dynamic : null
);
node = owner._semantics;
node = renderObjectOwner._semantics;
} else {
owner._semantics = null;
renderObjectOwner._semantics = null;
node = currentSemantics;
}
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
if (haveConcreteNode)
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics);
} else {
assert(_ancestorChain.length == 1);
}
@ -713,9 +713,9 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
class _ForkingSemanticsFragment extends _SemanticsFragment {
_ForkingSemanticsFragment({
RenderObject owner,
RenderObject renderObjectOwner,
Iterable<_SemanticsFragment> children
}) : super(owner: owner, children: children) {
}) : super(renderObjectOwner: renderObjectOwner, children: children) {
assert(children != null);
assert(children.length > 1);
}
@ -727,7 +727,7 @@ class _ForkingSemanticsFragment extends _SemanticsFragment {
assert(geometry != null);
geometry.applyAncestorChain(_ancestorChain);
for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == owner);
assert(child._ancestorChain.last == renderObjectOwner);
yield* child.compile(
geometry: new _SemanticsGeometry.copy(geometry),
currentSemantics: null,
@ -880,7 +880,8 @@ class PipelineOwner {
}
}
bool _semanticsEnabled = false;
SemanticsOwner get semanticsOwner => _semanticsOwner;
SemanticsOwner _semanticsOwner;
bool _debugDoingSemantics = false;
List<RenderObject> _nodesNeedingSemantics = <RenderObject>[];
@ -892,8 +893,10 @@ class PipelineOwner {
///
/// See [RendererBinding] for an example of how this function is used.
void flushSemantics() {
if (_semanticsOwner == null)
return;
Timeline.startSync('Semantics');
assert(_semanticsEnabled);
assert(_semanticsOwner != null);
assert(() { _debugDoingSemantics = true; return true; });
try {
_nodesNeedingSemantics.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
@ -906,6 +909,7 @@ class PipelineOwner {
assert(() { _debugDoingSemantics = false; return true; });
Timeline.finishSync();
}
_semanticsOwner.sendSemanticsTree();
}
/// Cause the entire subtree rooted at the given [RenderObject] to
@ -1858,8 +1862,8 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
assert(!owner._debugDoingSemantics);
assert(_semantics == null);
assert(_needsSemanticsUpdate);
assert(owner._semanticsEnabled == false);
owner._semanticsEnabled = true;
assert(owner._semanticsOwner == null);
owner._semanticsOwner = new SemanticsOwner();
owner._nodesNeedingSemantics.add(this);
owner.requestVisualUpdate();
}
@ -1917,7 +1921,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// tree will be out of date.
void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) {
assert(!attached || !owner._debugDoingSemantics);
if ((attached && !owner._semanticsEnabled) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry)))
if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry)))
return;
if (!noGeometry && (_semantics == null || (_semantics.hasChildren && _semantics.wasAffectedByClip))) {
// Since the geometry might have changed, we need to make sure to reapply any clips.
@ -1981,7 +1985,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
// early-exit if we're not dirty and have our own semantics
if (!_needsSemanticsUpdate && hasSemantics) {
assert(_semantics != null);
return new _CleanSemanticsFragment(owner: this);
return new _CleanSemanticsFragment(renderObjectOwner: this);
}
List<_SemanticsFragment> children;
visitChildrenForSemantics((RenderObject child) {
@ -2004,16 +2008,16 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
_needsSemanticsGeometryUpdate = false;
Iterable<SemanticAnnotator> annotators = getSemanticAnnotators();
if (parent is! RenderObject)
return new _RootSemanticsFragment(owner: this, annotators: annotators, children: children);
return new _RootSemanticsFragment(renderObjectOwner: this, annotators: annotators, children: children);
if (hasSemantics)
return new _ConcreteSemanticsFragment(owner: this, annotators: annotators, children: children);
return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotators: annotators, children: children);
if (annotators.isNotEmpty)
return new _ImplicitSemanticsFragment(owner: this, annotators: annotators, children: children);
return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotators: annotators, children: children);
_semantics = null;
if (children == null)
return null;
if (children.length > 1)
return new _ForkingSemanticsFragment(owner: this, children: children);
return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children);
assert(children.length == 1);
return children.single;
}

View file

@ -78,7 +78,7 @@ class SemanticsNode extends AbstractNode {
/// The root node is assigned an identifier of zero.
SemanticsNode.root({
SemanticActionHandler handler,
Object owner
SemanticsOwner owner
}) : _id = 0,
_actionHandler = handler {
attach(owner);
@ -131,21 +131,34 @@ class SemanticsNode extends AbstractNode {
final Set<SemanticAction> _actions = new Set<SemanticAction>();
/// Adds the given action to the set of semantic actions.
///
/// If the user chooses to perform an action,
/// [SemanticActionHandler.performAction] will be called with the chosen
/// action.
void addAction(SemanticAction action) {
if (_actions.add(action))
_markDirty();
}
/// Adds the [SemanticAction.scrollLeft] and [SemanticAction.scrollRight] actions.
void addHorizontalScrollingActions() {
addAction(SemanticAction.scrollLeft);
addAction(SemanticAction.scrollRight);
}
/// Adds the [SemanticAction.scrollUp] and [SemanticAction.scrollDown] actions.
void addVerticalScrollingActions() {
addAction(SemanticAction.scrollUp);
addAction(SemanticAction.scrollDown);
}
/// Adds the [SemanticAction.increase] and [SemanticAction.decrease] actions.
void addAdjustmentActions() {
addAction(SemanticAction.increase);
addAction(SemanticAction.decrease);
}
bool _hasAction(SemanticAction action) {
return _actionHandler != null && _actions.contains(action);
}
@ -285,6 +298,9 @@ class SemanticsNode extends AbstractNode {
_markDirty();
}
@override
SemanticsOwner get owner => super.owner;
@override
SemanticsNode get parent => super.parent;
@ -309,15 +325,16 @@ class SemanticsNode extends AbstractNode {
return true;
}
static Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
static Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
@override
void attach(Object owner) {
void attach(SemanticsOwner owner) {
super.attach(owner);
assert(!_nodes.containsKey(_id));
_nodes[_id] = this;
_detachedNodes.remove(this);
assert(!owner._nodes.containsKey(_id));
owner._nodes[_id] = this;
owner._detachedNodes.remove(this);
if (_dirty) {
_dirty = false;
_markDirty();
}
if (parent != null)
_inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
if (_children != null) {
@ -328,26 +345,27 @@ class SemanticsNode extends AbstractNode {
@override
void detach() {
assert(owner._nodes.containsKey(_id));
assert(!owner._detachedNodes.contains(this));
owner._nodes.remove(_id);
owner._detachedNodes.add(this);
super.detach();
assert(_nodes.containsKey(_id));
assert(!_detachedNodes.contains(this));
_nodes.remove(_id);
_detachedNodes.add(this);
if (_children != null) {
for (SemanticsNode child in _children)
child.detach();
}
}
static List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
bool _dirty = false;
void _markDirty() {
if (_dirty)
return;
_dirty = true;
assert(!_dirtyNodes.contains(this));
assert(!_detachedNodes.contains(this));
_dirtyNodes.add(this);
if (attached) {
assert(!owner._dirtyNodes.contains(this));
assert(!owner._detachedNodes.contains(this));
owner._dirtyNodes.add(this);
}
}
mojom.SemanticsNode _serialize() {
@ -397,35 +415,71 @@ class SemanticsNode extends AbstractNode {
return result;
}
static List<mojom.SemanticsListener> _listeners;
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType($_id');
if (_dirty)
buffer.write(" (${ owner != null && owner._dirtyNodes.contains(this) ? 'dirty' : 'STALE' })");
if (_shouldMergeAllDescendantsIntoThisNode)
buffer.write(' (leaf merge)');
buffer.write('; $rect');
if (wasAffectedByClip)
buffer.write(' (clipped)');
for (SemanticAction action in _actions) {
buffer.write('; $action');
}
if (hasCheckedState) {
if (isChecked)
buffer.write('; checked');
else
buffer.write('; unchecked');
}
if (label.isNotEmpty)
buffer.write('; "$label"');
buffer.write(')');
return buffer.toString();
}
/// Returns a string representation of this node and its descendants.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
if (_children != null && _children.isNotEmpty) {
for (int index = 0; index < _children.length - 1; index += 1) {
SemanticsNode child = _children[index];
result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
}
result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines ")}';
}
return result;
}
}
class SemanticsOwner {
final List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
List<mojom.SemanticsListener> _listeners;
/// Whether there are currently any consumers of semantic data.
///
/// If there are no consumers of semantic data, there is no need to compile
/// semantic data into a [SemanticsNode] tree.
static bool get hasListeners => _listeners != null && _listeners.length > 0;
/// Called when the first consumer of semantic data arrives.
///
/// Typically set by [RendererBinding].
static VoidCallback onSemanticsEnabled;
bool get hasListeners => _listeners != null && _listeners.length > 0;
/// Add a consumer of semantic data.
///
/// After the [PipelineOwner] updates the semantic data for a given frame, it
/// calls [sendSemanticsTree], which uploads the data to each listener
/// registered with this function.
static void addListener(mojom.SemanticsListener listener) {
if (!hasListeners) {
assert(onSemanticsEnabled != null); // initialise the binding _before_ adding listeners
onSemanticsEnabled();
}
void addListener(mojom.SemanticsListener listener) {
_listeners ??= <mojom.SemanticsListener>[];
_listeners.add(listener);
}
/// Uploads the semantics tree to the listeners registered with [addListener].
static void sendSemanticsTree() {
void sendSemanticsTree() {
assert(hasListeners);
for (SemanticsNode oldNode in _detachedNodes) {
// The other side will have forgotten this node if we even send
@ -491,7 +545,7 @@ class SemanticsNode extends AbstractNode {
_dirtyNodes.clear();
}
static SemanticActionHandler _getSemanticActionHandlerForId(int id, { @required SemanticAction action }) {
SemanticActionHandler _getSemanticActionHandlerForId(int id, { @required SemanticAction action }) {
assert(action != null);
SemanticsNode result = _nodes[id];
if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._hasAction(action)) {
@ -508,59 +562,29 @@ class SemanticsNode extends AbstractNode {
return result._actionHandler;
}
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType($_id');
if (_dirty)
buffer.write(" (${ _dirtyNodes.contains(this) ? 'dirty' : 'STALE' })");
if (_shouldMergeAllDescendantsIntoThisNode)
buffer.write(' (leaf merge)');
buffer.write('; $rect');
if (wasAffectedByClip)
buffer.write(' (clipped)');
for (SemanticAction action in _actions) {
buffer.write('; $action');
}
if (hasCheckedState) {
if (isChecked)
buffer.write('; checked');
else
buffer.write('; unchecked');
}
if (label.isNotEmpty)
buffer.write('; "$label"');
buffer.write(')');
return buffer.toString();
}
/// Returns a string representation of this node and its descendants.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
if (_children != null && _children.isNotEmpty) {
for (int index = 0; index < _children.length - 1; index += 1) {
SemanticsNode child = _children[index];
result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
}
result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines ")}';
}
return result;
void performAction(int id, SemanticAction action) {
SemanticActionHandler handler = _getSemanticActionHandlerForId(id, action: action);
handler?.performAction(action);
}
}
/// Exposes the [SemanticsNode] tree to the underlying platform.
class SemanticsServer extends mojom.SemanticsServer {
SemanticsServer({ @required this.semanticsOwner }) {
assert(semanticsOwner != null);
}
final SemanticsOwner semanticsOwner;
@override
void addSemanticsListener(mojom.SemanticsListenerProxy listener) {
// TODO(abarth): We should remove the listener when this pipe closes.
// See <https://github.com/flutter/flutter/issues/3342>.
SemanticsNode.addListener(listener);
semanticsOwner.addListener(listener);
}
@override
void performAction(int id, mojom.SemanticAction encodedAction) {
SemanticAction action = SemanticAction.values[encodedAction.mojoEnumValue];
SemanticActionHandler node = SemanticsNode._getSemanticActionHandlerForId(id, action: action);
node?.performAction(action);
semanticsOwner.performAction(id, SemanticAction.values[encodedAction.mojoEnumValue]);
}
}

View file

@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'gesture_detector.dart';
@ -30,16 +31,23 @@ class SemanticsDebugger extends StatefulWidget {
}
class _SemanticsDebuggerState extends State<SemanticsDebugger> {
_SemanticsClient _client;
@override
void initState() {
super.initState();
_SemanticsDebuggerListener.ensureInstantiated();
_SemanticsDebuggerListener.instance.addListener(_update);
// TODO(abarth): We shouldn't reach out to the WidgetsBinding.instance
// static here because we might not be in a tree that's attached to that
// binding. Instead, we should find a way to get to the PipelineOwner from
// the BuildContext.
WidgetsBinding.instance.ensureSemantics();
_client = new _SemanticsClient(WidgetsBinding.instance.pipelineOwner.semanticsOwner)
..addListener(_update);
}
@override
void dispose() {
_SemanticsDebuggerListener.instance.removeListener(_update);
_client.removeListener(_update);
super.dispose();
}
@ -58,21 +66,21 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
void _handleTap() {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance._performAction(_lastPointerDownLocation, SemanticAction.tap);
_client._performAction(_lastPointerDownLocation, SemanticAction.tap);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handleLongPress() {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance._performAction(_lastPointerDownLocation, SemanticAction.longPress);
_client._performAction(_lastPointerDownLocation, SemanticAction.longPress);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handlePanEnd(DragEndDetails details) {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance.handlePanEnd(_lastPointerDownLocation, details.velocity);
_client.handlePanEnd(_lastPointerDownLocation, details.velocity);
setState(() {
_lastPointerDownLocation = null;
});
@ -81,7 +89,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
@override
Widget build(BuildContext context) {
return new CustomPaint(
foregroundPainter: new _SemanticsDebuggerPainter(_SemanticsDebuggerListener.instance.generation, _lastPointerDownLocation),
foregroundPainter: new _SemanticsDebuggerPainter(_client.generation, _client, _lastPointerDownLocation),
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleTap,
@ -196,6 +204,11 @@ class _SemanticsDebuggerEntry {
|| actions.contains(SemanticAction.scrollDown);
}
bool get _isAdjustable {
return actions.contains(SemanticAction.increase)
|| actions.contains(SemanticAction.decrease);
}
TextPainter textPainter;
void _updateMessage() {
List<String> annotations = <String>[];
@ -215,6 +228,8 @@ class _SemanticsDebuggerEntry {
annotations.add('long-pressable');
if (_isScrollable)
annotations.add('scrollable');
if (_isAdjustable)
annotations.add('adjustable');
String message;
if (annotations.isEmpty) {
assert(label != null);
@ -295,16 +310,12 @@ class _SemanticsDebuggerEntry {
}
}
class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.SemanticsListener {
_SemanticsDebuggerListener._() {
SemanticsNode.addListener(this);
class _SemanticsClient extends ChangeNotifier implements mojom.SemanticsListener {
_SemanticsClient(this.semanticsOwner) {
semanticsOwner.addListener(this);
}
static _SemanticsDebuggerListener instance;
static final SemanticsServer _server = new SemanticsServer();
static void ensureInstantiated() {
instance ??= new _SemanticsDebuggerListener._();
}
final SemanticsOwner semanticsOwner;
_SemanticsDebuggerEntry get rootNode => _nodes[0];
final Map<int, _SemanticsDebuggerEntry> _nodes = <int, _SemanticsDebuggerEntry>{};
@ -357,7 +368,7 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti
void _performAction(Point position, SemanticAction action) {
_SemanticsDebuggerEntry entry = _hitTest(position, (_SemanticsDebuggerEntry entry) => entry.actions.contains(action));
_server.performAction(entry?.id ?? 0, mojom.SemanticAction.values[action.index]);
semanticsOwner.performAction(entry?.id ?? 0, action);
}
void handlePanEnd(Point position, Velocity velocity) {
@ -366,10 +377,13 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti
if (vx.abs() == vy.abs())
return;
if (vx.abs() > vy.abs()) {
if (vx.sign < 0)
if (vx.sign < 0) {
_performAction(position, SemanticAction.decrease);
_performAction(position, SemanticAction.scrollLeft);
else
} else {
_performAction(position, SemanticAction.increase);
_performAction(position, SemanticAction.scrollRight);
}
} else {
if (vy.sign < 0)
_performAction(position, SemanticAction.scrollUp);
@ -380,14 +394,15 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti
}
class _SemanticsDebuggerPainter extends CustomPainter {
const _SemanticsDebuggerPainter(this.generation, this.pointerPosition);
const _SemanticsDebuggerPainter(this.generation, this.client, this.pointerPosition);
final int generation;
final _SemanticsClient client;
final Point pointerPosition;
@override
void paint(Canvas canvas, Size size) {
_SemanticsDebuggerEntry rootNode = _SemanticsDebuggerListener.instance.rootNode;
_SemanticsDebuggerEntry rootNode = client.rootNode;
rootNode?.paint(canvas, rootNode.findDepth());
if (pointerPosition != null) {
Paint paint = new Paint();
@ -399,6 +414,7 @@ class _SemanticsDebuggerPainter extends CustomPainter {
@override
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
return generation != oldDelegate.generation
|| client != oldDelegate.client
|| pointerPosition != oldDelegate.pointerPosition;
}
}

View file

@ -394,7 +394,7 @@ void main() {
});
testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
GlobalKey key = new GlobalKey();
await tester.pumpWidget(
new Overlay(

View file

@ -136,9 +136,10 @@ void main() {
test('objects can be detached and re-attached: semantics', () {
TestTree testTree = new TestTree();
TestSemanticsListener listener = new TestSemanticsListener();
SemanticsNode.addListener(listener);
renderer.ensureSemantics();
renderer.pipelineOwner.semanticsOwner.addListener(listener);
// Lay out, composite, paint, and update semantics
layout(testTree.root, phase: EnginePhase.sendSemanticsTree);
layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(listener.updates.length, equals(1));
// Remove testTree from the custom render view
renderer.renderView.child = null;
@ -148,7 +149,7 @@ void main() {
testTree.child.markNeedsSemanticsUpdate();
expect(listener.updates.length, equals(0));
// Lay out, composite, paint, and update semantics again
layout(testTree.root, phase: EnginePhase.sendSemanticsTree);
layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(listener.updates.length, equals(1));
});
}

View file

@ -12,8 +12,7 @@ enum EnginePhase {
compositingBits,
paint,
composite,
flushSemantics,
sendSemanticsTree
flushSemantics
}
class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, RendererBinding, GestureBinding {
@ -33,24 +32,21 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
renderView.compositeFrame();
if (phase == EnginePhase.composite)
return;
if (SemanticsNode.hasListeners) {
pipelineOwner.flushSemantics();
if (phase == EnginePhase.flushSemantics)
return;
SemanticsNode.sendSemanticsTree();
}
pipelineOwner.flushSemantics();
assert(phase == EnginePhase.flushSemantics);
}
}
TestRenderingFlutterBinding _renderer;
TestRenderingFlutterBinding get renderer => _renderer;
TestRenderingFlutterBinding get renderer {
_renderer ??= new TestRenderingFlutterBinding();
return _renderer;
}
void layout(RenderBox box, { BoxConstraints constraints, EnginePhase phase: EnginePhase.layout }) {
assert(box != null); // If you want to just repump the last box, call pumpFrame().
assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry.
_renderer ??= new TestRenderingFlutterBinding();
renderer.renderView.child = null;
if (constraints != null) {
box = new RenderPositionedBox(

View file

@ -11,7 +11,7 @@ import 'test_semantics.dart';
void main() {
testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
await tester.pumpWidget(
new Material(
child: new Center(

View file

@ -43,10 +43,7 @@ class OffscreenWidgetTree {
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame();
if (SemanticsNode.hasListeners) {
pipelineOwner.flushSemantics();
SemanticsNode.sendSemanticsTree();
}
pipelineOwner.flushSemantics();
buildOwner.finalizeTree();
}

View file

@ -11,7 +11,7 @@ import 'test_semantics.dart';
void main() {
testWidgets('Semantics 1', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
// smoketest
await tester.pumpWidget(

View file

@ -11,7 +11,7 @@ import 'test_semantics.dart';
void main() {
testWidgets('Semantics 2', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
// this test is the same as the test in Semantics 1, but
// starting with the second branch being ignored and then

View file

@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() {
testWidgets('Semantics 3', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
// implicit annotators
await tester.pumpWidget(

View file

@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() {
testWidgets('Semantics 4', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
// O
// / \ O=root

View file

@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() {
testWidgets('Semantics 5', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
await tester.pumpWidget(
new Stack(

View file

@ -11,7 +11,7 @@ import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
void main() {
testWidgets('Semantics 7 - Merging', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
String label;

View file

@ -10,7 +10,7 @@ import 'test_semantics.dart';
void main() {
testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async {
TestSemanticsListener client = new TestSemanticsListener();
TestSemanticsListener client = new TestSemanticsListener(tester);
await tester.pumpWidget(
new MergeSemantics(

View file

@ -2,12 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
class TestSemanticsListener implements mojom.SemanticsListener {
TestSemanticsListener() {
SemanticsNode.addListener(this);
TestSemanticsListener(WidgetTester tester) {
tester.binding.ensureSemantics();
tester.binding.pipelineOwner.semanticsOwner.addListener(this);
}
final List<mojom.SemanticsNode> updates = <mojom.SemanticsNode>[];

View file

@ -463,12 +463,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
renderView.compositeFrame(); // this sends the bits to the GPU
if (_phase == EnginePhase.composite)
return;
if (SemanticsNode.hasListeners) {
pipelineOwner.flushSemantics();
if (_phase == EnginePhase.flushSemantics)
return;
SemanticsNode.sendSemanticsTree();
}
pipelineOwner.flushSemantics();
if (_phase == EnginePhase.flushSemantics)
return;
buildOwner.finalizeTree();
}