mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This patch adds a consistent, simple description for the common pattern of having a single child widget.
388 lines
12 KiB
Dart
388 lines
12 KiB
Dart
// Copyright 2015 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
|
|
|
|
import 'basic.dart';
|
|
import 'framework.dart';
|
|
import 'gesture_detector.dart';
|
|
|
|
/// Visualizes the semantics for the child.
|
|
///
|
|
/// This widget is useful for understand how an app presents itself to
|
|
/// accessibility technology.
|
|
class SemanticsDebugger extends StatefulWidget {
|
|
const SemanticsDebugger({ Key key, this.child }) : super(key: key);
|
|
|
|
/// The widget below this widget in the tree.
|
|
final Widget child;
|
|
|
|
@override
|
|
_SemanticsDebuggerState createState() => new _SemanticsDebuggerState();
|
|
}
|
|
|
|
class _SemanticsDebuggerState extends State<SemanticsDebugger> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_SemanticsDebuggerListener.ensureInstantiated();
|
|
_SemanticsDebuggerListener.instance.addListener(_update);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_SemanticsDebuggerListener.instance.removeListener(_update);
|
|
super.dispose();
|
|
}
|
|
|
|
void _update() {
|
|
setState(() {
|
|
// the generation of the _SemanticsDebuggerListener has changed
|
|
});
|
|
}
|
|
|
|
Point _lastPointerDownLocation;
|
|
void _handlePointerDown(PointerDownEvent event) {
|
|
setState(() {
|
|
_lastPointerDownLocation = event.position;
|
|
});
|
|
}
|
|
|
|
void _handleTap() {
|
|
assert(_lastPointerDownLocation != null);
|
|
_SemanticsDebuggerListener.instance.handleTap(_lastPointerDownLocation);
|
|
setState(() {
|
|
_lastPointerDownLocation = null;
|
|
});
|
|
}
|
|
void _handleLongPress() {
|
|
assert(_lastPointerDownLocation != null);
|
|
_SemanticsDebuggerListener.instance.handleLongPress(_lastPointerDownLocation);
|
|
setState(() {
|
|
_lastPointerDownLocation = null;
|
|
});
|
|
}
|
|
void _handlePanEnd(Velocity velocity) {
|
|
assert(_lastPointerDownLocation != null);
|
|
_SemanticsDebuggerListener.instance.handlePanEnd(_lastPointerDownLocation, velocity);
|
|
setState(() {
|
|
_lastPointerDownLocation = null;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return new CustomPaint(
|
|
foregroundPainter: new _SemanticsDebuggerPainter(_SemanticsDebuggerListener.instance.generation, _lastPointerDownLocation),
|
|
child: new GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: _handleTap,
|
|
onLongPress: _handleLongPress,
|
|
onPanEnd: _handlePanEnd,
|
|
excludeFromSemantics: true, // otherwise if you don't hit anything, we end up receiving it, which causes an infinite loop...
|
|
child: new Listener(
|
|
onPointerDown: _handlePointerDown,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: new IgnorePointer(
|
|
ignoringSemantics: false,
|
|
child: config.child
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
typedef bool _SemanticsDebuggerEntryFilter(_SemanticsDebuggerEntry entry);
|
|
|
|
class _SemanticsDebuggerEntry {
|
|
_SemanticsDebuggerEntry(this.id);
|
|
|
|
final int id;
|
|
bool canBeTapped = false;
|
|
bool canBeLongPressed = false;
|
|
bool canBeScrolledHorizontally = false;
|
|
bool canBeScrolledVertically = false;
|
|
bool hasCheckedState = false;
|
|
bool isChecked = false;
|
|
String label;
|
|
Matrix4 transform;
|
|
Rect rect;
|
|
List<_SemanticsDebuggerEntry> children;
|
|
|
|
@override
|
|
String toString() {
|
|
return '_SemanticsDebuggerEntry($id; $rect; "$label"'
|
|
'${canBeTapped ? "; canBeTapped" : ""}'
|
|
'${canBeLongPressed ? "; canBeLongPressed" : ""}'
|
|
'${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
|
|
'${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
|
|
'${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}'
|
|
')';
|
|
}
|
|
String toStringDeep([ String prefix = '']) {
|
|
if (prefix.length > 20)
|
|
return '$prefix<ABORTED>\n';
|
|
String result = '$prefix$this\n';
|
|
for (_SemanticsDebuggerEntry child in children.reversed) {
|
|
prefix += ' ';
|
|
result += '${child.toStringDeep(prefix)}';
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int findDepth() {
|
|
if (children == null || children.isEmpty)
|
|
return 1;
|
|
return children.map((_SemanticsDebuggerEntry e) => e.findDepth()).reduce((int runningDepth, int nextDepth) {
|
|
return math.max(runningDepth, nextDepth);
|
|
}) + 1;
|
|
}
|
|
|
|
static const TextStyle textStyles = const TextStyle(
|
|
color: const Color(0xFF000000),
|
|
fontSize: 10.0,
|
|
height: 0.8,
|
|
textAlign: TextAlign.center
|
|
);
|
|
|
|
TextPainter textPainter;
|
|
void updateMessage() {
|
|
List<String> annotations = <String>[];
|
|
bool wantsTap = false;
|
|
if (hasCheckedState) {
|
|
annotations.add(isChecked ? 'checked' : 'unchecked');
|
|
wantsTap = true;
|
|
}
|
|
if (canBeTapped) {
|
|
if (!wantsTap)
|
|
annotations.add('button');
|
|
} else {
|
|
if (wantsTap)
|
|
annotations.add('disabled');
|
|
}
|
|
if (canBeLongPressed)
|
|
annotations.add('long-pressable');
|
|
if (canBeScrolledHorizontally || canBeScrolledVertically)
|
|
annotations.add('scrollable');
|
|
String message;
|
|
if (annotations.isEmpty) {
|
|
assert(label != null);
|
|
message = label;
|
|
} else {
|
|
if (label == '') {
|
|
message = annotations.join('; ');
|
|
} else {
|
|
message = '$label (${annotations.join('; ')})';
|
|
}
|
|
}
|
|
message = message.trim();
|
|
if (message != '') {
|
|
textPainter ??= new TextPainter();
|
|
textPainter.text = new TextSpan(style: textStyles, text: message);
|
|
textPainter.maxWidth = rect.width;
|
|
textPainter.maxHeight = rect.height;
|
|
textPainter.layout();
|
|
} else {
|
|
textPainter = null;
|
|
}
|
|
}
|
|
|
|
void paint(Canvas canvas, int rank) {
|
|
canvas.save();
|
|
if (transform != null)
|
|
canvas.transform(transform.storage);
|
|
if (!rect.isEmpty) {
|
|
Color lineColor = new Color(0xFF000000 + new math.Random(id).nextInt(0xFFFFFF));
|
|
Rect innerRect = rect.deflate(rank * 1.0);
|
|
if (innerRect.isEmpty) {
|
|
Paint fill = new Paint()
|
|
..color = lineColor
|
|
..style = PaintingStyle.fill;
|
|
canvas.drawRect(rect, fill);
|
|
} else {
|
|
Paint fill = new Paint()
|
|
..color = const Color(0xFFFFFFFF)
|
|
..style = PaintingStyle.fill;
|
|
canvas.drawRect(rect, fill);
|
|
Paint line = new Paint()
|
|
..strokeWidth = rank * 2.0
|
|
..color = lineColor
|
|
..style = PaintingStyle.stroke;
|
|
canvas.drawRect(innerRect, line);
|
|
}
|
|
if (textPainter != null) {
|
|
canvas.save();
|
|
canvas.clipRect(rect);
|
|
textPainter.paint(canvas, rect.topLeft.toOffset());
|
|
canvas.restore();
|
|
}
|
|
}
|
|
for (_SemanticsDebuggerEntry child in children)
|
|
child.paint(canvas, rank - 1);
|
|
canvas.restore();
|
|
}
|
|
|
|
_SemanticsDebuggerEntry hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
|
|
if (transform != null) {
|
|
Matrix4 invertedTransform = new Matrix4.identity();
|
|
double determinant = invertedTransform.copyInverse(transform);
|
|
if (determinant == 0.0)
|
|
return null;
|
|
position = MatrixUtils.transformPoint(invertedTransform, position);
|
|
}
|
|
if (!rect.contains(position))
|
|
return null;
|
|
_SemanticsDebuggerEntry result;
|
|
for (_SemanticsDebuggerEntry child in children.reversed) {
|
|
result = child.hitTest(position, filter);
|
|
if (result != null)
|
|
break;
|
|
}
|
|
if (result == null || !filter(result))
|
|
result = this;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class _SemanticsDebuggerListener implements mojom.SemanticsListener {
|
|
_SemanticsDebuggerListener._() {
|
|
SemanticsNode.addListener(this);
|
|
}
|
|
|
|
static _SemanticsDebuggerListener instance;
|
|
static final SemanticsServer _server = new SemanticsServer();
|
|
static void ensureInstantiated() {
|
|
instance ??= new _SemanticsDebuggerListener._();
|
|
}
|
|
|
|
Set<VoidCallback> _listeners = new Set<VoidCallback>();
|
|
void addListener(VoidCallback callback) {
|
|
assert(!_listeners.contains(callback));
|
|
_listeners.add(callback);
|
|
}
|
|
void removeListener(VoidCallback callback) {
|
|
_listeners.remove(callback);
|
|
}
|
|
|
|
Map<int, _SemanticsDebuggerEntry> nodes = <int, _SemanticsDebuggerEntry>{};
|
|
|
|
_SemanticsDebuggerEntry _updateNode(mojom.SemanticsNode node) {
|
|
_SemanticsDebuggerEntry entry = nodes.putIfAbsent(node.id, () => new _SemanticsDebuggerEntry(node.id));
|
|
if (node.flags != null) {
|
|
entry.canBeTapped = node.flags.canBeTapped;
|
|
entry.canBeLongPressed = node.flags.canBeLongPressed;
|
|
entry.canBeScrolledHorizontally = node.flags.canBeScrolledHorizontally;
|
|
entry.canBeScrolledVertically = node.flags.canBeScrolledVertically;
|
|
entry.hasCheckedState = node.flags.hasCheckedState;
|
|
entry.isChecked = node.flags.isChecked;
|
|
}
|
|
if (node.strings != null) {
|
|
assert(node.strings.label != null);
|
|
entry.label = node.strings.label;
|
|
} else {
|
|
assert(entry.label != null);
|
|
}
|
|
if (node.geometry != null) {
|
|
if (node.geometry.transform != null) {
|
|
assert(node.geometry.transform.length == 16);
|
|
// TODO(ianh): Replace this with a cleaner call once
|
|
// https://github.com/google/vector_math.dart/issues/159
|
|
// is fixed.
|
|
List<double> array = node.geometry.transform;
|
|
entry.transform = new Matrix4(
|
|
array[0], array[1], array[2], array[3],
|
|
array[4], array[5], array[6], array[7],
|
|
array[8], array[9], array[10], array[11],
|
|
array[12], array[13], array[14], array[15]
|
|
);
|
|
} else {
|
|
entry.transform = null;
|
|
}
|
|
entry.rect = new Rect.fromLTWH(node.geometry.left, node.geometry.top, node.geometry.width, node.geometry.height);
|
|
}
|
|
entry.updateMessage();
|
|
if (node.children != null) {
|
|
Set<_SemanticsDebuggerEntry> oldChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children ?? const <_SemanticsDebuggerEntry>[]);
|
|
entry.children?.clear();
|
|
entry.children ??= new List<_SemanticsDebuggerEntry>();
|
|
for (mojom.SemanticsNode child in node.children)
|
|
entry.children.add(_updateNode(child));
|
|
Set<_SemanticsDebuggerEntry> newChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children);
|
|
Set<_SemanticsDebuggerEntry> removedChildren = oldChildren.difference(newChildren);
|
|
for (_SemanticsDebuggerEntry oldChild in removedChildren)
|
|
nodes.remove(oldChild.id);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
int generation = 0;
|
|
|
|
@override
|
|
void updateSemanticsTree(List<mojom.SemanticsNode> nodes) {
|
|
generation += 1;
|
|
for (mojom.SemanticsNode node in nodes)
|
|
_updateNode(node);
|
|
for (VoidCallback listener in _listeners)
|
|
listener();
|
|
}
|
|
|
|
_SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
|
|
return nodes[0]?.hitTest(position, filter);
|
|
}
|
|
|
|
void handleTap(Point position) {
|
|
_server.tap(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeTapped)?.id ?? 0);
|
|
}
|
|
void handleLongPress(Point position) {
|
|
_server.longPress(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeLongPressed)?.id ?? 0);
|
|
}
|
|
void handlePanEnd(Point position, Velocity velocity) {
|
|
double vx = velocity.pixelsPerSecond.dx;
|
|
double vy = velocity.pixelsPerSecond.dy;
|
|
if (vx.abs() == vy.abs())
|
|
return;
|
|
if (vx.abs() > vy.abs()) {
|
|
if (vx.sign < 0)
|
|
_server.scrollLeft(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
|
|
else
|
|
_server.scrollRight(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
|
|
} else {
|
|
if (vy.sign < 0)
|
|
_server.scrollUp(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
|
|
else
|
|
_server.scrollDown(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _SemanticsDebuggerPainter extends CustomPainter {
|
|
const _SemanticsDebuggerPainter(this.generation, this.pointerPosition);
|
|
|
|
final int generation;
|
|
final Point pointerPosition;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
_SemanticsDebuggerListener.instance.nodes[0]?.paint(
|
|
canvas,
|
|
_SemanticsDebuggerListener.instance.nodes[0].findDepth()
|
|
);
|
|
if (pointerPosition != null) {
|
|
Paint paint = new Paint();
|
|
paint.color = const Color(0x7F0090FF);
|
|
canvas.drawCircle(pointerPosition, 10.0, paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
|
|
return generation != oldDelegate.generation
|
|
|| pointerPosition != oldDelegate.pointerPosition;
|
|
}
|
|
}
|