Commit 9d4e0e85 authored by Adam Barth's avatar Adam Barth

Merge pull request #2142 from abarth/interactive_text

Add the ability to recognize gestures on text spans
parents 4ef20148 8e326d72
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextBox; import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextBox;
import 'package:flutter/gestures.dart';
import 'basic_types.dart'; import 'basic_types.dart';
import 'text_editing.dart'; import 'text_editing.dart';
import 'text_style.dart'; import 'text_style.dart';
...@@ -26,7 +28,8 @@ class TextSpan { ...@@ -26,7 +28,8 @@ class TextSpan {
const TextSpan({ const TextSpan({
this.style, this.style,
this.text, this.text,
this.children this.children,
this.recognizer
}); });
/// The style to apply to the text and the children. /// The style to apply to the text and the children.
...@@ -44,6 +47,9 @@ class TextSpan { ...@@ -44,6 +47,9 @@ class TextSpan {
/// children. /// children.
final List<TextSpan> children; final List<TextSpan> children;
/// If non-null, will receive events that hit this text span.
final GestureRecognizer recognizer;
void build(ui.ParagraphBuilder builder) { void build(ui.ParagraphBuilder builder) {
final bool hasStyle = style != null; final bool hasStyle = style != null;
if (hasStyle) if (hasStyle)
...@@ -60,13 +66,47 @@ class TextSpan { ...@@ -60,13 +66,47 @@ class TextSpan {
builder.pop(); builder.pop();
} }
void writePlainText(StringBuffer result) { bool visitTextSpan(bool visitor(TextSpan span)) {
if (text != null) if (text != null) {
result.write(text); if (!visitor(this))
return false;
}
if (children != null) { if (children != null) {
for (TextSpan child in children) for (TextSpan child in children) {
child.writePlainText(result); if (!child.visitTextSpan(visitor))
return false;
}
} }
return true;
}
TextSpan getSpanForPosition(TextPosition position) {
TextAffinity affinity = position.affinity;
int targetOffset = position.offset;
int offset = 0;
TextSpan result;
visitTextSpan((TextSpan span) {
assert(result == null);
int endOffset = offset + span.text.length;
if (targetOffset == offset && affinity == TextAffinity.downstream ||
targetOffset > offset && targetOffset < endOffset ||
targetOffset == endOffset && affinity == TextAffinity.upstream) {
result = span;
return false;
}
offset = endOffset;
return true;
});
return result;
}
String toPlainText() {
StringBuffer buffer = new StringBuffer();
visitTextSpan((TextSpan span) {
buffer.write(span.text);
return true;
});
return buffer.toString();
} }
String toString([String prefix = '']) { String toString([String prefix = '']) {
...@@ -89,9 +129,10 @@ class TextSpan { ...@@ -89,9 +129,10 @@ class TextSpan {
final TextSpan typedOther = other; final TextSpan typedOther = other;
return typedOther.text == text return typedOther.text == text
&& typedOther.style == style && typedOther.style == style
&& typedOther.recognizer == recognizer
&& _deepEquals(typedOther.children, children); && _deepEquals(typedOther.children, children);
} }
int get hashCode => hashValues(style, text, hashList(children)); int get hashCode => hashValues(style, text, recognizer, hashList(children));
} }
/// An object that paints a [TextSpan] into a canvas. /// An object that paints a [TextSpan] into a canvas.
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'semantics.dart'; import 'semantics.dart';
...@@ -81,6 +83,16 @@ class RenderParagraph extends RenderBox { ...@@ -81,6 +83,16 @@ class RenderParagraph extends RenderBox {
bool hitTestSelf(Point position) => true; bool hitTestSelf(Point position) => true;
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is! PointerDownEvent)
return;
_layoutText(constraints);
Offset offset = entry.localPosition.toOffset();
TextPosition position = _textPainter.getPositionForOffset(offset);
TextSpan span = _textPainter.text.getSpanForPosition(position);
span?.recognizer?.addPointer(event);
}
void performLayout() { void performLayout() {
_layoutText(constraints); _layoutText(constraints);
size = constraints.constrain(_textPainter.size); size = constraints.constrain(_textPainter.size);
...@@ -100,9 +112,7 @@ class RenderParagraph extends RenderBox { ...@@ -100,9 +112,7 @@ class RenderParagraph extends RenderBox {
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* { Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode node) { yield (SemanticsNode node) {
StringBuffer buffer = new StringBuffer(); node.label = text.toPlainText();
text.writePlainText(buffer);
node.label = buffer.toString();
}; };
} }
......
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
void main() {
test('Can tap a hyperlink', () {
testWidgets((WidgetTester tester) {
bool didTapLeft = false;
TapGestureRecognizer tapLeft = new TapGestureRecognizer(
router: Gesturer.instance.pointerRouter,
gestureArena: Gesturer.instance.gestureArena,
onTap: () {
didTapLeft = true;
}
);
bool didTapRight = false;
TapGestureRecognizer tapRight = new TapGestureRecognizer(
router: Gesturer.instance.pointerRouter,
gestureArena: Gesturer.instance.gestureArena,
onTap: () {
didTapRight = true;
}
);
Key textKey = new Key('text');
tester.pumpWidget(
new Center(
child: new RichText(
key: textKey,
text: new TextSpan(
children: <TextSpan>[
new TextSpan(
text: 'xxxxxxxx',
recognizer: tapLeft
),
new TextSpan(text: 'yyyyyyyy'),
new TextSpan(
text: 'zzzzzzzzz',
recognizer: tapRight
),
]
)
)
)
);
Element element = tester.findElementByKey(textKey);
RenderBox box = element.renderObject;
expect(didTapLeft, isFalse);
expect(didTapRight, isFalse);
tester.tapAt(box.localToGlobal(Point.origin) + new Offset(2.0, 2.0));
expect(didTapLeft, isTrue);
expect(didTapRight, isFalse);
didTapLeft = false;
tester.tapAt(box.localToGlobal(Point.origin) + new Offset(30.0, 2.0));
expect(didTapLeft, isTrue);
expect(didTapRight, isFalse);
didTapLeft = false;
tester.tapAt(box.localToGlobal(new Point(box.size.width, 0.0)) + new Offset(-2.0, 2.0));
expect(didTapLeft, isFalse);
expect(didTapRight, isTrue);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment