How can I Scroll to the first occurrence of a given string in a Text Widget

Issue

say I have a song lyric app and there is just one Scaffold with a Text widget that displays the entire lyric and the lyrics are written in the format

  1. ….

Chorus:

  1. ….

  2. ….

and I have a FAB, onClick of which I need the text to auto scroll to the text "Chorus:", this text is literally in every song, but when the verses are a about 4+, they usually go off screen, so, user usually has to manually scroll to the chorus again after each verse that’s beyond the screen height, but I need this to be done automatically at the tap of a button

scroll up till the string "chorus" is in view, how would I do this in flutter

TEXT

const kTheAstronomers = '''1. Yeah, you woke up in London
    At least that's what you said
    I sat here scrollin'
    And I saw you with your aunt
    A demon on your left
    An angel on your right
    But you're hypocritical
    Always political
    
    Chorus:
    Say you mean one thing
    But you go ahead and lie
    Oh, you lie-lie, lie-lie
    And you say you're not the bad type
    
    2. Oh, you posted on Twitter
    Said you had other plans
    But your mother, she called me
    Said, "Come have lunch with the fam"
    Guess you didn't tell her that
    You should've called me back
    I guess you changed your number or somethin\' '''

LYRIC SCREEN

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBody: true,
      body: SafeArea(
        child: SingleChildScrollView(
          physics: const BouncingScrollPhysics(),
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10),
            child: Text(
              kTheAstronomers,
              style: const TextStyle(
                fontSize: 30,
                fontFamily: 'Montserrat',
                fontWeight: FontWeight.w600,
              ),
            ),
          ),
        ),
      )
      floatingActionButton: FAB(onPressed: autoScrollToChorus),

,

Solution

I found a way.

I had to change the way I displayed the text, instead of using one text widget, I used a ListView builder to display two texts, but before that, in initState, when my page receives the text, I split the text into a list of two separate texts, one containing the first part and the other containing from the Chorus down, then I give this list to the listview builder (you could also just use a column and create two separate widgets and just pass the scroll key to the second text, knowing it’s the second part of the text)

  final GlobalKey _key = GlobalKey();

  void _autoScrollToChorus() async {
    BuildContext context = _key.currentContext!;
    await Scrollable.ensureVisible(context);
  }
  
  late List<String> lyricList;

  @override
  initState() {
    lyricList =
        kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ListView.builder(
            itemCount: lyricList.length,
            itemBuilder: (context, idx) {
              return Text(
                key: idx == 1 ? _key : null,
                lyricList[idx],
                style: const TextStyle(
                  fontWeight: FontWeight.w600,
                  fontSize: 30,
                ),
              );
            }),
      ),
      floatingActionButton: lyricList.length > 1 ? FloatingActionButton(
        onPressed: _autoScrollToChorus,
        child: const Padding(
          padding: EdgeInsets.all(8.0),
          child: Text("Chorus"),
        ),
      ) : null,
    );
  }

Thanks to @Priyaank I knew to use the key and scroll to a particular widget

a more advanced solution that makes it possible to hide the button when the chorus is in view USING THE SCROLLABLE_POSITIONED_LIST PACKAGE

  final GlobalKey _key = GlobalKey();
  final ItemScrollController _itemScrollController = ItemScrollController();
  final ItemPositionsListener _itemListener = ItemPositionsListener.create();
  late List<String> lyricList;
  bool chorusIsVisible = true;

  void _autoScrollToChorus() {
    // BuildContext context = _key.currentContext!;
    // await Scrollable.ensureVisible(context);
    _itemScrollController.scrollTo(
        index: 1,
        duration: const Duration(milliseconds: 500),
        alignment: 0.5
    );
  }

  @override
  initState() {
    lyricList =
        kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
    super.initState();
    if(lyricList.length > 1) {
      _itemListener.itemPositions.addListener(() {
        chorusIsVisible = _itemListener.itemPositions.value.where((item) {
          final isTopVisible = item.itemLeadingEdge >= 0;
          return isTopVisible;
        }
        ).map((item) => item.index).toList().contains(1);
        setState(() {});
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ScrollablePositionedList.builder(
          itemScrollController: _itemScrollController,
            itemPositionsListener: _itemListener,
            itemCount: lyricList.length,
            itemBuilder: (context, idx) {
              return Text(
                lyricList[idx],
                style: const TextStyle(
                  fontWeight: FontWeight.w600,
                  fontSize: 30,
                ),
              );
            }),
      ),
      floatingActionButton: lyricList.length > 1 && !chorusIsVisible ? FloatingActionButton(
        onPressed: _autoScrollToChorus,
        child: const Padding(
          padding: EdgeInsets.all(8.0),
          child: Text("Chorus"),
        ),
      ) : null,
    );
  }
}

Answered By – public static void Main

Answer Checked By – Senaida (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *