New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SelectableText.rich used along with TapGestureRecognizer is not working #43494
Comments
Hi @anjaneyasivan |
This comment has been minimized.
This comment has been minimized.
I use a GestureDetector to handle tap(onTapUp seems cant be triggered so i use pandown panend...) and build a paragraph to know what textspan contains the tap offset and trigger it's onTap.But it need a extra paragraph, reduce performance and increase memory usage |
Could you share some example codes of achieving this ? |
This issue is affecting a Is there any update or working examples? |
@lukelyyeung @Cretezy my workaround class _TextMessageBoxState extends _MessageBoxState {
TextSpan _messageTextSpan;
bool _containsUrl = false;
Paragraph _paragraph;
ParagraphStyle _paragraphStyle;
ParagraphConstraints _paragraphConstraints;
int _currentTapUrlSpanIndex;
Offset _startPosition;
Offset _nowPosition;
Timer _cancelTapTimer;
static final RegExp _urlRegExp = RegExp(
r'https?://[\w_-]+(?:(?:\.[\w_-]+)+)[\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-]?');
@override
void initState() {
super.initState();
final List<TextSpan> messageSpanList = <TextSpan>[];
widget.message.msg.splitMapJoin(_urlRegExp, onMatch: (Match match) {
_containsUrl = true;
final String url = match.group(0);
messageSpanList.add(TextSpan(
text: url,
style: TextStyle(color: Colors.indigoAccent),
recognizer: TapGestureRecognizer()
..onTap = () =>
router.push('/webView', arguments: <Symbol, String>{#url: url}),
));
return '';
}, onNonMatch: (String nonMatch) {
messageSpanList.add(TextSpan(text: nonMatch));
return '';
});
_buildMessageTextSpan(messageSpanList);
}
void _buildMessageTextSpan(List<TextSpan> messageSpanList) =>
_messageTextSpan = TextSpan(children: messageSpanList);
bool _needRebuildParagraph(
ParagraphStyle style, ParagraphConstraints constraints) {
if (_paragraph == null ||
_paragraphConstraints != constraints ||
_paragraphStyle != style) {
_paragraphStyle = style;
_paragraphConstraints = constraints;
return true;
} else {
return false;
}
}
TextSpan _getSpanByOffset(
Offset offset,
TextStyle textStyle,
BoxConstraints constraints,
) {
if (_needRebuildParagraph(
ParagraphStyle(
fontSize: textStyle.fontSize,
fontFamily: textStyle.fontFamily,
fontWeight: textStyle.fontWeight,
fontStyle: textStyle.fontStyle,
),
ParagraphConstraints(width: constraints.maxWidth),
)) {
final ParagraphBuilder builder = ParagraphBuilder(_paragraphStyle);
_messageTextSpan.build(builder);
_paragraph = builder.build();
_paragraph.layout(_paragraphConstraints);
}
final TextPosition position = _paragraph.getPositionForOffset(offset);
if (position == null) {
return null;
}
return _messageTextSpan.getSpanForPosition(position) as TextSpan;
}
void _checkTap() {
if (_currentTapUrlSpanIndex != null) {
final List<TextSpan> messageSpanList =
List<TextSpan>.from(_messageTextSpan.children);
final TextSpan span = messageSpanList[_currentTapUrlSpanIndex];
messageSpanList[_currentTapUrlSpanIndex] = TextSpan(
text: span.text,
children: span.children,
style: span.style.copyWith(
backgroundColor: Colors.transparent,
),
recognizer: span.recognizer,
semanticsLabel: span.semanticsLabel,
);
_currentTapUrlSpanIndex = null;
setState(() => _buildMessageTextSpan(messageSpanList));
if ((_nowPosition - _startPosition).distanceSquared <= 900 &&
_cancelTapTimer.isActive) {
_cancelTapTimer.cancel();
final TapGestureRecognizer recognizer =
span.recognizer as TapGestureRecognizer;
if (recognizer.onTap != null) {
recognizer.onTap();
}
}
}
}
@override
Widget buildBox(BuildContext context) {
final TextStyle textStyle = TextStyle(
fontSize: 16.sp,
color: Colors.black87,
);
final Widget text = SelectableText.rich(
_messageTextSpan,
style: textStyle,
);
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: isSentByMe
? const Color(AppColor.LoginInputNormalColor)
: Colors.white,
),
child: _containsUrl
? LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) =>
Listener(
onPointerDown: (PointerDownEvent event) {
_startPosition = event.localPosition;
_nowPosition = event.localPosition;
_cancelTapTimer =
Timer(const Duration(milliseconds: 300), _checkTap);
final TextSpan span = _getSpanByOffset(
_startPosition,
textStyle,
constraints,
);
if (span.recognizer is TapGestureRecognizer) {
final List<TextSpan> messageSpanList =
List<TextSpan>.from(_messageTextSpan.children);
_currentTapUrlSpanIndex = messageSpanList.indexOf(span);
if (_currentTapUrlSpanIndex == -1) {
_currentTapUrlSpanIndex = null;
return;
}
setState(() {
messageSpanList[_currentTapUrlSpanIndex] = TextSpan(
text: span.text,
children: span.children,
style: span.style.copyWith(
backgroundColor: Colors.blueAccent.withOpacity(0.6),
),
recognizer: span.recognizer,
semanticsLabel: span.semanticsLabel,
);
_buildMessageTextSpan(messageSpanList);
});
}
},
onPointerMove: (PointerMoveEvent event) =>
_nowPosition = event.localPosition,
onPointerUp: (PointerUpEvent event) => _checkTap(),
child: text,
),
)
: text,
);
}
} |
Somebody please fix this 😭 |
@windrunner414 Is it possible that you give a simpler example? Just like a single TextSpan with a gesture recognizer would be nicer. Hard to know what your workaround is in that piece of code |
class A extends StatefulWidget {
final TextSpan textSpan = TextSpan(children: [... some TextSpan with tapgesturerecognizer]);
createState...
}
class _AState extends State<A> {
Paragraph _paragraph;
ParagraphStyle _paragraphStyle;
ParagraphConstraints _paragraphConstraints;
int _currentTapUrlSpanIndex;
Offset _startPosition;
Offset _nowPosition;
Timer _cancelTapTimer;
bool _needRebuildParagraph(
ParagraphStyle style, ParagraphConstraints constraints) {
if (_paragraph == null ||
_paragraphConstraints != constraints ||
_paragraphStyle != style) {
_paragraphStyle = style;
_paragraphConstraints = constraints;
return true;
} else {
return false;
}
}
TextSpan _getSpanByOffset(
Offset offset,
TextStyle textStyle,
BoxConstraints constraints,
) {
if (_needRebuildParagraph(
ParagraphStyle(
fontSize: textStyle.fontSize,
fontFamily: textStyle.fontFamily,
fontWeight: textStyle.fontWeight,
fontStyle: textStyle.fontStyle,
),
ParagraphConstraints(width: constraints.maxWidth),
)) {
final ParagraphBuilder builder = ParagraphBuilder(_paragraphStyle);
widget.textSpan.build(builder);
_paragraph = builder.build();
_paragraph.layout(_paragraphConstraints);
}
final TextPosition position = _paragraph.getPositionForOffset(offset);
if (position == null) {
return null;
}
return widget.textSpan.getSpanForPosition(position) as TextSpan;
}
void _checkTap() {
if (_currentTapUrlSpanIndex != null) {
_currentTapUrlSpanIndex = null;
if ((_nowPosition - _startPosition).distanceSquared <= 900 &&
_cancelTapTimer.isActive) {
_cancelTapTimer.cancel();
final TapGestureRecognizer recognizer =
span.recognizer as TapGestureRecognizer;
if (recognizer.onTap != null) {
recognizer.onTap();
}
}
}
}
@override
Widget build(BuildContext context) {
final TextStyle textStyle = TextStyle(
fontSize: 16,
color: Colors.black87,
);
final Widget text = SelectableText.rich(
widget.textSpan,
style: textStyle,
);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) =>
Listener(
onPointerDown: (PointerDownEvent event) {
_startPosition = event.localPosition;
_nowPosition = event.localPosition;
_cancelTapTimer =
Timer(const Duration(milliseconds: 300), _checkTap);
final TextSpan span = _getSpanByOffset(
_startPosition,
textStyle,
constraints,
);
if (span.recognizer is TapGestureRecognizer) {
_currentTapUrlSpanIndex = widget.textSpan.children.indexOf(span);
if (_currentTapUrlSpanIndex == -1) {
_currentTapUrlSpanIndex = null;
return;
}
}
},
onPointerMove: (PointerMoveEvent event) =>
_nowPosition = event.localPosition,
onPointerUp: (PointerUpEvent event) => _checkTap(),
child: text,
),
);
}
} it's simplest, maybe. |
Hi, using @windrunner414 fix I made an example project, which works for me with SelectableLinkify: https://github.com/cmaster11/flutter_selectablelinkify_open_workaround |
I have same issue. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This is a blocker for flutter_html Sub6Resources/flutter_html#169 (comment) |
why was this issue closed ? |
@mrifni it was fixed in master branch |
I think it merged to stable branch. flutter 1.20.1 |
This is broken again. |
@SANSKARJAIN2 as for this comment the issue should be fixed on master |
|
Still experiencing this issue (https://github.com/flutter/flutter_markdown/issues/173).
|
@ciriousjoker tagging @pedromassango for visibility |
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of |
I am trying to create a Rich text widget that has clickable links.
Whenever i am using SelectableText.rich to build, the tap recognizers are not getting recognized. But if i am using the norma Text.rich method, then everything seems working fine. Is there a solution by which i can make the text selectable and listen to the click events directly from the TextSpan or InlineSpan elements.
The text was updated successfully, but these errors were encountered: