Skip to content

Commit 4db47db

Browse files
authored
LinkedText (Linkify) (flutter#125927)
New LinkedText widget and TextLinker class for easily adding hyperlinks to text.
1 parent ee9aef0 commit 4db47db

File tree

18 files changed

+2520
-0
lines changed

18 files changed

+2520
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/gestures.dart';
6+
import 'package:flutter/material.dart';
7+
8+
// This example demonstrates highlighting both URLs and Twitter handles with
9+
// different actions and different styles.
10+
11+
void main() {
12+
runApp(const TextLinkerApp());
13+
}
14+
15+
class TextLinkerApp extends StatelessWidget {
16+
const TextLinkerApp({
17+
super.key,
18+
});
19+
20+
@override
21+
Widget build(BuildContext context) {
22+
return MaterialApp(
23+
title: 'Flutter Demo',
24+
theme: ThemeData(
25+
primarySwatch: Colors.blue,
26+
),
27+
home: const MyHomePage(title: 'Flutter Link Twitter Handle Demo'),
28+
);
29+
}
30+
}
31+
32+
class MyHomePage extends StatelessWidget {
33+
const MyHomePage({
34+
super.key,
35+
required this.title
36+
});
37+
38+
final String title;
39+
static const String _text = '@FlutterDev is our Twitter account, or find us at www.flutter.dev';
40+
41+
void _handleTapTwitterHandle(BuildContext context, String linkString) {
42+
final String handleWithoutAt = linkString.substring(1);
43+
final String twitterUriString = 'https://www.twitter.com/$handleWithoutAt';
44+
final Uri? uri = Uri.tryParse(twitterUriString);
45+
if (uri == null) {
46+
throw Exception('Failed to parse $twitterUriString.');
47+
}
48+
_showDialog(context, uri);
49+
}
50+
51+
void _handleTapUrl(BuildContext context, String urlText) {
52+
final Uri? uri = Uri.tryParse(urlText);
53+
if (uri == null) {
54+
throw Exception('Failed to parse $urlText.');
55+
}
56+
_showDialog(context, uri);
57+
}
58+
59+
void _showDialog(BuildContext context, Uri uri) {
60+
// A package like url_launcher would be useful for actually opening the URL
61+
// here instead of just showing a dialog.
62+
Navigator.of(context).push(
63+
DialogRoute<void>(
64+
context: context,
65+
builder: (BuildContext context) => AlertDialog(title: Text('You tapped: $uri')),
66+
),
67+
);
68+
}
69+
70+
@override
71+
Widget build(BuildContext context) {
72+
return Scaffold(
73+
appBar: AppBar(
74+
title: Text(title),
75+
),
76+
body: Center(
77+
child: Builder(
78+
builder: (BuildContext context) {
79+
return SelectionArea(
80+
child: _TwitterAndUrlLinkedText(
81+
text: _text,
82+
onTapUrl: (String urlString) => _handleTapUrl(context, urlString),
83+
onTapTwitterHandle: (String handleString) => _handleTapTwitterHandle(context, handleString),
84+
),
85+
);
86+
},
87+
),
88+
),
89+
);
90+
}
91+
}
92+
93+
class _TwitterAndUrlLinkedText extends StatefulWidget {
94+
const _TwitterAndUrlLinkedText({
95+
required this.text,
96+
required this.onTapUrl,
97+
required this.onTapTwitterHandle,
98+
});
99+
100+
final String text;
101+
final ValueChanged<String> onTapUrl;
102+
final ValueChanged<String> onTapTwitterHandle;
103+
104+
@override
105+
State<_TwitterAndUrlLinkedText> createState() => _TwitterAndUrlLinkedTextState();
106+
}
107+
108+
class _TwitterAndUrlLinkedTextState extends State<_TwitterAndUrlLinkedText> {
109+
final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];
110+
late Iterable<InlineSpan> _linkedSpans;
111+
late final List<TextLinker> _textLinkers;
112+
113+
final RegExp _twitterHandleRegExp = RegExp(r'@[a-zA-Z0-9]{4,15}');
114+
115+
void _disposeRecognizers() {
116+
for (final GestureRecognizer recognizer in _recognizers) {
117+
recognizer.dispose();
118+
}
119+
_recognizers.clear();
120+
}
121+
122+
void _linkSpans() {
123+
_disposeRecognizers();
124+
final Iterable<InlineSpan> linkedSpans = TextLinker.linkSpans(
125+
<TextSpan>[TextSpan(text: widget.text)],
126+
_textLinkers,
127+
);
128+
_linkedSpans = linkedSpans;
129+
}
130+
131+
@override
132+
void initState() {
133+
super.initState();
134+
135+
_textLinkers = <TextLinker>[
136+
TextLinker(
137+
regExp: LinkedText.defaultUriRegExp,
138+
linkBuilder: (String displayString, String linkString) {
139+
final TapGestureRecognizer recognizer = TapGestureRecognizer()
140+
..onTap = () => widget.onTapUrl(linkString);
141+
_recognizers.add(recognizer);
142+
return _MyInlineLinkSpan(
143+
text: displayString,
144+
color: const Color(0xff0000ee),
145+
recognizer: recognizer,
146+
);
147+
},
148+
),
149+
TextLinker(
150+
regExp: _twitterHandleRegExp,
151+
linkBuilder: (String displayString, String linkString) {
152+
final TapGestureRecognizer recognizer = TapGestureRecognizer()
153+
..onTap = () => widget.onTapTwitterHandle(linkString);
154+
_recognizers.add(recognizer);
155+
return _MyInlineLinkSpan(
156+
text: displayString,
157+
color: const Color(0xff00aaaa),
158+
recognizer: recognizer,
159+
);
160+
},
161+
),
162+
];
163+
164+
_linkSpans();
165+
}
166+
167+
@override
168+
void didUpdateWidget(_TwitterAndUrlLinkedText oldWidget) {
169+
super.didUpdateWidget(oldWidget);
170+
171+
if (widget.text != oldWidget.text
172+
|| widget.onTapUrl != oldWidget.onTapUrl
173+
|| widget.onTapTwitterHandle != oldWidget.onTapTwitterHandle) {
174+
_linkSpans();
175+
}
176+
}
177+
178+
@override
179+
void dispose() {
180+
_disposeRecognizers();
181+
super.dispose();
182+
}
183+
184+
@override
185+
Widget build(BuildContext context) {
186+
if (_linkedSpans.isEmpty) {
187+
return const SizedBox.shrink();
188+
}
189+
190+
return Text.rich(
191+
TextSpan(
192+
style: DefaultTextStyle.of(context).style,
193+
children: _linkedSpans.toList(),
194+
),
195+
);
196+
}
197+
}
198+
199+
class _MyInlineLinkSpan extends TextSpan {
200+
_MyInlineLinkSpan({
201+
required String text,
202+
required Color color,
203+
required super.recognizer,
204+
}) : super(
205+
style: TextStyle(
206+
color: color,
207+
decorationColor: color,
208+
decoration: TextDecoration.underline,
209+
),
210+
mouseCursor: SystemMouseCursors.click,
211+
text: text,
212+
);
213+
}

0 commit comments

Comments
 (0)