Skip to content

Commit

Permalink
Add the ability to recognize gestures on text spans
Browse files Browse the repository at this point in the history
Currently the interface for recognizing gestures on text spans is pretty ugly,
but hopefully we can improve it with time.

Fixes flutter#156
  • Loading branch information
abarth committed Feb 25, 2016
1 parent 6260966 commit 8e326d7
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 10 deletions.
55 changes: 48 additions & 7 deletions packages/flutter/lib/src/painting/text_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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 = '']) {
Expand All @@ -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.
Expand Down
16 changes: 13 additions & 3 deletions packages/flutter/lib/src/rendering/paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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();
};
}

Expand Down
80 changes: 80 additions & 0 deletions packages/flutter/test/widget/hyperlink_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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);
});
});
}

0 comments on commit 8e326d7

Please sign in to comment.