最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

flutter - How can I scroll to a substring in a TextField - Stack Overflow

programmeradmin0浏览0评论

My textfield has a lot of text and is scrollable. I want to scroll to a specific substring or index (marked with red) when I click on a button. How can I achieve this?

I have tried following solution:

void scrollToWord(String word) {
  final text = _controller.text;
  
  // Find the position of the word in the text
  int index = text.indexOf(word);
  if (index == -1) {
    // Word not found, return early
    return;
  }

  // Move the cursor to the start of the word
  _controller.selection = TextSelection.fromPosition(TextPosition(offset: index));

  // Calculate the scroll position (find position of word's start)
  final textPainter = TextPainter(
    text: TextSpan(text: text),
    textDirection: TextDirection.ltr,
  );
  textPainter.layout(maxWidth: MediaQuery.of(context).size.width);
  
  // Calculate the word's start position on screen
  final wordStartOffset = textPainter.getOffsetForCaret(TextPosition(offset: index));
  final wordHeight = textPainter.size.height;

  // Scroll to the word's position
  _scrollController.animateTo(
    wordStartOffset.dy, // Scroll position
    duration: Duration(milliseconds: 300),
    curve: Curves.easeInOut,
  );
}

But this does not work, as the word startOffset is way off somehow.

My textfield has a lot of text and is scrollable. I want to scroll to a specific substring or index (marked with red) when I click on a button. How can I achieve this?

I have tried following solution:

void scrollToWord(String word) {
  final text = _controller.text;
  
  // Find the position of the word in the text
  int index = text.indexOf(word);
  if (index == -1) {
    // Word not found, return early
    return;
  }

  // Move the cursor to the start of the word
  _controller.selection = TextSelection.fromPosition(TextPosition(offset: index));

  // Calculate the scroll position (find position of word's start)
  final textPainter = TextPainter(
    text: TextSpan(text: text),
    textDirection: TextDirection.ltr,
  );
  textPainter.layout(maxWidth: MediaQuery.of(context).size.width);
  
  // Calculate the word's start position on screen
  final wordStartOffset = textPainter.getOffsetForCaret(TextPosition(offset: index));
  final wordHeight = textPainter.size.height;

  // Scroll to the word's position
  _scrollController.animateTo(
    wordStartOffset.dy, // Scroll position
    duration: Duration(milliseconds: 300),
    curve: Curves.easeInOut,
  );
}

But this does not work, as the word startOffset is way off somehow.

Share Improve this question edited Feb 6 at 16:03 Frank van Puffelen 599k85 gold badges888 silver badges858 bronze badges asked Feb 6 at 14:20 LukasLukas 8492 gold badges15 silver badges46 bronze badges 2
  • How you rendered your text string ? using segments as List<String> or just a single large text? – Aks Commented Feb 7 at 7:17
  • Its just a single large String – Lukas Commented Feb 7 at 20:34
Add a comment  | 

1 Answer 1

Reset to default 0

After some research, I found a solution to this problem.

Let's divide it into parts to be clear:

Problem Statement:

You want to:

  1. Take a word as input from the user (or from any other resource).
  2. Find this word inside the text stored in a TextField's controller.text.
  3. Get the position (offset) of this word within the TextField.
  4. Scroll to that position (offset) so the word becomes in visible scope.

To achieve this, you need two main controllers:

ScrollController: To control the scrolling behavior of the TextField.

TextEditingController: To manage the text and cursor position inside the TextField.

1. The Programmatical Part

Defining and initializing the Controllers:

 // Controller that will catch the target Word from the User
 // Also will be used to control the cursor position
 late final TextEditingController _searchController;

 // The Target contrller that we will scroll from it 
 late final TextEditingController _targetController;

 // The ScrollController of the TextField Widget
 late final ScrollController _scrollController;


// Initialize the Controllers in the initState
 @override
 void initState() {
   _searchController = TextEditingController();
   _targetController = TextEditingController();

   _scrollController = ScrollController();
   super.initState();
 }

 @override
 void dispose() {
   _searchController.dispose();
   _targetController.dispose();
   _scrollController.dispose();
   super.dispose();
 }

Then You Create the method that will control the Scroll:

 void get _scrollToSearchedText {
   log("Start to Scroll .....");

   final String contentText = _targetController.text;

   final String searchText = _searchController.text;

   final int indexOfTextinContent = contentText.indexOf(searchText);

   if (indexOfTextinContent != -1 || contentText.isNotEmpty) {
     log("trying ....");
     // ensure that all frames has been build and there are no Widgets need to bind
     WidgetsBinding.instance.addPostFrameCallback(
       (_) {
         final textPainter = TextPainter(
           text: TextSpan(
             text: contentText.substring(0, indexOfTextinContent),
             style: const TextStyle(fontSize: 16),
           ),
           textDirection: TextDirection.ltr,
         );

         // Layout the text to calculate the size
         // Computes the visual position of the glyphs for painting the text.
         textPainter.layout();

         // Calculate the scroll offset based on the text width and height

         final double scrollOffset = textPainter.size.height *
             (textPainter.size.width /
                 _scrollController.position.viewportDimension);

         // Scroll to the calculated offset

         _scrollController.jumpTo(scrollOffset.clamp(
             0.0, _scrollController.position.maxScrollExtent));

      
         // Move the cursor to the start of the searched word
         _targetController.selection = TextSelection.collapsed(
           offset: indexOfTextinContent,
         );
       },
     );
   }
 }

2. The UI Part

- Create the TextFielWidget Component

class CustomTextFielWidget extends StatelessWidget {
  const CustomTextFielWidget({
    super.key,
    required this.controller,
    required this.hintText,
    this.isTargetField = false,
    this.scrollController,
  });

  final TextEditingController controller;
  final String hintText;
  final bool isTargetField;

  final ScrollController? scrollController;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: isTargetField ? context.screenHeight * .5 : null, <---- Height is must
      padding: const EdgeInsets.all(10),
      child: TextField(
        controller: controller,
        scrollController: scrollController,
        scrollPhysics: const AlwaysScrollableScrollPhysics(),
        maxLines: isTargetField ? null : 1,
        style: TextStyle(
          fontSize: isTargetField ? 20 : 18,
          fontWeight: FontWeight.bold,
          fontFamily: isTargetField ? FontFamily.verlaFont : null,
        ),
        decoration: InputDecoration(
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(10),
            borderSide: BorderSide(
              color: Colors.grey.withOpacity(0.6),
              width: 1.3,
            ),
          ),
          hintText: hintText,
          fillColor: Colors.grey.withOpacity(0.3),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(10),
            borderSide: BorderSide(
              color: Colors.grey.withOpacity(0.6),
              width: 2.0,
            ),
          ),
        ),
      ),
    );
  }
}

You must define a height to a the Field or give it a Constrains to prevent it from Expansion

- Create the Button Widget Component

class ScrollButtonWidget extends StatelessWidget {
  const ScrollButtonWidget({
    super.key,
    required this.onScrollTap,
  });
  final void Function() onScrollTap;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: context.screenWidth * .7,
      height: context.screenHeight * .07,
      child: MaterialButton(
        onPressed: onScrollTap,
        color: Colors.blue,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20),
        ),
        child: const Text("Scroll To Target Word"),
      ),
    );
  }
}

Finally this is the Full Code

import 'dart:developer';

import 'package:flutter/material.dart';

class ScrollToTextWidget extends StatefulWidget {
  const ScrollToTextWidget({super.key});

  @override
  State<ScrollToTextWidget> createState() => _ScrollToTextWidgetState();
}

class _ScrollToTextWidgetState extends State<ScrollToTextWidget> {
  // Controller that will catch the target Word from the User
  late final TextEditingController _searchController;

  // The Target contrller that we will scroll from it 
  late final TextEditingController _targetController;

  // The ScrollController of the TextField Widget
  late final ScrollController _scrollController;

  @override
  void initState() {
    _searchController = TextEditingController();
    _targetController = TextEditingController();

    _scrollController = ScrollController();
    super.initState();
  }

  @override
  void dispose() {
    _searchController.dispose();
    _targetController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  void get _scrollToSearchedText {
    log("Start to Scroll .....");

    final String contentText = _targetController.text;

    final String searchText = _searchController.text;

    final int indexOfTextinContent = contentText.indexOf(searchText);

    if (indexOfTextinContent != -1 || contentText.isNotEmpty) {
      log("trying ....");
      WidgetsBinding.instance.addPostFrameCallback(
        (_) {
          final TextPainter textPainter = TextPainter(
            text: TextSpan(
              text: contentText.substring(0, indexOfTextinContent),
              style: const TextStyle(fontSize: 16),
            ),
            textDirection: TextDirection.ltr,
          );

          // Layout the text to calculate the size
          // Computes the visual position of the glyphs for painting the text.
          textPainter.layout();

          // Calculate the scroll offset based on the text width and height

          final double scrollOffset = textPainter.size.height *
              (textPainter.size.width /
                  _scrollController.position.viewportDimension);

          // Scroll to the calculated offset

          _scrollController.jumpTo(scrollOffset.clamp(
              0.0, _scrollController.position.maxScrollExtent));

          
          // Move the cursor to the start of the searched word
          _targetController.selection = TextSelection.collapsed(
            offset: indexOfTextinContent,
          );
        },
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Scroll To Text"),
        automaticallyImplyLeading: false,
        backgroundColor: Colors.green,
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: SingleChildScrollView(
              child: Column(
                children: <Widget>[
                  gapH1,
                  CustomTextFielWidget(
                    controller: _searchController,
                    hintText: "Enter Target Text",
                  ),
                  gapH1,
                  CustomTextFielWidget(
                    controller: _targetController,
                    hintText: "Content Text",
                    scrollController: _scrollController,
                    isTargetField: true,
                  ),
                  gapH2,
                ],
              ),
            ),
          ),
          ScrollButtonWidget(
            onScrollTap: () {
              _scrollToSearchedText;
            },
          )
        ],
      ),
    );
  }
}

class CustomTextFielWidget extends StatelessWidget {
  const CustomTextFielWidget({
    super.key,
    required this.controller,
    required this.hintText,
    this.isTargetField = false,
    this.scrollController,
  });

  final TextEditingController controller;
  final String hintText;
  final bool isTargetField;

  final ScrollController? scrollController;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: isTargetField ? context.screenHeight * .5 : null,
      padding: const EdgeInsets.all(10),
      child: TextField(
        controller: controller,
        scrollController: scrollController,
        scrollPhysics: const AlwaysScrollableScrollPhysics(),
        maxLines: isTargetField ? null : 1,
        style: TextStyle(
          fontSize: isTargetField ? 20 : 18,
          fontWeight: FontWeight.bold,
          fontFamily: isTargetField ? FontFamily.verlaFont : null,
        ),
        decoration: InputDecoration(
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(10),
            borderSide: BorderSide(
              color: Colors.grey.withOpacity(0.6),
              width: 1.3,
            ),
          ),
          hintText: hintText,
          fillColor: Colors.grey.withOpacity(0.3),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(10),
            borderSide: BorderSide(
              color: Colors.grey.withOpacity(0.6),
              width: 2.0,
            ),
          ),
        ),
      ),
    );
  }
}

class ScrollButtonWidget extends StatelessWidget {
  const ScrollButtonWidget({
    super.key,
    required this.onScrollTap,
  });
  final void Function() onScrollTap;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: context.screenWidth * .7,
      height: context.screenHeight * .07,
      child: MaterialButton(
        onPressed: onScrollTap,
        color: Colors.blue,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20),
        ),
        child: const Text("Scroll To Target Word"),
      ),
    );
  }
}

You can ignore some part of code like gapH1, screenWidth or screenHeight it's a properties I created

Also....

there is some note about the cursor , if you want the cursor be at the beginning of the word you the target textfield must be focus , you can handle this simply by wrapping the TextField with FocusScope Widget and control the focus form it when the user click on the button

发布评论

评论列表(0)

  1. 暂无评论