7

Need to know the dx and dy coordinates of the current cursor position in the TextField. This is required to implement the mentions/tag functionality, wherein a popup needs to be shown a few pixel below the cursor of the TextField.

Anirudh Agarwal
  • 2,044
  • 2
  • 14
  • 8

2 Answers2

8

You can use the FocusNode to gain the offset of the text field itself.Then use the TextPainter class to calculated the layout width as shown in this post and use it to position your tag. Then perhaps use some overlay logic to show the tag as shown here.

  1. Create a FocusNode object and attach it to the text field.
  2. Then either in onChanged callback or its TextEditingController's call back proceed with the logic to position your tag using the FocusNode.offset.dx and FocusNode.offset.dy.
  3. FocusNode only provides the bounding rect offset. So you will need a TextPainter instance to calculate the width of the newly entered text. for this you will need TextStyle defined up ahead.
  4. Using both the values from 2 and 3 calculate the position of your tag with some extra offset for visual aesthetics.

Following code is a sample using the above techniques. A live version of this solution is available in this dartpad.

// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. 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/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter  Show Text Tag Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Show Text Tag demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  FocusNode _focusNode = FocusNode();
  GlobalKey _textFieldKey = GlobalKey();
  TextStyle _textFieldStyle = TextStyle(fontSize: 20);

  @override
  void initState() {
    super.initState();
  }

  // Code reference for overlay logic from MTECHVIRAL's video
  // https://www.youtube.com/watch?v=KuXKwjv2gTY

  showOverlaidTag(BuildContext context, String newText) async {

    TextPainter painter = TextPainter(
      textDirection: TextDirection.ltr,
      text: TextSpan(
        style: _textFieldStyle,
        text: newText,
      ),
    );
    painter.layout();


    OverlayState overlayState = Overlay.of(context);
    OverlayEntry suggestionTagoverlayEntry = OverlayEntry(builder: (context) {
      return Positioned(

        // Decides where to place the tag on the screen.
        top: _focusNode.offset.dy + painter.height + 3,
        left: _focusNode.offset.dx + painter.width + 10,

        // Tag code.
        child: Material(
            elevation: 4.0,
            color: Colors.lightBlueAccent,          
            child: Text(
              'Show tag here',
              style: TextStyle(
                fontSize: 20.0,
              ),
            )),
      );
    });
    overlayState.insert(suggestionTagoverlayEntry);

    // Removes the over lay entry from the Overly after 500 milliseconds 
    await Future.delayed(Duration(milliseconds: 500));
    suggestionTagoverlayEntry.remove();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          child: TextField(
            focusNode: _focusNode,
            key: _textFieldKey,
            style: _textFieldStyle,
            onChanged: (String nextText) {
              showOverlaidTag(context, nextText);
            },
          ),
          width: 400.0,
        ),
      ),
    );
  }
}

A screen shot of how this looks like is shown below.You will have to adjust the position to suit your needs and also the duration / visibility logic of the overlay if you are going to use it.

enter image description here

Abhilash Chandran
  • 6,803
  • 3
  • 32
  • 50
  • could you expand your answer to work with multiline textfields? – Alb Dec 06 '21 at 10:08
  • The problem is where you don't press Enter then and write multiline and line break down to for example 5 lines the `painter.heigh` remain like height of 1 line. https://stackoverflow.com/questions/70748383/flutter-calculate-height-of-text-line-inside-text-filed @Abhilash Chandran – Cyrus the Great Jan 18 '22 at 06:21
  • This also only works if the caret is at the end of the text input. If you move the caret somewhere else, the overlay will still show at the end of the line. The correct answer is the one below: https://stackoverflow.com/a/68165106/3466729 – Gpack Aug 16 '23 at 02:01
6

To get the coordinates of the current cursor (also called caret) in a Textfield in flutter, I think you can use TextPainter > getOffsetForCaret method which return the offset at which to paint the caret. Then, from the offset you can get the The x and y component of the caret.

Observe the xCarret, yCarret in the code below which correspond to the top left coordinate of the cursor on the screen. You can deduce the yCarretBottom position by adding the preferredLineHeight to yCarret.

The method getOffsetForCaret need a caretPrototype which we made with Rect.fromLTWH and the width of the cursor given by the property cursorWidth of the TextField.

Flutter caret example


import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Get cursor (caret) position',
      debugShowCheckedModeBanner: false,
      home: MyHomePage(title: 'Get cursor (caret) position'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, this.title}) : super(key: key);

  final String? title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  GlobalKey _textFieldKey = GlobalKey();
  TextStyle _textFieldStyle = TextStyle(fontSize: 20);
  TextEditingController _textFieldController = TextEditingController();
  late TextField _textField;
  double xCaret = 0.0;
  double yCaret = 0.0;
  double painterWidth = 0.0;
  double painterHeight = 0.0;
  double preferredLineHeight = 0.0;

  @override
  void initState() {
    super.initState();

    /// Listen changes on your text field controller
    _textFieldController.addListener(() {
      _updateCaretOffset(_textFieldController.text);
    });
  }

  void _updateCaretOffset(String text) {
    TextPainter painter = TextPainter(
      textDirection: TextDirection.ltr,
      text: TextSpan(
        style: _textFieldStyle,
        text: text,
      ),
    );
    painter.layout();

    TextPosition cursorTextPosition = _textFieldController.selection.base;
    Rect caretPrototype = Rect.fromLTWH(
        0.0, 0.0, _textField.cursorWidth, _textField.cursorHeight ?? 0);
    Offset caretOffset =
        painter.getOffsetForCaret(cursorTextPosition, caretPrototype);
    setState(() {
      xCaret = caretOffset.dx;
      yCaret = caretOffset.dy;
      painterWidth = painter.width;
      painterHeight = painter.height;
      preferredLineHeight = painter.preferredLineHeight;
    });
  }

  @override
  Widget build(BuildContext context) {
    String text = '''
xCaret: $xCaret
yCaret: $yCaret
yCaretBottom: ${yCaret + preferredLineHeight}
''';

    _textField = TextField(
      controller: _textFieldController,
      keyboardType: TextInputType.multiline,
      key: _textFieldKey,
      style: _textFieldStyle,
      minLines: 1,
      maxLines: 2,
    );

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(text),
            Padding(
              child: _textField,
              padding: EdgeInsets.all(40),
            ),
          ]),
    );
  }
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
Adrien Arcuri
  • 1,962
  • 1
  • 16
  • 30
  • I implemented this and placed an `Icon` inside a `Positioned` inside a `Stack` to display something at the position of the cursor. Although your answer is the best performing answer so far, it is a bit flawed. After inserting more text, the `Icon` will be moving further right than the cursor is. – Alb Dec 01 '21 at 16:08
  • The problem is where you don't press Enter then and write multiline and line break down to for example 5 lines the painter.heigh remain like height of 1 line. @Adrien Arcuri – Cyrus the Great Jan 18 '22 at 06:22