Lawrence
Lawrence

Reputation: 11

How can I conditionally change text color in a Tile during audio playback

I am learning Dart and making an app where you should hear an audio playback onTap on a piece of text (Tile). While playback is on, if another Tile is tapped, I want the color of the previous unfinished recording to return to default, and the new playback tile to change.

This is my app at the moment:

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


List<String> titles = <String>[
  'class1',
  'class2',
  'class3',
];

List<String> sentences = <String>[
  'sentence1',
  'sentence2',
  'sentence3',
  'sentence4'
];

List<String> audio = <String>[
  '1.mp3',
  '2.mp3',
  '3.mp3',
  '4.mp3'
];


void main() => runApp(const AppBarApp());

class AppBarApp extends StatelessWidget {
  const AppBarApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
          colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
      home: const AppBarExample(),
    );
  }
}

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

  @override
  State<AppBarExample> createState() => _AppBarExampleState();

}

class _AppBarExampleState extends State<AppBarExample> {
  @override
  Widget build(BuildContext context) {
    ...//styling details ...
    bool? isCurrentlyPlaying;

    return DefaultTabController(
      ...//tabbar setup details ...
          children: <Widget>[
            ListView.builder(
              itemCount: sentences.length,
              itemBuilder:(BuildContext context, int index) {
                return ListTile(
                  tileColor: index.isOdd
                   ? oddItemColor : evenItemColor,
                  title: AudioPlayerWidget(index: index,
                  isCurrentlyPlaying: false,
                  onPlay: () {setState (() {isCurrentlyPlaying = true;});},
                  ),
                );
              },
            ),
            ...//other items in the ListView ...
              },
            ),
          ],
        ),
      ),
    );
  }
}

// AudioPlayer 
final audioPlayer = AudioPlayer();

class AudioPlayerWidget extends StatefulWidget {
  final int index;
  final VoidCallback onPlay;
  final bool isCurrentlyPlaying;
  AudioPlayerWidget ({Key ? key, required this.index, required this.onPlay, required this.isCurrentlyPlaying}) : super(key: key);

  @override 
  _AudioPlayerWidgetState createState() => _AudioPlayerWidgetState();

}

class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
  bool isPlaying = false;

  void _playAudio() {
    audioPlayer.stop();
    audioPlayer.play(AssetSource('audio/${widget.index+1}.mp3'));
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        widget.onPlay();
        _playAudio();
        },
        child: Text(sentences[widget.index],
        style: TextStyle(
          color: widget.isCurrentlyPlaying ? Colors.indigo.shade900 : Colors.black,
        )
        ),
      );
  }
}

The logic is as follows:

My issue is that onPlay() does not get triggered as expected, so the color remains black (default), and I am unable to diagnose why.

I previously successfully implemented alternative logic where colors change as expected if audio is allowed to play until the end. It is the interrupt logic, where I effectively want to have a value of "iscurrentlyplaying" assigned to each tile, that does not work.

Upvotes: 0

Views: 69

Answers (2)

Lawrence
Lawrence

Reputation: 11

After a day or so of hitting my head against the wall I found a way to achieve what I needed. In my original code I think I was confusing the state of AudioPlayer widget and the ListView widget. As a result, I was defining currentlyPlaying twice, overwriting with false each time.

Posting my solution here:

  1. Define currentlyPlayingIndex var to track which widget is playing
final bool isCurrentlyPlaying;
  1. Define a onPlay as well as onStop callback functions This allows for changing text color once playback is completed. It is also the function that is called in case of interrupting the original playback by tapping on another element in the UI. This way the state which controls font color can be turned both on and off easily.
onPlay: () {
  setState(() {
    isCurrentlyPlaying = true;
    currentlyPlayingIndex = index;
  });
},
onStop: () {
  setState(() {
    isCurrentlyPlaying = false;
    currentlyPlayingIndex = null;
  });
},

  1. Use "and" to make sure isCurrentlyPlaying is only true when the tapped widget is playing, and automatically false otherwise
isCurrentlyPlaying: isCurrentlyPlaying && currentlyPlayingIndex == index
  1. In _playAudio function, use onPlay rather than widget.onPlay, and make use of onStop as well
void _playAudio() {
 audioPlayer.stop();
 onPlay();
 audioPlayer.play(AssetSource('audio/${index + 1}.mp3'));
 audioPlayer.onPlayerComplete.listen((event) {
  onStop();
});

Full code for reference

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

List<String> titles = <String>[
  'class1',
  'class2',
  'class3',
];

List<String> sentences = <String>[
  'sentence1',
  'sentence2',
  'sentence3',
  'sentence4'
];

List<String> audio = <String>[
  '1.mp3',
  '2.mp3',
  '3.mp3',
  '4.mp3'
];

void main() => runApp(const AppBarApp());

class AppBarApp extends StatelessWidget {
  const AppBarApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
          colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
      home: const AppBarExample(),
    );
  }
}

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

  @override
  State<AppBarExample> createState() => _AppBarExampleState();
}

class _AppBarExampleState extends State<AppBarExample> {
  // >> These vars control playback <<
  bool isCurrentlyPlaying = false;
  int? currentlyPlayingIndex;

  @override
  Widget build(BuildContext context) {
    //...styling goes here

    return DefaultTabController(
      //... styling etc.
      child: Scaffold(
        appBar: AppBar(
          title: const Text('My app'),
          //...styling etc.
        body: TabBarView(
          children: <Widget>[
            ListView.builder(
              itemCount: sentences.length,
             // >> Key widget playback logic <<
              itemBuilder: (BuildContext context, int index) {
                return ListTile(
                  tileColor: index.isOdd ? oddItemColor : evenItemColor,
                  title: AudioPlayerWidget(
                    index: index,
                    onPlay: () {
                      setState(() {
                        isCurrentlyPlaying = true;
                        currentlyPlayingIndex = index;
                      });
                    },
                    onStop: () {
                      setState(() {
                        isCurrentlyPlaying = false;
                        currentlyPlayingIndex = null;
                      });
                    },
                    isCurrentlyPlaying: isCurrentlyPlaying && currentlyPlayingIndex == index,
                  ),
                );
              },
            ),
            //...other ListView items (tiles)

// >> audioplayer definitions <<
final audioPlayer = AudioPlayer();

class AudioPlayerWidget extends StatelessWidget {
  final int index;
  final VoidCallback onPlay;
  final VoidCallback onStop;
  final bool isCurrentlyPlaying;
  AudioPlayerWidget({Key? key, required this.index, required this.onPlay, required this.onStop, required this.isCurrentlyPlaying}) : super(key: key);

  void _playAudio() {
    // interrupt upon tap
    audioPlayer.stop();
    onPlay();
    audioPlayer.play(AssetSource('audio/${index + 1}.mp3'));
    // return to original color once playback finished
    audioPlayer.onPlayerComplete.listen((event) {
      onStop();
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        // >>conditionally kill the playback, and use `onStop` to update state and conditionally recolour font to default<<
        if (isCurrentlyPlaying) {
          audioPlayer.stop();
          onStop();
        } else {
          _playAudio();
        }
      },
      child: Text(
        sentences[index],
        style: >> font conditional coloring <<
          color: isCurrentlyPlaying ? Colors.orange : Colors.blue,
        ),
      ),
    );
  }
}

I hope this helps somebody :)

Upvotes: 1

Ivo
Ivo

Reputation: 23357

In this part of the code:

                return ListTile(
                  tileColor: index.isOdd
                   ? oddItemColor : evenItemColor,
                  title: AudioPlayerWidget(index: index,
                  isCurrentlyPlaying: false,
                  onPlay: () {setState (() {isCurrentlyPlaying = true;});},
                  ),
                );

you hardcoded isCurrentlyPlaying: false. This should be isCurrentlyPlaying: isCurrentlyPlaying.

You should also change

bool? isCurrentlyPlaying;

to

bool isCurrentlyPlaying = false;

so that it is false by default

Upvotes: 0

Related Questions