mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Now we just have one TextSpan class that handles both simple strings, trees of children, and styling both. This approach simplifies the interface for most clients. This patch also removes StyledText, which was weakly typed and tricky to use correctly. The replacement is RichText, which is strongly typed and uses TextSpan.
371 lines
12 KiB
Dart
371 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 StatefulComponent {
|
|
const SemanticsDebugger({ Key key, this.child }) : super(key: key);
|
|
|
|
final Widget child;
|
|
|
|
_SemanticsDebuggerState createState() => new _SemanticsDebuggerState();
|
|
}
|
|
|
|
class _SemanticsDebuggerState extends State<SemanticsDebugger> {
|
|
void initState() {
|
|
super.initState();
|
|
_SemanticsDebuggerListener.ensureInstantiated();
|
|
_SemanticsDebuggerListener.instance.addListener(_update);
|
|
}
|
|
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;
|
|
});
|
|
}
|
|
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;
|
|
|
|
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 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 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;
|
|
|
|
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;
|
|
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);
|
|
}
|
|
}
|
|
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
|
|
return generation != oldDelegate.generation
|
|
|| pointerPosition != oldDelegate.pointerPosition;
|
|
}
|
|
}
|