Skip to content
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

Closed
anjaneyasivan opened this issue Oct 25, 2019 · 25 comments · Fixed by #54479
Closed

SelectableText.rich used along with TapGestureRecognizer is not working #43494

anjaneyasivan opened this issue Oct 25, 2019 · 25 comments · Fixed by #54479
Assignees
Labels
a: quality A truly polished experience a: typography Text rendering, possibly libtxt customer: crowd Affects or could affect many people, though not necessarily a specific customer. f: gestures flutter/packages/flutter/gestures repository. framework flutter/packages/flutter repository. See also f: labels.

Comments

@anjaneyasivan
Copy link

anjaneyasivan commented Oct 25, 2019

I am trying to create a Rich text widget that has clickable links.

import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

class InlineTapableElements extends StatefulWidget {
  @override
  _DemoComponentState createState() => _DemoComponentState();
}

class _DemoComponentState extends State<InlineTapableElements> {

  TapGestureRecognizer _recognizer = TapGestureRecognizer()
  ..onTap = () {
    print('tapped on textspan');
  };

  @override
  Widget build(BuildContext context) {
    return SelectableText.rich(
        TextSpan(
            children: [
              TextSpan(
                text: 'Some text', recognizer: _recognizer)
            ].toList()));
  }

  @override
  void dispose() {
    _recognizer.dispose();
    super.dispose();
  }
}

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.

@iapicca
Copy link
Contributor

iapicca commented Oct 25, 2019

Hi @anjaneyasivan
I tried this code myself
and I can confirm that onTap desn't trigger,
although I think it's a quite understandable conflict.
Thank you

@iapicca iapicca added a: quality A truly polished experience a: typography Text rendering, possibly libtxt f: gestures flutter/packages/flutter/gestures repository. labels Oct 25, 2019
@neithern neithern mentioned this issue Nov 13, 2019
9 tasks
@windrunner414

This comment has been minimized.

@windrunner414
Copy link

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

@VladyslavBondarenko VladyslavBondarenko added the framework flutter/packages/flutter repository. See also f: labels. label Jan 15, 2020
@lukelyyeung
Copy link

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 ?

@Cretezy
Copy link

Cretezy commented Jan 30, 2020

This issue is affecting a flutter_linkify too, unfortunately (Cretezy/flutter_linkify#31).

Is there any update or working examples?

@windrunner414
Copy link

windrunner414 commented Jan 31, 2020

@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,
    );
  }
}

@da-revo
Copy link

da-revo commented Feb 2, 2020

Somebody please fix this 😭

@Cretezy
Copy link

Cretezy commented Feb 2, 2020

@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

@windrunner414
Copy link

windrunner414 commented Feb 3, 2020

@Cretezy

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.
I wrote this by mobile phone

@cmaster11
Copy link

Hi, using @windrunner414 fix I made an example project, which works for me with SelectableLinkify: https://github.com/cmaster11/flutter_selectablelinkify_open_workaround

@onaoura
Copy link

onaoura commented Feb 26, 2020

I have same issue.
Is there any progress for solving this please?

@zhjuncai

This comment has been minimized.

@iapicca iapicca added the customer: crowd Affects or could affect many people, though not necessarily a specific customer. label Feb 29, 2020
@MohammadJomaa

This comment has been minimized.

@raneem-git

This comment has been minimized.

@raneem-almadi

This comment has been minimized.

@stevenspiel
Copy link

This is a blocker for flutter_html Sub6Resources/flutter_html#169 (comment)

@mrifni
Copy link

mrifni commented May 1, 2020

why was this issue closed ?

@chunhtai
Copy link
Contributor

chunhtai commented May 1, 2020

@mrifni it was fixed in master branch

@cuonghoang96
Copy link

I think it merged to stable branch. flutter 1.20.1

@SANSKARJAIN2
Copy link

SANSKARJAIN2 commented Oct 7, 2020

This is broken again.
Gestures are not detected and to have a second check.
I tried @iapicca code provided in the 1st comment and it is not working as expected.

@iapicca
Copy link
Contributor

iapicca commented Oct 7, 2020

@SANSKARJAIN2 as for this comment the issue should be fixed on master
on which channel are you experiencing the issue?

@SANSKARJAIN2
Copy link

@SANSKARJAIN2 as for this comment the issue should be fixed on master
on which channel are you experiencing the issue?
Sorry,
I am working on stable version.
will migrate to master.
Thank you for clearing my issue.

@ciriousjoker
Copy link

Still experiencing this issue (https://github.com/flutter/flutter_markdown/issues/173).

Flutter 1.24.0-10.2.pre • channel dev • https://github.com/flutter/flutter.git
Framework • revision 022b333a08 (3 days ago) • 2020-11-18 11:35:09 -0800
Engine • revision 07c1eed46b
Tools • Dart 2.12.0 (build 2.12.0-29.10.beta)

@iapicca
Copy link
Contributor

iapicca commented Nov 22, 2020

@ciriousjoker
can you provide a minimal reproducible code sample?

tagging @pedromassango for visibility

@github-actions
Copy link

github-actions bot commented Aug 9, 2021

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 flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 9, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
a: quality A truly polished experience a: typography Text rendering, possibly libtxt customer: crowd Affects or could affect many people, though not necessarily a specific customer. f: gestures flutter/packages/flutter/gestures repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging a pull request may close this issue.