ActionBar / Adapter / Animation / ArrayAdapter / SharedPreferences / Text

Dirty Phrasebook – Part 5

On 1st April 2015 I published a joke app to Google Play named Dirty Phrasebook which is based upon Monty Python’s Dirty Hungarian Phrasebook sketch. I this series of articles we’ll take a look in to the code (which will be open-sourced along with the final article). In this article we’ll continue looking at the UI logic.

ic_launcherNow we’ll turn our attention to our MainActivity which hooks up the translation engine, layouts, and InputView that we’ve already looked at. Let’s start with the onCreate() method which is the logical starting point for any Activity:

public class MainActivity extends ActionBarActivity implements AdapterView.OnItemSelectedListener, InputView.InputListener {
    private static final String SHARED_PREFERENCES_NAME = "com.stylingandroid.dirtyphrasebook";
    private static final String KEY_PHRASE = "KEY_PHRASE";
    private static final String KEY_LANGUAGE = "KEY_LANGUAGE";

    private ActionBar actionBar;
    private View container;
    private InputView inputView;
    private View translationPanel;
    private TextView translationLabel;
    private View translationTts;
    private TextView translation;
    private Spinner spinner;

    private SharedPreferences sharedPreferences;

    private Translator translator;
    private TextToSpeechCompat textToSpeech;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!BuildConfig.DEBUG) {
            Fabric.with(this, new Crashlytics());
        }
        setContentView(R.layout.activity_main);
        sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
        translator = Translator.getInstance(this);
        String[] languages = getResources().getStringArray(R.array.readable_languages);

        findViews();
        setupToolbar();
        setupLanguageSpinner(languages);
        setupCopy();
        restoreAppState(languages[0]);

        inputView.registerInputListener(this);
        textToSpeech = TextToSpeechCompat.newInstance(this, new TtsInitListener());
        translationTts.setOnClickListener(new TtsCLickListener());
    }
}

As with InputView there’s quite a lot going on here. Firstly I elected to include Crashlytics reporting which will notify me of any crashes which occur out in the field. I would consider some kind of crash reporting virtually essential for any app being published to Google Play or another app store. I chose Crashlytics for ease of integration, and because I already have an account, but other crash reporting solutions are available.

Next we get a handle to a SharedPreferences instance which will be used to maintain the app state even if the app is killed and restarted. We then get an instance of our Translator, and load the list of readable language strings which we’ll use to populate the language selection Spinner.

Next we have the findViews() method which gets references to the Views in the layout:

private void findViews() {
    container = findViewById(R.id.layout_container);
    spinner = (Spinner) findViewById(R.id.language);
    inputView = (InputView) findViewById(R.id.input_view);
    translationPanel = findViewById(R.id.translation_panel);
    translationLabel = (TextView) findViewById(R.id.translation_label);
    translationTts = findViewById(R.id.translation_speak);
    translation = (TextView) findViewById(R.id.translation);
}

Then we have the setupToolbar() method which configures the ActionBar to use the Toolbar which we defined in the layout:

private void setupToolbar() {
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    actionBar = getSupportActionBar();
    if (actionBar != null) {
        actionBar.setDisplayShowTitleEnabled(false);
    }
}

After that we have setupLanguageSpinner() which is responsible for creating the Adapter for our language selection Spinner:

public class MainActivity extends ActionBarActivity implements AdapterView.OnItemSelectedListener, InputView.InputListener {
    .
    .
    .
    private void setupLanguageSpinner() {
        Arrays.sort(languages);
        Context themedContext = actionBar != null ? actionBar.getThemedContext() : this;
        ArrayAdapter adapter = new ArrayAdapter<>(themedContext, R.layout.drop_item, languages);
        spinner.setAdapter(adapter);
        spinner.setOnItemSelectedListener(this);
    }


    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        String language = spinner.getSelectedItem().toString();
        translator.setLanguage(language);
        updateTtsStatus(language);
        updateTranslation(inputView.getText());
        sharedPreferences.edit().putString(KEY_LANGUAGE, language).apply();
    }

    @Override
    public void onNothingSelected(AdapterView parent) {
        //NO-OP
    }
    .
    .
    .
}

onItemSelected() gets called when a new language is selected. This sets the language on the translation engine, updates the language for TextToSpeech (more on this later), updates the translation text to change the translation to the new language, and saves the language selection in SharedPreferences to store the app state.

Next setupCopy() gets called which sets up a long press handler on the translation TextView which will copy the translation text to the clipboard:

public class MainActivity extends ActionBarActivity implements AdapterView.OnItemSelectedListener, InputView.InputListener {
    .
    .
    .
    private void setupCopy() {
        View translationCopy = findViewById(R.id.translation_copy);
        translationCopy.setOnLongClickListener(new MainActivity.CopyListener());
    }

    private class CopyListener implements View.OnLongClickListener {
        @Override
        public boolean onLongClick(View v) {
            ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
            clipboardManager.setPrimaryClip(ClipData.newPlainText(getString(R.string.app_name), translation.getText()));
            Toast.makeText(MainActivity.this, getString(R.string.text_compied), Toast.LENGTH_LONG).show();
            return true;
        }
    }
    .
    .
    .
}

Next restoreAppState() gets called which will restore the app state from SharedPreferences:

private void restoreAppState(String defaultLanguage) {
    String language = sharedPreferences.getString(KEY_LANGUAGE, defaultLanguage);
    spinner.setSelection(spinner.getAdapter().getPosition(language));
    String phrase = sharedPreferences.getString(KEY_PHRASE, null);
    if (!TextUtils.isEmpty(phrase)) {
        inputView.setText(phrase);
        translationPanel.setVisibility(View.VISIBLE);
    }
}

The next thing that we do is register MainActivity as an InputListener with InputView – we covered this in the previous article – which ensures that MainActivity receives callbacks when the input mode state changes:

public class MainActivity extends ActionBarActivity implements AdapterView.OnItemSelectedListener, InputView.InputListener {
    .
    .
    .
    @Override
    public void inputChanged(String input) {
        if (TextUtils.isEmpty(input)) {
            clearTranslation();
            sharedPreferences.edit().remove(KEY_PHRASE).apply();
        } else {
            updateTranslation(input);
            sharedPreferences.edit().putString(KEY_PHRASE, input).apply();
        }
    }

    @Override
    public Animator startInputMode() {
        isInputMode = true;
        container.setOnClickListener(endEditListener);
        return getHideTranslationAnimator();
    }

    private Animator getHideTranslationAnimator() {
        Animator animator = ObjectAnimator.ofFloat(translationPanel, ALPHA, 1f, 0f);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                translationPanel.setVisibility(View.INVISIBLE);
            }
        });
        return animator;
    }

    @Override
    public Animator endInputMode(boolean hasValue) {
        isInputMode = false;
        container.setOnClickListener(null);
        return getShowTranslationAnimator(hasValue);
    }

    private Animator getShowTranslationAnimator(boolean hasValue) {
        translationPanel.setVisibility(View.VISIBLE);
        return ObjectAnimator.ofFloat(translationPanel, ALPHA, translationPanel.getAlpha(), hasValue ? 1f : 0f);
    }

    private void updateTranslation(String input) {
        translationLabel.setText(spinner.getSelectedItem().toString());
        translation.setText(translator.getTranslation(input));
    }

    private void clearTranslation() {
        Animator animator = getHideTranslationAnimator();
        animator.setDuration(inputView.getAnimationDuration());
        animator.start();
    }

    private class EndEditListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            inputView.endInputMode();
        }
    }
    .
    .
    .
}

The inputChanged() method will cause the translation to be shown or hidden depending on whether or not the input text contains any text. Whenever the input text changes we save it to SharedPreferences so that we can maintain the app state.

The startInputMode() and endInputMode() methods enable MainActivity to create any necessary animations as input mode is entered and exited. One thing worthy of mention is the OnClickListener which gets set and unset on container. The purpose of this is to enable edit mode to be exited if the user clicks outside of InputView whilst in input mode. Once again I’m indebted to Benoit Duffez for this suggestion.

The updateTranslation() method gets called whenever input mode is ended and the input is not empty. This calls the translation engine to perform the actual ‘translation’ and update the translation text.

Next we set up the Adapter for the language selection Spinner and onItemSelected() gets called when a new language is selected. This sets the language on the translation engine, updates the language for TextToSpeech (more on this later), updates the translation text to change the translation to the new language, and saves the language selection in SharedPreferences to store the app state.

The final thing that happens in onCreate() is the setup of the TextToSpeech engine. we’ll cover the actual implementation of this in the next article, but here we need to handle the engine initialisation and also add the handlers for the user clicking on the TextToSpeach playback link, and also change the visibility of this depending on the availability of the target language in the TextToSpeech engine:

public class MainActivity extends ActionBarActivity implements AdapterView.OnItemSelectedListener, InputView.InputListener {
    .
    .
    .
    private class TtsCLickListener implements View.OnClickListener {

        @Override
        public void onClick(View v) {
            if (isTtsAvailable) {
                String language = spinner.getSelectedItem().toString();
                Locale locale = translator.getLocaleForLanguage(language);
                if (textToSpeech.isLanguageAvailable(locale)) {
                    textToSpeech.setLanguage(locale);
                } else {
                    textToSpeech.setLanguage(Locale.getDefault());
                }
                textToSpeech.speak(translation.getText().toString(), TextToSpeech.QUEUE_FLUSH, 1f);
            }
        }
    }

    private class TtsInitListener implements TextToSpeech.OnInitListener {

        @Override
        public void onInit(int status) {
            if (status == TextToSpeech.SUCCESS) {
                isTtsAvailable = true;
                String language = spinner.getSelectedItem().toString();
                updateTtsStatus(language);
            }
        }
    }

    private void updateTtsStatus(String language) {
        Locale locale = translator.getLocaleForLanguage(language);
        if (isTtsAvailable && textToSpeech.isLanguageAvailable(locale)) {
            translationLabel.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_tts, 0, 0, 0);
            translationTts.setEnabled(true);
        } else {
            translationLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
            translationTts.setEnabled(false);
        }
    }
    .
    .
    .
}

We also have yet another method of the user ending input mode by clicking the back button:

@Override
public void onBackPressed() {
    if (isInputMode) {
        inputView.endInputMode();
    } else {
        super.onBackPressed();
    }
}

Finally we have our onDestroy() method which unregisters MainActivity as an InputListener with InputView, and shuts down the TextToSpeech engine:

@Override
protected void onDestroy() {
    textToSpeech.shutdown();
    inputView.unregisterInputListener(this);
    super.onDestroy();
}

We’re almost there! All that remains is the TextToSpeech implementation which we’ll cover in the final article in this series.

The source code for this series is available here.

I am deeply indebted to my fantastic team of volunteer translators who generously gave their time and language skills to make this project sooo much better. They are Sebastiano Poggi (Italian), Zvonko Grujić (Croatian), Conor O’Donnell (Gaelic), Stefan Hoth (German), Hans Petter Eide (Norwegian), Wiebe Elsinga (Dutch), Imanol Pérez Iriarte (Spanish), Adam Graves (Malay), Teo Ramone (Greek), Mattias Isegran Bergander (Swedish), Morten Grouleff (Danish), George Medve (Hungarian), Anup Cowkur (Hindi), Draško Sarić (Serbian), Polson Keeratibumrungpong (Thai), Benoit Duffez (French), and Vasily Sochinsky (Russian).

© 2015, Mark Allison. All rights reserved.

Copyright © 2015 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.