web-dev-qa-db-fra.com

Android Text-To-Speech API Sounds Robotic

J'apprends Android développement pour la première fois et mon objectif est de créer une application Hello World simple qui prend du texte et les lit à haute voix.

J'ai basé mon code sur un exemple que j'ai trouvé et voici mon code:

class MainFeeds : AppCompatActivity() {


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



        card.setOnClickListener{
            Toast.makeText(this, "Hello", Toast.LENGTH_LONG).show()
            TTS(this, "Hello this is leo")
        }
    }

}


class TTS(private val activity: Activity,
          private val message: String) : TextToSpeech.OnInitListener {

          private val tts: TextToSpeech = TextToSpeech(activity, this, "com.google.Android.tts")

    override fun onInit(i: Int) {
        if (i == TextToSpeech.SUCCESS) {

            val localeUS = Locale.US

            val result: Int
            result = tts.setLanguage(localeUS)

            if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                Toast.makeText(activity, "This Language is not supported", Toast.LENGTH_SHORT).show()
            } else {
                speakOut(message)
            }

        } else {
            Toast.makeText(activity, "Initilization Failed!", Toast.LENGTH_SHORT).show()
        }
    }

    private fun speakOut(message: String) {
        tts.speak(message, TextToSpeech.QUEUE_FLUSH, null, null)
    }
}

Et cela fonctionne parfaitement bien, le problème que je rencontre est que l'audio qui sort du synthétiseur semble extrêmement robotique, presque comme lorsque j'utilise Google Maps et que je suis déconnecté d'Internet. L'utilisation de l'Assistant Google vocal utilise-t-elle une autre API que je dois activer?

EDIT: J'ai essayé d'exécuter l'application sur mon pixel 2xl et cela semble toujours robotique, car il n'utilise pas la voix de l'Assistant Google.

15
Stupid.Fat.Cat

La qualité de la parole dépend avant tout du "moteur vocal" utilisé par l'objet TextToSpeech que vous avez créé:

private val tts: TextToSpeech = TextToSpeech(activity, this)

Si vous aviez plutôt saisi:

private val tts: TextToSpeech = TextToSpeech(activity, this, "com.google.Android.tts")

Ensuite, tout appareil sur lequel vous exécutez ce code - tentera d'utiliser le moteur de reconnaissance vocale Google ... mais il ne sera réellement utilisé que s'il existe sur l'appareil.

De même, l'utilisation de "com.samsung.SMT" tenterait d'utiliser le moteur vocal Samsung (qui est également de haute qualité, mais généralement installé uniquement sur les appareils Samsung [réels]).

La disponibilité ou non du moteur vocal Google ne dépend pas tant du Android niveau API de l'appareil (tant qu'il est suffisamment récent pour exécuter le moteur Google), mais de savoir si oui ou non le véritable moteur de synthèse vocale de Google est installé sur l'appareil.

Pour vous assurer que le moteur Google est installé:

Sur un Android Emulateur Studio:

Créez un nouvel émulateur et sélectionnez une image système qui a "API Google" ou "Google Play" dans la colonne "cible".

Sur un appareil réel:

Accédez au Play Store et installez le moteur vocal Google .

J'écris actuellement une application "TTS Diagnostics" que je publierai via Github lorsqu'elle sera prête. J'ai appris que TTS sur Android (ou au moins essayer de prédire son comportement) peut être une vraie bête.

Je suggère également la documentation, bien sûr: Java | Kotlin .

2
Boober Bunz

J'ai fait un petit programme de test qui devrait répondre à cette question pour vous.

Il vous montre une liste de toutes les voix du moteur Google, et vous cliquez dessus et vous les écoutez! Yay!

Ce qu'il fait réellement:

  • Initialise un objet TextToSpeech à l'aide du moteur de synthèse vocale de Google s'il existe sur l'appareil.
  • Vous permet de choisir une voix spécifique dans un ListView qui contient TOUTES les voix potentiellement disponibles correspondant aux paramètres régionaux spécifiés dans le code (dans ce cas, l'anglais) ... et correspondant à la version du moteur de synthèse vocale Google que vous avez installée.

De cette façon, vous pouvez tester toutes les voix pour voir si la voix "Assistant Google" que vous recherchez se trouve quelque part, et si elle n'est pas disponible, vous pouvez continuer à vérifier lorsque de nouvelles versions du moteur de synthèse vocale Google sont publiées. Il me semble que les voix de la plus haute qualité dans ce test sont à la fois de qualité: 400, et précisent qu'une connexion réseau est requise.

REMARQUES:

  • Une voix (en particulier en anglais) continuera probablement à "jouer" même si elle n'est "pas installée". En effet, lorsque vous utilisez setVoice (Voice v), le moteur (Google) renverra un "succès" int même si la voix demandée n'est pas disponible (!), Tant qu'il dispose d'une autre voix de "sauvegarde". de la même langue. Malheureusement, il fait tout cela en arrière-plan et rapporte toujours sournoisement qu'il utilise la même voix exacte que vous avez demandée même si vous utilisez getVoice () et comparez des objets. :(.

  • Généralement, si une voix le dit IS installé, alors la voix que vous entendez est la voix que vous avez demandée.

  • Pour ces raisons, vous voudrez vous assurer que vous êtes sur Internet lorsque vous testez ces voix (afin qu'elles s'installent automatiquement lorsque vous demandez des voix non disponibles) ... et aussi pour que les voix qui nécessitent une connexion réseau ne soient pas "rétrogradation automatique."

  • Vous pouvez balayer/actualiser la liste des voix afin de vérifier si les voix ont déjà été installées, ou utiliser le menu déroulant du système pour surveiller les téléchargements ... ou accéder aux paramètres de synthèse vocale de Google dans les paramètres système de l'appareil .

  • Dans la liste, les fonctionnalités vocales telles que "réseau requis" et "installé" ne sont que des échos de ce que le moteur Google rapporte et peuvent ne pas être exactes. :(

  • La qualité de voix maximale possible spécifiée dans la Documentation de la classe de voix est de 500. Dans mes tests, je n'ai pu trouver que des voix jusqu'à la qualité 400. Cela peut être dû au fait que je n'ai pas la dernière version de Google text-to -speech installé sur mon appareil de test (et je n'ai pas accès au Play Store pour le mettre à jour). Si vous utilisez un véritable appareil, je vous suggère d'installer la dernière version de Google TTS à l'aide de Google Play Store. Vous pouvez vérifier la version du moteur dans les journaux. Selon Wikipedia, la dernière version en date de cette écriture est 3.15.18.200023596. La version sur mon appareil de test est 3.13.1.

Pour recréer cette application de test, créez un projet Java Java dans Android Studio avec une API minimale de 21. (getVoices () ne fonctionne pas 21).

Manifeste:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"
    package=" [ your.package.name ] "
    Android:windowSoftInputMode="stateHidden">

    <application
        Android:allowBackup="true"
        Android:icon="@mipmap/ic_launcher"
        Android:label="@string/app_name"
        Android:roundIcon="@mipmap/ic_launcher_round"
        Android:supportsRtl="true"
        Android:theme="@style/AppTheme">
        <activity Android:name=".MainActivity">
            <intent-filter>
                <action Android:name="Android.intent.action.MAIN" />
                <category Android:name="Android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Activité principale:

package [ your package name ];

import Android.content.Intent;
import Android.content.pm.PackageManager;
import Android.content.pm.ResolveInfo;
import Android.graphics.Color;
import Android.speech.tts.TextToSpeech;
import Android.speech.tts.UtteranceProgressListener;
import Android.speech.tts.Voice;
import Android.support.v4.widget.SwipeRefreshLayout;
import Android.support.v7.app.AppCompatActivity;
import Android.os.Bundle;
import Android.util.Log;
import Android.view.View;
import Android.view.inputmethod.InputMethodManager;
import Android.widget.AdapterView;
import Android.widget.EditText;
import Android.widget.ListView;
import Android.widget.Spinner;
import Android.widget.TextView;

import Java.util.ArrayList;
import Java.util.Collections;
import Java.util.Comparator;
import Java.util.HashMap;
import Java.util.List;
import Java.util.Locale;

public class MainActivity extends AppCompatActivity {

    EditText textToSpeak;
    TextView progressView;
    TextToSpeech googleTTS;
    ListView voiceListView;
    SwipeRefreshLayout swipeRefreshLayout;
    Long timeOfSpeakRequest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textToSpeak = findViewById(R.id.textToSpeak);
        textToSpeak.setText("Do I sound robotic to you?  1,2,3,4... yabadabadoo.  "
                + "ooo! ahh! la-la-la-la-la!  num-num-dibby-dibby-num-tick-tock...  "
                + "Can I pronounce the Word, Antidisestablishmentarianism?  "
                + "Gerp!  My pants are too tight!  "
                + "CODE RED!  CODE RED!  Initiate disassemble!  Ice Cream is cold "
                + "...in my pants.  Exterminate!  exterminate!  Directive 4 is "
                + "classified."
        );
        progressView = findViewById(R.id.progressView);
        voiceListView = findViewById(R.id.voiceListView);
        swipeRefreshLayout = findViewById(R.id.swipeRefresh);


        // Create the TTS and wait until it's initialized to do anything else
        if (isGoogleEngineInstalled()) {
            createGoogleTTS();
        } else {
            Log.i("XXX", "onCreate(): Google not installed -- nothing done.");
        }

    }

    @Override
    protected void onStart() {
        super.onStart();

        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                assignFullSetOfVoicesToVoiceListView();
            }
        });

    }

    // this is where the program really begins (when the TTS is initialized)
    private void onTTSInitialized() {

        setUpWhatHappensWhenAVoiceItemIsClicked();
        setUtteranceProgressListenerOnTheTTS();
        assignFullSetOfVoicesToVoiceListView();

    }

    // FACTORED/EXTRACTED METHODS ----------------------------------------------------------------
    // These are just pulled out to make onCreate() easier to read and the basic sequence
    // of events more obvious.

    private void createGoogleTTS() {

        googleTTS = new TextToSpeech(this, new TextToSpeech.OnInitListener() {
            @Override
            public void onInit(int status) {
                if (status != TextToSpeech.ERROR) {
                    Log.i("XXX", "Google tts initialized");
                    onTTSInitialized();
                } else {
                    Log.i("XXX", "Internal Google engine init error.");
                }
            }
        }, "com.google.Android.tts");

    }

    private void setUpWhatHappensWhenAVoiceItemIsClicked() {
        voiceListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Voice desiredVoice = (Voice) parent.getAdapter().getItem(position);
                // if (setting the desired voice is "successful")...
                // in the case of google engine, this does not necessarily mean the voice you
                // want will actually be used. :(
                if (googleTTS.setVoice(desiredVoice) == 0) {
                    Log.i("XXX", "Speech voice set to: " + desiredVoice.toString());
                    // TTS did may "auto-downgrade" voice selection
                    // due to internal reason such as no data
                    // Unfortunately it will not tell you, and there seems to be no
                    // way of checking whether the presently selected voice (getVoice()) "equals"
                    // the desired voice.
                    speak();
                }
            }
        });
    }

    private void setUtteranceProgressListenerOnTheTTS() {

        UtteranceProgressListener blurp = new UtteranceProgressListener() {

            @Override // MIN API 15
            public void onStart(String s) {
                long timeSinceSpeakCall = System.currentTimeMillis() - timeOfSpeakRequest;
                Log.i("XXX", "progress.onStart() callback.  "
                        + timeSinceSpeakCall + " millis since speak() was called.");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        progressView.setTextColor(Color.GREEN);
                        progressView.setText("PROGRESS: STARTED");
                    }
                });
            }

            @Override // MIN API 15
            public void onDone(String s) {
                long timeSinceSpeakCall = System.currentTimeMillis() - timeOfSpeakRequest;
                Log.i("XXX", "progress.onDone() callback.  "
                        + timeSinceSpeakCall + " millis since speak() was called.");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        progressView.setTextColor(Color.GREEN);
                        progressView.setText("PROGRESS: DONE");
                    }
                });
            }

            // Getting an error can simply mean that the particular voice is not available
            // to the device yet... and still needs to be downloaded / is still downloading
            @Override // MIN API 15 (depracated at API 21)
            public void onError(String s) {
                long timeSinceSpeakCall = System.currentTimeMillis() - timeOfSpeakRequest;
                Log.i("XXX", "progress.onERROR() callback.  "
                        + timeSinceSpeakCall + " millis since speak() was called.");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        progressView.setTextColor(Color.RED);
                        progressView.setText("PROGRESS: ERROR");
                    }
                });

            }
        };
        googleTTS.setOnUtteranceProgressListener(blurp);

    }

    // must happens AFTER tts is initialized
    private void assignFullSetOfVoicesToVoiceListView() {

        googleTTS.stop();

        List<Voice> tempVoiceList = new ArrayList<>();

        for (Voice v : googleTTS.getVoices()) {
            if (v.getLocale().getLanguage().contains("en")) { // only English voices
                tempVoiceList.add(v);
            }
        }

        // Sort the list alphabetically by name
        Collections.sort(tempVoiceList, new Comparator<Voice>() {
            @Override
            public int compare(Voice v1, Voice v2) {
                Log.i("XXX", "comparing item");
                return (v2.getName().compareToIgnoreCase(v1.getName()));
            }
        });

        VoiceAdapter tempAdapter = new VoiceAdapter(this, tempVoiceList);

        voiceListView.setAdapter(tempAdapter);
        swipeRefreshLayout.setRefreshing(false);
        progressView.setTextColor(Color.BLACK);
        progressView.setText("PROGRESS: ...");

    }

    private void speak() {
        HashMap<String, String> map = new HashMap<>();
        map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "merp");
        timeOfSpeakRequest = System.currentTimeMillis();
        googleTTS.speak(textToSpeak.getText().toString(), TextToSpeech.QUEUE_FLUSH, map);
    }

    // Checks if Google Engine is installed
    // ... (and gives more info in Logs).
    // The version number is going to dictate the quality of voices available
    private boolean isGoogleEngineInstalled() {

        final Intent ttsIntent = new Intent();
        ttsIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
        final PackageManager pm = getPackageManager();
        final List<ResolveInfo> list = pm.queryIntentActivities(ttsIntent, PackageManager.GET_META_DATA);

        boolean googleIsInstalled = false;

        for (int i = 0; i < list.size(); i++) {

            ResolveInfo resolveInfoUnderScrutiny = list.get(i);
            String engineName = resolveInfoUnderScrutiny.activityInfo.applicationInfo.packageName;

            if (engineName.equals("com.google.Android.tts")) {
                String version = "null";
                try {
                    version = pm.getPackageInfo(engineName,
                            PackageManager.GET_META_DATA).versionName;
                } catch (Exception e) {
                    Log.i("XXX", "Error getting google engine verion: " + e.toString());
                }
                Log.i("XXX", "Google engine version " + version + " is installed!");
                googleIsInstalled = true;
            } else {
                Log.i("XXX", "Google Engine is not installed!");
            }

        }
        return googleIsInstalled;
    }
}

VoiceAdapter.Java:

package [ your package name ];

import Android.content.Context;
import Android.graphics.Color;
import Android.speech.tts.Voice;
import Android.view.LayoutInflater;
import Android.view.View;
import Android.view.ViewGroup;
import Android.widget.BaseAdapter;
import Android.widget.TextView;

import Java.util.List;

public class VoiceAdapter extends BaseAdapter {

    private Context mContext;
    private LayoutInflater mInflater;
    private List<Voice> mDataSource;

    public VoiceAdapter(Context context, List<Voice> voicesToDisplay) {
        mContext = context;
        mDataSource = voicesToDisplay;
        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public int getCount() {
        return mDataSource.size();
    }

    @Override
    public Object getItem(int position) {
        return mDataSource.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        // In a real app this method is not efficient,
        // and "View Holder Pattern" shoudl be used instead.
        View rowView = mInflater.inflate(R.layout.list_item_voice, parent, false);

        if (position%2 == 0) {
            rowView.setBackgroundColor(Color.rgb(245,245,245));
        }

        Voice voiceUnderScrutiny = mDataSource.get(position);

        // example output of Voice.toString() :
        // "Voice[Name: pt-br-x-afs#male_2-local, locale: pt_BR, quality: 400, latency: 200,
        // requiresNetwork: false, features: [networkTimeoutMs, notInstalled, networkRetriesCount]]"

        // Get title element
        TextView voiceTitleTextView =
                (TextView) rowView.findViewById(R.id.voice_title);

        TextView qualityTextView =
                (TextView) rowView.findViewById(R.id.voice_quality);

        TextView networkRequiredTextView =
                (TextView) rowView.findViewById(R.id.voice_network);

        TextView isInstalledTextView =
                (TextView) rowView.findViewById(R.id.voice_installed);

        TextView featuresTextView =
                (TextView) rowView.findViewById(R.id.voice_features);

        voiceTitleTextView.setText("VOICE NAME: " + voiceUnderScrutiny.getName());

        // Voice Quality...
        // ( https://developer.Android.com/reference/Android/speech/tts/Voice.html )
        // 100 = Very Low, 200 = Low, 300 = Normal, 400 = High, 500 = Very High
        qualityTextView.setText(  "QLTY: " + ((Integer) voiceUnderScrutiny.getQuality()).toString()  );
        if (voiceUnderScrutiny.getQuality() == 500) {
            qualityTextView.setTextColor(Color.GREEN); // set v. high quality to green
        }

        if (!voiceUnderScrutiny.isNetworkConnectionRequired()) {
            networkRequiredTextView.setText("NET_REQ?: NO");
        } else {
            networkRequiredTextView.setText("NET_REQ?: YES");
        }

        if (!voiceUnderScrutiny.getFeatures().contains("notInstalled")) {
            isInstalledTextView.setTextColor(Color.GREEN);
            isInstalledTextView.setText("INSTLLD?: YES");
        } else {
            isInstalledTextView.setTextColor(Color.RED);
            isInstalledTextView.setText("INSTLLD?: NO");
        }

        featuresTextView.setText("FEATURES: " + voiceUnderScrutiny.getFeatures().toString());

        return rowView;
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<Android.support.constraint.ConstraintLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    xmlns:app="http://schemas.Android.com/apk/res-auto"
    xmlns:tools="http://schemas.Android.com/tools"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:focusable="true"
    Android:focusableInTouchMode="true"
    tools:context=".MainActivity">

    <EditText
        Android:id="@+id/textToSpeak"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        Android:ems="10"
        Android:inputType="textPersonName"
        Android:text="textToSpeak..."
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Android.support.v4.widget.SwipeRefreshLayout
        Android:id="@+id/swipeRefresh"
        Android:layout_width="0dp"
        Android:layout_height="0dp"
        Android:layout_marginBottom="8dp"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/progressView">

    <ListView
        Android:id="@+id/voiceListView"
        Android:layout_width="0dp"
        Android:layout_height="0dp"
        Android:layout_marginBottom="8dp"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp">

    </ListView>

    </Android.support.v4.widget.SwipeRefreshLayout>

    <TextView
        Android:id="@+id/progressView"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        Android:text="UTTERANCE_PROGRESS:"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textToSpeak" />

</Android.support.constraint.ConstraintLayout>

list_item_voice.xml:

<?xml version="1.0" encoding="utf-8"?>
<Android.support.constraint.ConstraintLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    xmlns:app="http://schemas.Android.com/apk/res-auto"
    xmlns:tools="http://schemas.Android.com/tools"
    Android:layout_width="match_parent"
    Android:layout_height="wrap_content"

    Android:layout_centerInParent="true"
    Android:paddingBottom="8dp"
    Android:paddingLeft="16dp"
    Android:paddingRight="16dp"
    Android:paddingTop="8dp"
    >

    <TextView
        Android:id="@+id/voice_title"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        Android:text="NAME:"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <TextView
        Android:id="@+id/voice_installed"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginTop="8dp"
        Android:fontFamily="monospace"
        Android:text="INSTALLED? "
        Android:textAlignment="textStart"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/voice_network"
        app:layout_constraintTop_toBottomOf="@+id/voice_title" />

    <TextView
        Android:id="@+id/voice_quality"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        Android:text="QUALITY:"
        app:layout_constraintEnd_toStartOf="@+id/voice_network"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/voice_title" />

    <TextView
        Android:id="@+id/voice_features"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginBottom="8dp"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        Android:text="FEATURES:"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/voice_quality" />

    <TextView
        Android:id="@+id/voice_network"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginEnd="8dp"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        Android:text="NET_REQUIRED?"
        app:layout_constraintEnd_toStartOf="@+id/voice_installed"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/voice_quality"
        app:layout_constraintTop_toBottomOf="@+id/voice_title" />

</Android.support.constraint.ConstraintLayout>
1
Boober Bunz