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 @@
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextBox;
import 'package:flutter/gestures.dart';
import 'basic_types.dart';
import 'text_editing.dart';
import 'text_style.dart';
......@@ -26,7 +28,8 @@ class TextSpan {
const TextSpan({
this.style,
this.text,
this.children
this.children,
this.recognizer
});
/// The style to apply to the text and the children.
......@@ -44,6 +47,9 @@ class 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) {
final bool hasStyle = style != null;
if (hasStyle)
......@@ -60,13 +66,47 @@ class TextSpan {
builder.pop();
}
void writePlainText(StringBuffer result) {
if (text != null)
result.write(text);
bool visitTextSpan(bool visitor(TextSpan span)) {
if (text != null) {
if (!visitor(this))
return false;
}
if (children != null) {
for (TextSpan child in children)
child.writePlainText(result);
for (TextSpan child in children) {
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 = '']) {
......@@ -89,9 +129,10 @@ class TextSpan {
final TextSpan typedOther = other;
return typedOther.text == text
&& typedOther.style == style
&& typedOther.recognizer == recognizer
&& _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.
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'box.dart';
import 'object.dart';
import 'semantics.dart';
......@@ -81,6 +83,16 @@ class RenderParagraph extends RenderBox {
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() {
_layoutText(constraints);
size = constraints.constrain(_textPainter.size);
......@@ -100,9 +112,7 @@ class RenderParagraph extends RenderBox {
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode node) {
StringBuffer buffer = new StringBuffer();
text.writePlainText(buffer);
node.label = buffer.toString();
node.label = text.toPlainText();
};
}
......
// 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