Crear el projecte a Android Studio (codi)

Perfecte — et dono una guia pas a pas (en català) per crear una app Android (no Compose) anomenada tts_cat que fa servir TextToSpeech. La UI serà basada en XML i tindrà:

  • Un contenidor (usaré ConstraintLayout) amb un camp de text (EditText) amb text precarregat en català.
  • 2 botons: Parlar i Parar.
  • Lògica a MainActivity.kt per inicialitzar TTS, parlar el text i aturar la parla.
  • Maneig de la vida del TTS (alliberament a onDestroy) i comprovació si falta la veu catalana (i acció per instal·lar dades de TTS).

Segueix aquests passos i copia/enganxa els fitxers.


1) Crear el projecte a Android Studio

  1. File > New > New Project.
  2. Escull Empty Activity (no Compose).
  3. Nom: tts_cat
  4. Language: Kotlin
  5. Minimum SDK: API 21+ (recomanat)
  6. Finalitza i deixa que Android Studio generi el projecte.

2) build.gradle (Module: app)

Assegura’t que tens dependències bàsiques (Android Studio normalment ja les posa). Exemple rellevant:

android {
    compileSdk 34

    defaultConfig {
        applicationId "com.example.tts_cat"
        minSdk 21
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }

    // no cal activar Compose
}

dependencies {
    implementation "androidx.core:core-ktx:1.10.1"
    implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "com.google.android.material:material:1.9.0"
}

(Si Android Studio usa versions diferents, deixa les que proposa; el més important: no fer servir dependències de Compose.)


3) Layout XML: res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:padding="16dp">

    <!-- Contenidor (equivalent "div") -->
    <LinearLayout
        android:id="@+id/container"
        android:orientation="vertical"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:padding="12dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:background="?attr/selectableItemBackground">
        
        <!-- EditText amb text precarregat en català -->
        <EditText
            android:id="@+id/etText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Introdueix text..."
            android:inputType="textMultiLine"
            android:minLines="3"
            android:gravity="top|start"
            android:text="Hola! Aquest és un missatge de prova en català." />

        <!-- Botons en fila -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="12dp">

            <Button
                android:id="@+id/btnSpeak"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Parlar" />

            <Button
                android:id="@+id/btnStop"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_weight="1"
                android:text="Parar" />
        </LinearLayout>
    </LinearLayout>

    <!-- Opcional: text d'estat a la part de baix -->
    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Estat: inactiu"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@+id/container"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Nota: he posat un LinearLayout dins ConstraintLayout per crear el “contenidor” que demanaves (equivalent d’un <div>). L’EditText arriba amb text precarregat.


4) MainActivity.kt (Kotlin) — lògica completa TTS

Copia aquest fitxer (modifica el package per adaptar-lo a applicationId si cal):

package com.example.tts_cat

import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.speech.tts.TextToSpeech.Engine
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.util.Locale

class MainActivity : AppCompatActivity(), TextToSpeech.OnInitListener {

    private var tts: TextToSpeech? = null

    private lateinit var etText: EditText
    private lateinit var btnSpeak: Button
    private lateinit var btnStop: Button
    private lateinit var tvStatus: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // bind views
        etText = findViewById(R.id.etText)
        btnSpeak = findViewById(R.id.btnSpeak)
        btnStop = findViewById(R.id.btnStop)
        tvStatus = findViewById(R.id.tvStatus)

        // Inicialitza TextToSpeech
        tts = TextToSpeech(this, this)

        btnSpeak.setOnClickListener {
            val text = etText.text.toString().ifBlank { "Hola! Aquest és un missatge de prova en català." }
            speakText(text)
        }

        btnStop.setOnClickListener {
            stopSpeaking()
        }
    }

    override fun onInit(status: Int) {
        if (status == TextToSpeech.SUCCESS) {
            // Intentem establir català (Catalonia, Espanya)
            val localeCa = Locale("ca", "ES")
            val result = tts?.setLanguage(localeCa)

            if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                // La veu catalana no està disponible
                tvStatus.text = "Estat: Català no disponible, s'utilitza idioma per defecte."
                Toast.makeText(this, "Veu catalana no disponible. Pots instal·lar dades TTS.", Toast.LENGTH_LONG).show()

                // Prova d'obrir instal·lador de dades TTS (pot permetre descarregar veus)
                try {
                    val installIntent = Intent(Engine.ACTION_INSTALL_TTS_DATA)
                    startActivity(installIntent)
                } catch (e: Exception) {
                    // Si no és suportat, només fem servir l'idioma per defecte
                    tts?.language = Locale.getDefault()
                }

            } else {
                // Tot OK: català disponible
                tvStatus.text = "Estat: TTS inicialitzat (català)."
                // Opcional: parla automàticament en obrir l'app
                speakText("Hola Android! Benvingut a l'aplicació.")
            }
        } else {
            tvStatus.text = "Estat: Error al inicialitzar TTS."
            Toast.makeText(this, "No s'ha pogut inicialitzar TextToSpeech.", Toast.LENGTH_SHORT).show()
        }
    }

    private fun speakText(text: String) {
        if (tts == null) return

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // id d'utt
            tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, "UTTERANCE_ID")
        } else {
            @Suppress("DEPRECATION")
            tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null)
        }
        tvStatus.text = "Estat: parlant..."
    }

    private fun stopSpeaking() {
        tts?.stop()
        tvStatus.text = "Estat: aturat"
    }

    override fun onDestroy() {
        tts?.stop()
        tts?.shutdown()
        super.onDestroy()
    }
}

Explicació ràpida del codi

  • La MainActivity implementa TextToSpeech.OnInitListener.
  • tts = TextToSpeech(this, this) inicialitza el motor.
  • onInit comprova si el català (Locale("ca","ES")) està suportat. Si no hi és, intenta obrir l’instal·lador de dades TTS (Engine.ACTION_INSTALL_TTS_DATA).
  • speakText(...) crida tts.speak(...). Per API >= 21 s’usa el paràmetre utteranceId.
  • stopSpeaking() atura la parla.
  • A onDestroy tanquem i alliberem recursos.

5) Strings (opcional)

Pots posar els textos a res/values/strings.xml si vols:

<resources>
    <string name="app_name">tts_cat</string>
    <string name="hello_default">Hola! Aquest és un missatge de prova en català.</string>
    <string name="btn_speak">Parlar</string>
    <string name="btn_stop">Parar</string>
</resources>

I a l’XML utilitzar @string/….


6) Provar l’app

  • Executa l’aplicació en un dispositiu físic (recomanat) o un emulador amb suport TTS.
  • Si no s’escolta català, Android normalment et deixarà descarregar paquets de veu a Configuració → Accessibilitat / Entrada i Idioma → Sortida de text a veu (pot variar segons fabricant). També el codi obrirà el flux d’instal·lació de dades TTS.
  • El botó Parlar llegirà el text del EditText. El botó Parar aturarà la parla.

7) Millores opcionals (per si vols ampliar)

  • Afegir un spinner per triar idioma (ca, es, en) dins la mateixa Activity.
  • Guardar l’últim text escrit amb SharedPreferences.
  • Afegir controls de velocitat (setSpeechRate) i to (setPitch) per l’usuari.
  • Gestionar esdeveniments d’UtteranceProgressListener per actualitzar UI quan acaba la parla.

Exemple ràpid per ajustar velocitat:

tts?.setSpeechRate(1.0f) // 1.0 és normal, 0.5 més lent, 2.0 més ràpid
tts?.setPitch(1.0f)      // 1.0 normal

Si vols, ara:

  • Et faig el codi amb selector d’idiomes dins la mateixa MainActivity.kt; o
  • Et dono un fitxer activity_main.xml amb una mica més d’estil; o
  • T’explico com instal·lar la veu catalana des del dispositiu pas a pas.

Quina d’aquestes prefereixes que fem ara? (si vols, faig directament el selector d’idiomes dins la mateixa Activity)