Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter - How to get the coordinates of the cursor in a TextField?

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.

like image 643
Anirudh Agarwal Avatar asked Dec 09 '19 06:12

Anirudh Agarwal


People also ask

How do you change the cursor in TextField Flutter?

To change TextField cursor color in Flutter, simply add the cursorColor property and set the color of your choice.

How do you find the focus on a TextField Flutter?

To listen to focus change, you can add a listner to the FocusNode and specify the focusNode to TextField . This gist represents how to ensure a focused node to be visible on the ui. Hope it helps!

What is TextEditingController Flutter?

TextEditingController class Null safety. A controller for an editable text field. Whenever the user modifies a text field with an associated TextEditingController, the text field updates value and the controller notifies its listeners.


2 Answers

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

like image 165
Abhilash Chandran Avatar answered Sep 23 '22 22:09

Abhilash Chandran


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),
            ),
          ]),
    );
  }
}
like image 20
Adrien Arcuri Avatar answered Sep 23 '22 22:09

Adrien Arcuri