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 turn our attention to the UI.
Before we get stuck in the details of the UI I should own up to quite shamelessly gaining inspiration for the UI by looking at the Google Translate app which (supposedly!) performs a very similar function to Dirty Phrasebook. My reasoning for this was that some highly talented people have spent time and effort in determining a good UX for a very similar function to that which I was looking to provide.
However, Google Translate does real translations rather than the fake translations which Dirty Phrasebook does. As a result I needed to diverge from the Google Translate UI a little. Firstly Google Translate has both a source and target language selection. As we have already covered Dirty Phrasebook breaks the input down to a hash code and so is completely agnostic of the input language, so I only need a target language selection. Also, Google Translate remembers previously entered phrases and provides auto-completion. Dirty Phrasebook will always translate the same input the the same target string so it might get a little boring for the user if (s)he were encouraged to enter the same thing again, so I decided to skip this.
The entire UI is managed within a single Activity with a single layout. Let’s break this down in to it’s main components:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:sa="http://schemas.android.com/apk/res-auto" android:id="@+id/layout_container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" sa:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" sa:popupTheme="@style/ThemeOverlay.AppCompat.Light"> <Spinner android:id="@+id/language" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </android.support.v7.widget.Toolbar> . . . </LinearLayout>
First we have a Toolbar containing a Spinner which will be the target language selection.
Next we have a custom control to handle the text input named InputView which will consume half of the remaining space (because of the layout_weight):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:sa="http://schemas.android.com/apk/res-auto" android:id="@+id/layout_container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> . . . <com.stylingandroid.dirtyphrasebook.view.InputView android:id="@+id/input_view" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:clipChildren="false" sa:elevation="8dp" sa:duration="@android:integer/config_shortAnimTime" sa:offset="?attr/actionBarSize" /> . . . </LinearLayout>
And finally a FrameLayout consuming the remaining space which contains a CardView which, in turn contains a clickable title containing the target language name (clicking will send the translation string to the Text To Speech engine), and the translation string (long clicking this will copy the translated string to the clipboard).
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:sa="http://schemas.android.com/apk/res-auto" android:id="@+id/layout_container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> . . .</LinearLayout>
The UI operates in two distinct modes: when the user is editing the input string, and when a translation is being displayed. I didn’t want to try and display a live translation as the user types because the translation string will change drastically with virtually every text change (because of how hash codes of strings are calculated) so would give away the inner workings, so felt this was a good way of achieving that.
When in input mode, the CardView containing the translation will be hidden, and InputView will move up to cover the Toolbar thus preventing a change in language selection.
InputView is a composite custom control. In other words it’s simply a ViewGroup of stock controls which are grouped together in order to add some behaviour to them. The layout consist of a RelativeLayout containing an zero height and width View (which we’ll use to hold focus away from the EditText), the EditText control which will receive keyboard input, an ImageView to provide clear text function, and another ImageView to submit the text and perform the translation:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" android:padding="@dimen/card_padding"> <View android:id="@+id/focus_holder" android:layout_width="0dp" android:layout_height="0dp" android:focusableInTouchMode="true" /> <EditText android:id="@+id/input" style="@style/Widget.TextView.Input" android:layout_width="match_parent" android:layout_height="match_parent" android:inputType="textMultiLine" android:imeOptions="flagNoFullscreen|actionDone" android:gravity="top" android:hint="@string/type_here" /> <ImageView android:id="@+id/clear_input" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@id/input" android:layout_alignEnd="@id/input" android:layout_alignRight="@id/input" android:padding="8dp" android:src="@drawable/ic_clear" android:visibility="invisible" android:contentDescription="@string/clear_input" /> <ImageView android:id="@+id/input_done" android:layout_width="32dip" android:layout_height="32dip" android:background="@drawable/done_background" android:src="@drawable/ic_arrow_forward" android:padding="2dp" android:layout_margin="8dp" tools:ignore="UnusedAttribute" android:elevation="4dp" android:visibility="invisible" android:layout_alignBottom="@id/input" android:layout_alignEnd="@id/input" android:layout_alignRight="@id/input" android:contentDescription="@string/done" /> </RelativeLayout>
We’ll cover some of the actual logic behind all of this in the next article.
The remaining layouts that we have are to allow us to control the theming of the Spinner within the Toolbar. One is for the item which will displayed within the Spinner control:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@android:id/text1" style="?android:attr/spinnerItemStyle" android:layout_width="match_parent" android:layout_height="match_parent" android:textColor="@color/primary_text_default_material_dark" android:singleLine="true" android:gravity="center_vertical" android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" android:paddingRight="?android:attr/listPreferredItemPaddingRight" android:minHeight="?android:attr/listPreferredItemHeightSmall" tools:ignore="RtlHardcoded" />
The other is for the items which will be displayed within the drop down list of the Spinner:
<?xml version="1.0" encoding="utf-8"?> <CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" style="?attr/spinnerDropDownItemStyle" android:singleLine="true" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:ellipsize="marquee" android:textColor="@color/list_text" />
The second is required because getting a light theme with a dark Toolbar containing a Spinner is devilishly hard to get consistent on pre and post Lollipop even using the AppCompat library. On pre-Lollipop the dark Toolbar is apparently ignored and so you get a Spinner implementation which would work better on a light Toolbar and the opposite is true on Lollipop and later. A full dive in to this is outside the scope of this series, but may be re-visited in a future series. However, after much battling I eventually conceded to an implementation consisting of a Spinner with white text and a dark indicator with drop down list consisting of dark text on a light background on pre-Lollipop devices; and a Spinner with white text and a white indicator with a drop down list consisting of light text on a dark background on Lollipop. The latter was made possible because of drop_list.xml
and having two separate colour definitions for @color/list_text
:
<resources> <color name="sa_green">#1E9618</color> <color name="sa_green_dark">#146310</color> <color name="selector">#3FFFFFFF</color> <color name="list_text">@color/primary_text_default_material_light</color> </resources>
<resources> <color name="list_text">@color/primary_text_default_material_dark</color> </resources>
That’s the basics of the UI. In the next article we’ll take a look at the code which binds this all together.
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.