问题
I'm trying to use TextToSpeech in my app.
I wrote it as a bound service. Problem appears when I need to read text before activity stops. It says just half of text because activity calls unbind and finish itself.
What is better pattern to write Text to speech if I dont want write it direct into actvivity?
回答1:
Instead of just bind service, start your service using startService()
and then bind. After reading the text in onUtteranceCompleted()
call stopSelf()
.
回答2:
Here is my implementation - as inspired by Hoan's answer (and added many years later but hopefully useful to someone else to save them having to work it out as I did.
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Queue;
import timber.log.Timber;
/**
* This is a simplified text to speech service where each "instance" (each time startService is
* called) is only expected to speak once (we don't check this but the service gets stopped after it
* has spoken once). This was created for the case where we want an announcement to be made just
* before finishing an activity so couldn't do it in the activity itself because it gets destroyed
* before it finishes speaking.
*
* <p>Usage (in the activity that "speaks"):
* <li>Check there is text to speech data available on the device. See:
* https://android-developers.googleblog.com/2009/09/introduction-to-text-to-speech-in.html
* <li>Call startService after successfully checking for speech data
* <li>Bind the activity to the service onStart (and unbind onStop). Getting an instance of the service to later pass
* the text to speak though to.
* <li> call `.speak(textToSpeak)` when ready to speak (after which we cannot call `.speak()` again)
*
* <p>NB: We assume that the activity that started this service checks that TTS data is
* available.
* <p>NB: You need to start the service a few seconds before asking it to speak as the text to speech api doesn't
* start immediately.
*/
public class TTSSingleUtteranceService extends Service {
private boolean initDone = false;
private TextToSpeech textToSpeech;
Queue<Integer> startIds = new LinkedList<>();
// Binder given to clients
private final IBinder binder = new TTSBinder();
/**
* Class used for the client Binder. Because we know this service always runs in the same process
* as its clients, we don't need to deal with IPC.
*/
public class TTSBinder extends Binder {
public TTSSingleUtteranceService getService() {
// Return this instance of TTSService so clients can call public methods
return TTSSingleUtteranceService.this;
}
}
@Override
public void onCreate() {
super.onCreate();
// Initialise the TextToSpeech object
textToSpeech =
new TextToSpeech(
this,
status -> {
if (status == TextToSpeech.SUCCESS) {
initDone = true;
Locale locale = Locale.getDefault();
int ttsLang = textToSpeech.setLanguage(locale);
if (ttsLang == TextToSpeech.LANG_MISSING_DATA
|| ttsLang == TextToSpeech.LANG_NOT_SUPPORTED) {
Timber.e("TTS: Language %s is not supported!", locale);
}
textToSpeech.setOnUtteranceProgressListener(
new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Timber.i("Utterance started");
}
@Override
public void onDone(String utteranceId) {
Timber.i("Utterance completed. Shutting down service");
if (!startIds.isEmpty()) {
stopSelf(startIds.remove());
} else {
stopSelf();
}
}
@Override
public void onError(String utteranceId) {
Timber.i("Utterance errored");
}
});
Timber.i("TTS: Initialization success with locale=%s.", locale);
} else {
Timber.e("Text to speech initialisation failed");
}
});
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// add this startId to the queue of ids so that we can shutdown each one in order
startIds.add(startId);
// If we get killed, after returning from here, do not recreate the service unless there are
// pending intents to deliver.
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onDestroy() {
Timber.i("Destroying TTS service");
if (textToSpeech != null) {
textToSpeech.shutdown();
}
super.onDestroy();
}
/** method for clients */
public void speak(String text) {
Timber.i("speak called with text=%s", text);
if (text != null && initDone) {
String utteranceId = String.valueOf(text.hashCode());
textToSpeech.speak(text, TextToSpeech.QUEUE_ADD, null, utteranceId);
} else {
Timber.e("TTSError: textToSpeech is null or hasn't finished initialising!");
}
}
}
I then use this from the Android Activity like so
public class MyActivity extends AppCompatActivity {
TTSSingleUtteranceService ttsService;
boolean boundToTtsService = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check there is text to speech data available on the device and then will start the text to
// speech service in onActivityResult
Intent checkIntent = new Intent();
checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
startActivityForResult(checkIntent, TTS_CHECK_DATA_AVAILABLE);
}
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, TTSSingleUtteranceService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
unbindService(connection);
boundToTtsService = false;
}
/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection connection =
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
// We've bound to TTSService, cast the IBinder and get the TTSService instance
TTSSingleUtteranceService.TTSBinder binder = (TTSSingleUtteranceService.TTSBinder) service;
ttsService = binder.getService();
boundToTtsService = true;
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
boundToTtsService = false;
}
};
public void saveClicked() {
// this is called to save the data entered and then speak the results and finish the activity
// perform needed logic
// Speak text
if (boundToTtsService) {
ttsService.speak("This is the text that will be spoken");
}
finish();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == TTS_CHECK_DATA_AVAILABLE) {
if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
// success, start the TTS service
startService(new Intent(this, TTSSingleUtteranceService.class));
} else {
// missing data, install it
Timber.i("TTS: Missing text to speech resources - redirecting user to install them.");
Intent installIntent = new Intent();
installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
startActivity(installIntent);
}
}
}
}
Finally don't forget to add the service to your AndroidManifest.xml
(within the application section)
<service android:name=".util.TTSSingleUtteranceService" />
来源:https://stackoverflow.com/questions/19072553/text-to-speech-pattern-in-android