Search code examples
androidandroid-recyclerviewtext-to-speechsmooth-scrolling

how to sync android tts with a recyclerview smoothscroll


Ok so i have a recycler view that i add a bunch of text to, the text are in cards with a fixed height and width, when i press a button the text is read out using androids TTS engine.

The problem is if the text exceeds the room in the view I want the recycler view to scroll to the beginning of the text/recycler view, read out the text and scroll the view as it goes while keeping up or slowing down with the android tts engine, so I've set a custom linearlayoutmanager on my recycler view with help from here that allows me to speed up or slow down the scrolling depending on a float variable, I've then tried to accomplish this a few ways to no avail and found that I need to use the UtteranceProgressListener so i created a method that updates the position of the card and tried to pass this to the UtteranceProgressListener like this

    public void speakWordsExclusive(String speech, final int position) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        myTTS.speak(speech, TextToSpeech.QUEUE_ADD, params, "");
    }
    else{
        myTTS.speak(speech, TextToSpeech.QUEUE_ADD, map);
    }
    myTTS.setOnUtteranceProgressListener(new UtteranceProgressListener() {
        @Override
        public void onStart(final String utteranceId) {
        }

        @Override
        public void onDone(final String utteranceId) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
       //position should print 1,2,3, etc but just gives me the last number
       System.out.println("position UtteranceProgress " + position );
       //the line below will smooth scroll the view to the end position with 
       //no care for which cards is being read
       SpeakGridDB.recyclerView.getLayoutManager()
      .smoothScrollToPosition(SpeakGridDB.recyclerView, null, position );
                }
            });
        }
        @Override
        public void onError(String utteranceId) {
        }
    });
}

this doesnt work the position is always just the highest number so if I were expecting 1,2,3,4 i would actually just get 4 like this,

11-07 14:23:09.121 28882-28882/ss.sealstudios.com.socialstories 
I/System.out: position UtteranceProgress 4 
11-07 14:23:09.741 28882-  
28882/ss.sealstudios.com.socialstories 
I/System.out: position UtteranceProgress 4 
11-07 14:23:10.353 28882-
28882/ss.sealstudios.com.socialstories 
I/System.out: position UtteranceProgress 4 
11-07 14:23:10.944 28882-  
28882/ss.sealstudios.com.socialstories 
I/System.out: position UtteranceProgress 4

I give the UtteranceProgressListener the position from the method below

    public void speakAndMoveExclusive() {
    final ArrayList<String> list = new ArrayList<>();
    list.clear();
    words = list.toString();
    SpeakGridDB.recyclerView.getLayoutManager().scrollToPosition(0);
    for (int i = 0; i < SpeakGridDB.cardMakerList.size(); i++) {
        list.add(SpeakGridDB.cardMakerList.get(i).getCardSpeech());
        words = list.toString();
    }
    System.out.println(words);
    if (words == null) {
        speakWords("");
    } else if (words.contains(", 's")) {
        formatString = words.replaceFirst(", 's", "'s");
    } else if (words.contains(", ing")) {
        formatString = words.replaceFirst(", ing", "ing");
    }else{
        formatString = words;
    }
    List<String> items = Arrays.asList(formatString.split("\\s*,\\s*"));
    if (items.contains("[]")){
        speakWords("");
    }
    else{
        for(int j = 0; j < items.size();j++){

            speakWordsExclusive(items.get(j), j);
            //the below line prints the position just fine
            System.out.println("position j " + j);
            params.putString
            (TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,"UniqueID" + j);
            map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,"UniqueID" + 
            j);
        }
    }
} 

the position variable ive set in here obviously isnt the position but it is identical and the print out reflects that, like below

11-07 14:23:08.452 28882-28882/ss.sealstudios.com.socialstories 
I/System.out: position j 0
11-07 14:23:08.454 28882-28882/ss.sealstudios.com.socialstories 
I/System.out: position j 1
11-07 14:23:08.456 28882-28882/ss.sealstudios.com.socialstories  
I/System.out: position j 2
11-07 14:23:08.459 28882-28882/ss.sealstudios.com.socialstories 
I/System.out: position j 3

if i just give this to a static int and give that to the onUtteranceProgressListener then i get a slightly better result but it is still not in sync with each other

so im wondering whats going on between them I've looked through the docs and cannot seem to make any sense of it I've moved my call to onStart instead of onDone hoping for a better result but nothing changes can anyone help me with this?


Solution

  • ok like @brandall said its best practice to set an UtteranceProgressListener once and preferably from onInit(int initStatus) so ive switched to this model and have it working, my code generally works for scrolling the view, note the code i will paste throws a NPE in onDone where I'm setting the background colour something to do with it not being in view make sure your updating the view from a UI thread

        public void onInit(int initStatus) {
        if (initStatus == TextToSpeech.SUCCESS) {
            myTTS.setLanguage(Locale.UK);
            //myExclusiveTTS.setLanguage(Locale.UK);
            myExclusiveTTS.setOnUtteranceProgressListener(new 
            UtteranceProgressListener() {
                    @Override
                    public void onDone(String utteranceId) {
    
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                SpeakGridDB.recyclerView.getLayoutManager()
                               .getChildAt(cardCountPosition)
                               .setBackgroundResource(R.drawable.border);
                                cardCountPosition ++;
    
                                if(cardCountPosition ==                 
                                SpeakGridDB.cardMakerList.size()){
                                    cardCountPosition = 0;
    
                                }
                            }
                        });
    
                    }
    
                    @Override
                    public void onError(String utteranceId) {
                    }
    
                    @Override
                    public void onStart(String utteranceId) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                             moveView();
                             SpeakGridDB.recyclerView.getLayoutManager()
                            .getChildAt(cardCountPosition).setBackgroundResource
                            (R.drawable.selected_blue_border);
                            }
                        });
                    }
                });
    
        } else if (initStatus == TextToSpeech.ERROR) {
            Toast.makeText(this, "Oops sorry! Text To Speech failed... 
            SimpleAAC cannot fix this", Toast.LENGTH_LONG).show();
        }
    }
    
        public void moveView(){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (cardCountPosition < SpeakGridDB.cardMakerList.size()){
               SpeakGridDB.recyclerView.getLayoutManager()
              .smoothScrollToPosition(SpeakGridDB.recyclerView, null,
               cardCountPosition + 1);
                }
                else{
                   //this is never called
                    cardCountPosition = 0;
                }
    
            }
        });
    }