Interface code improvements
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Titanboard
|
||||
Titanboard is an input method for physical keyboard phones, supporting as low as Android 1.5
|
||||
Cupcake.
|
||||
|
||||
## Features
|
||||
- Support for very low Android versions, down to 1.5
|
||||
- Nimble and small; I set out to have the APK be at most 100 kB
|
||||
- Each physical key can have several additional symbols, which the user can cycle by long pressing
|
||||
a given key. This also applies for alternate (alt) symbols
|
||||
- 9 supported languages, with more planned (which are trivial to add)
|
||||
- No hard to memorize key combinations; reasonably uses what the physical keyboard has to offer
|
||||
- Pasting from clipboard akin to Gboard
|
||||
- Occupied screen estate is minimized; on Android 5.0 and higher, an additional floating interface
|
||||
mode is supported, which floats above the text you're typing, thus making the keyboard occupy no
|
||||
space at all
|
||||
|
||||
## Planned features
|
||||
- Autocorrect
|
||||
- Emojis
|
||||
- More sophisticated clipboard management
|
||||
|
||||
## Supported languages
|
||||
- English
|
||||
- German
|
||||
- French
|
||||
- Italian
|
||||
- Spanish
|
||||
- Polish
|
||||
- Russian (Phonetic)
|
||||
- Greek (polytonic diacritics under Q)
|
||||
- Arabic (Intellark)
|
||||
|
||||
## Supported devices
|
||||
Thus far, the supported devices are:
|
||||
- Unihertz Titan (additional diacritics under 1 key for all languages)
|
||||
@@ -1,96 +0,0 @@
|
||||
# Titanboard user experience
|
||||
|
||||
Keyboards are an integral part of the functioning of any computer, including the smartphone. Care
|
||||
should then be taken to design keyboards to be as intuitive, easy to use, and unobstructed as
|
||||
possible.
|
||||
|
||||
The Unihertz Titan already has a physical keyboard. It's poorly designed in many aspects. Mainly, it
|
||||
isn't very reminiscent of a regular QWERTY keyboard which people are used to:
|
||||
|
||||
1. The worst part is arguably the shift and alt keys being at the top of the keyboard, instead of on
|
||||
the bottom.
|
||||
2. The spacebar key is on the same row as the bottom QWERTY alphabet keys.
|
||||
3. The numerical keys, which are some of the most commonly used keys in phones (passwords, PIN
|
||||
codes)
|
||||
and in regular language are right below the alt key, which makes typing them very awkward. One
|
||||
could
|
||||
also argue that the numpad is in a "reversed" layout, but this is consistent with the usual
|
||||
layout
|
||||
found on phones.
|
||||
4. It lacks some commonly used symbols: ';', '&', '=', '<', '>', '$'. Other than that, it lacks some
|
||||
other
|
||||
symbols from regular QWERTY keyboards.
|
||||
5. There is no extra alt-gr key to type in localized letters.
|
||||
6. The keyboard is too big for one hand typing.
|
||||
7. In my opinion, it also lacks a control key, but that is unnecessary on smartphones, unless you
|
||||
use the terminal or modify spreadsheets. However, providing appropriate UI elements for the
|
||||
actions
|
||||
possible on regular computers should be the responsibility of the application. Smartphones are
|
||||
simply different devices, with smaller screens and less space for complexity.
|
||||
8. Emojis.
|
||||
|
||||
However, when designing a keyboard app for it one should already use all pre-existing quirks and
|
||||
decisions which materialized in the physical keyboard. For example, the alt key should be used for
|
||||
the alternate characters printed on the keyboard, the shift key should give us uppercase characters
|
||||
and so on.
|
||||
|
||||
# Solutions
|
||||
|
||||
Here are my solutions for the above listed problems:
|
||||
|
||||
1. This is solvable with shift/alt latching. This makes it so when you press these keys once, the
|
||||
next key pressed will be either uppercased or the alternate key visible on the physical button.
|
||||
Additionally, the shift and alt key can be toggled by double-clicking in a fast succession, similar
|
||||
to regular visual keyboards on Android.
|
||||
2. This is unfortunately unsolvable with software.
|
||||
3. See 1., latching the keys solves this issue.
|
||||
4. This is solved by assigning alternate symbols when alt is pressed/latched/locked and the given
|
||||
key is long pressed. A menu pops up, and as you hold the key the symbols are cycled through.
|
||||
5. Similar to regular visual keyboards on Android, a diacritic/alternate key is assigned to each key
|
||||
according to the language. Long pressing a key will allow you to cycle through available
|
||||
alternatives.
|
||||
6. A toggleable UI that shows the left/right hand side keys, depending on a customizable setting for
|
||||
left handed people.
|
||||
7. I have no plans on adding a control key, as it is largely unnecessary on phones.
|
||||
8. I do not really use emojis, but I am aware that they are used by the majority of people. Most
|
||||
IM apps provide their own emoji list, so this isn't a priority. Designing a good UX for it is also
|
||||
hard on the Titan.
|
||||
|
||||
Taking into account typing ergonomics and pre-existing UX choices of other keyboard designers, this
|
||||
input method should make typing on the Unihertz Titan a pleasant experience. Other than fixing the
|
||||
fundamental issues, the keyboard should also:
|
||||
|
||||
1. Provide a way of managing the clipboard, or at the very least pasting things with an UI element.
|
||||
This is to make the usage intuitive and not introduce any additional keys such as a ctrl key.
|
||||
2. Autocorrect, language switching, auto caps and periods.
|
||||
3. Some minimal customization, but just enough not to overwhelm the user.
|
||||
|
||||
For the Unihertz Titan Pocket/Slim, the sym key is unused. I have no ideas what it could be useful
|
||||
for. It can be set in Unihertz's software to do operating system actions, which I think is good.
|
||||
|
||||
# User interface
|
||||
|
||||
Google recommends that visual keyboards should not occupy more than half of the screen estate.
|
||||
However, most phones have long, vertical screens, which means essentially that the keyboard will
|
||||
be a square, and so will the remaining view. On the Titan, the screen is already square, so the
|
||||
remaining space will be proportionally much smaller. Therefore, the keyboard should occupy very
|
||||
minimal screen estate, as to allow the user to see the application they are interacting with.
|
||||
|
||||
For this reason, I make sure the keyboard occupies 20–25 % screen estate. This is enough for simple
|
||||
status bar, which shows the current shift/alt state, the current language, and a symbol bar which
|
||||
appears when long pressing keys.
|
||||
|
||||
# Keymaps
|
||||
Latin keymaps use the regular QWERTY layout of the Titan. There is one, large, general European key
|
||||
layout, which contains all characters used in Europe.
|
||||
|
||||
## European
|
||||
German has the most speakers, therefore it's diacritics are the first in the list. Then French and
|
||||
Italian, which have similar diacritics. These come next. Afterwards, Spanish, then Polish, and the
|
||||
rest.
|
||||
|
||||
## Cyrillic
|
||||
Regular JCUKEN layout, with additional characters for other non-Russian languages.
|
||||
|
||||
## Greek
|
||||
Works mostly like a regular Greek QWERTY keyboard.
|
||||
@@ -25,13 +25,10 @@ import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public class ClipboardHandler implements ClipboardManager.OnPrimaryClipChangedListener {
|
||||
private final static int MAX_CLIPS = 4;
|
||||
private final ArrayList<ClipData> mClips = new ArrayList<>();
|
||||
private final ClipboardManager mClipboardManager;
|
||||
private ClipData mCurrentClip;
|
||||
|
||||
public ClipboardHandler(Context ctx) {
|
||||
mClipboardManager = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
@@ -39,6 +36,10 @@ public class ClipboardHandler implements ClipboardManager.OnPrimaryClipChangedLi
|
||||
|
||||
@Override
|
||||
public void onPrimaryClipChanged() {
|
||||
ClipData newClip = mClipboardManager.getPrimaryClip();
|
||||
mCurrentClip = mClipboardManager.getPrimaryClip();
|
||||
}
|
||||
|
||||
public ClipData getCurrentClip() {
|
||||
return mCurrentClip;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public class InputMethod extends InputMethodService {
|
||||
public MetaKeyManager mMetaKeyManager;
|
||||
public KeymapManager mKeymapManager;
|
||||
public PreferenceHolder mPreferenceHolder;
|
||||
private ClipboardHandler mClipboardHandler;
|
||||
private InputView mInputView;
|
||||
private int mCurrentCursorPosition;
|
||||
private Handler mHandler;
|
||||
@@ -58,6 +59,7 @@ public class InputMethod extends InputMethodService {
|
||||
mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
||||
mMetaKeyManager = new MetaKeyManager(mVibrator);
|
||||
mKeymapManager = new KeymapManager(this);
|
||||
mClipboardHandler = new ClipboardHandler(this);
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mSymbolCycler = new SymbolCycler();
|
||||
mKeymapManager.cycleKeymaps(mPreferenceHolder.getSelectedKeymaps());
|
||||
|
||||
@@ -25,10 +25,8 @@ import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.Gravity;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -40,13 +38,32 @@ import java.util.ArrayList;
|
||||
|
||||
public class InputView extends RelativeLayout {
|
||||
private final InputMethod mInputMethod;
|
||||
private final LinearLayout.LayoutParams mSymbolLayout;
|
||||
private final LinearLayout.LayoutParams mCandidateLayout;
|
||||
private LinearLayout mSymbolList;
|
||||
private LinearLayout mCandidateList;
|
||||
private Button mStateButton;
|
||||
private Button mClipboardButton;
|
||||
|
||||
public InputView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs, 0);
|
||||
mInputMethod = (InputMethod) context;
|
||||
mSymbolLayout = new LinearLayout.LayoutParams(
|
||||
getResources().getDimensionPixelSize(R.dimen.symbol_width),
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, 0);
|
||||
mCandidateLayout = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, 0);
|
||||
|
||||
mSymbolLayout.setMargins(0, 0,
|
||||
getResources().getDimensionPixelSize(R.dimen.default_space), 0);
|
||||
mCandidateLayout.setMargins(0, 0,
|
||||
getResources().getDimensionPixelSize(R.dimen.default_space), 0);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
mSymbolLayout.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.default_space));
|
||||
mCandidateLayout.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.default_space));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,6 +72,10 @@ public class InputView extends RelativeLayout {
|
||||
mSymbolList = findViewById(R.id.symbol_list);
|
||||
mCandidateList = findViewById(R.id.candidate_list);
|
||||
mStateButton = findViewById(R.id.state_button);
|
||||
mClipboardButton = (Button) inflate(this.getContext(), R.layout.candidate, null);
|
||||
mClipboardButton.setTag(0);
|
||||
mClipboardButton.setLayoutParams(mCandidateLayout);
|
||||
mCandidateList.addView(mClipboardButton);
|
||||
|
||||
mStateButton.setOnClickListener(button -> {
|
||||
MetaKeyManager metaManager = mInputMethod.mMetaKeyManager;
|
||||
@@ -127,27 +148,13 @@ public class InputView extends RelativeLayout {
|
||||
* @param strings the strings to be displayed in the CyclerView
|
||||
*/
|
||||
public void prepareSymbols(ArrayList<String> strings) {
|
||||
ContextThemeWrapper ctw = new ContextThemeWrapper(this.getContext(), R.style.InputViewCandidate);
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||
getResources().getDimensionPixelSize(R.dimen.view_symbol_width),
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, 0);
|
||||
|
||||
lp.setMargins(0, 0,
|
||||
getResources().getDimensionPixelSize(R.dimen.view_default_space), 0);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
lp.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.view_default_space));
|
||||
}
|
||||
|
||||
for (int i = 0; i < strings.size(); ++i) {
|
||||
TextView tv = (TextView) mSymbolList.getChildAt(i);
|
||||
|
||||
if (tv == null) {
|
||||
tv = new TextView(ctw, null, 0);
|
||||
tv.setGravity(Gravity.CENTER);
|
||||
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX,
|
||||
getResources().getDimensionPixelSize(R.dimen.view_default_text_size));
|
||||
tv.setLayoutParams(lp);
|
||||
tv = (TextView) inflate(this.getContext(), R.layout.symbol, null);
|
||||
tv.setTag(i);
|
||||
tv.setLayoutParams(mSymbolLayout);
|
||||
mSymbolList.addView(tv);
|
||||
}
|
||||
|
||||
@@ -165,8 +172,17 @@ public class InputView extends RelativeLayout {
|
||||
TextView tv = (TextView) mSymbolList.getChildAt(i);
|
||||
|
||||
if (i == index) {
|
||||
HorizontalScrollView hsw = findViewById(R.id.symbol_list_container);
|
||||
tv.setPressed(true);
|
||||
tv.setTypeface(null, Typeface.BOLD);
|
||||
// This, if the symbol list is larger than the scroll view, scrolls it just to show
|
||||
// the selected symbol at the very end. It isn't really necessary, as it's a rarity
|
||||
// that a key has more than five symbols, but perhaps someone will want to add a
|
||||
// keymap that has a lot of symbols under one key, and also this is a safeguard for
|
||||
// smaller screen sizes.
|
||||
hsw.smoothScrollTo(tv.getRight()
|
||||
+ getResources().getDimensionPixelSize(R.dimen.default_space) * 2
|
||||
- hsw.getRight(), 0);
|
||||
} else {
|
||||
tv.setPressed(false);
|
||||
tv.setTypeface(null, Typeface.NORMAL);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ An input method for the Unihertz Titan/physical keyboard phones
|
||||
~ Copyright (C) 2024 Emil Kosz
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
~
|
||||
-->
|
||||
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/InputViewCandidate"
|
||||
android:drawablePadding="@dimen/candidate_side_padding"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="@dimen/candidate_max_width"
|
||||
android:paddingLeft="@dimen/candidate_side_padding"
|
||||
android:paddingRight="@dimen/candidate_side_padding"
|
||||
android:singleLine="true">
|
||||
|
||||
</Button>
|
||||
@@ -22,20 +22,20 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingLeft="@dimen/view_default_space"
|
||||
android:paddingTop="@dimen/view_default_space"
|
||||
android:paddingRight="@dimen/view_default_space">
|
||||
android:paddingLeft="@dimen/default_space"
|
||||
android:paddingTop="@dimen/default_space"
|
||||
android:paddingRight="@dimen/default_space">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/symbol_list_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/view_container_height"
|
||||
android:layout_height="@dimen/container_height"
|
||||
android:layout_alignTop="@id/state_button"
|
||||
android:layout_marginEnd="@dimen/view_default_space"
|
||||
android:layout_marginRight="@dimen/view_default_space"
|
||||
android:layout_marginEnd="@dimen/default_space"
|
||||
android:layout_marginRight="@dimen/default_space"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:layout_toStartOf="@+id/state_button"
|
||||
android:layout_toLeftOf="@+id/state_button"
|
||||
android:layout_toLeftOf="@id/state_button"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
@@ -50,14 +50,13 @@
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/candidate_list_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/view_container_height"
|
||||
android:layout_height="@dimen/container_height"
|
||||
android:layout_alignTop="@id/state_button"
|
||||
android:layout_marginEnd="@dimen/view_default_space"
|
||||
android:layout_marginRight="@dimen/view_default_space"
|
||||
android:layout_marginEnd="@dimen/default_space"
|
||||
android:layout_marginRight="@dimen/default_space"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:layout_toStartOf="@+id/state_button"
|
||||
android:layout_toLeftOf="@+id/state_button"
|
||||
android:visibility="visible">
|
||||
android:layout_toLeftOf="@id/state_button">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/candidate_list"
|
||||
@@ -69,11 +68,11 @@
|
||||
<Button
|
||||
android:id="@+id/state_button"
|
||||
style="@style/InputViewButton"
|
||||
android:layout_width="@dimen/view_button_width"
|
||||
android:layout_height="@dimen/view_button_height"
|
||||
android:layout_width="@dimen/button_width"
|
||||
android:layout_height="@dimen/button_height"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:text="@string/state_button_default"
|
||||
android:textSize="@dimen/view_default_text_size" />
|
||||
android:textSize="@dimen/default_text_size" />
|
||||
|
||||
</cfpi_fpsi.eu.titanboard.InputView>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ An input method for the Unihertz Titan/physical keyboard phones
|
||||
~ Copyright (C) 2024 Emil Kosz
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
~
|
||||
-->
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/InputViewCandidate"
|
||||
android:gravity="center"
|
||||
android:textSize="@dimen/default_text_size">
|
||||
|
||||
</TextView>
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
<resources>
|
||||
<string-array name="preferences_keymap_labels">
|
||||
<item>Test</item>
|
||||
<item>English</item>
|
||||
<item>German</item>
|
||||
<item>French</item>
|
||||
@@ -31,6 +32,7 @@
|
||||
</string-array>
|
||||
|
||||
<string-array name="preferences_keymaps">
|
||||
<item>tt</item>
|
||||
<item>en</item>
|
||||
<item>de</item>
|
||||
<item>fr</item>
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<dimen name="view_default_space">8dp</dimen>
|
||||
<dimen name="view_container_height">56dp</dimen>
|
||||
<dimen name="view_button_width">48dp</dimen>
|
||||
<dimen name="view_button_height">48dp</dimen>
|
||||
<dimen name="view_symbol_width">40dp</dimen>
|
||||
<dimen name="view_default_text_size">18sp</dimen>
|
||||
<dimen name="default_space">8dp</dimen>
|
||||
<dimen name="container_height">56dp</dimen>
|
||||
<dimen name="button_width">48dp</dimen>
|
||||
<dimen name="button_height">48dp</dimen>
|
||||
<dimen name="candidate_max_width">196dp</dimen>
|
||||
<dimen name="candidate_side_padding">12dp</dimen>
|
||||
<dimen name="symbol_width">40dp</dimen>
|
||||
<dimen name="default_text_size">18sp</dimen>
|
||||
</resources>
|
||||
@@ -18,6 +18,27 @@
|
||||
-->
|
||||
|
||||
<device-keymap-list>
|
||||
<!-- Test keymap -->
|
||||
<keymap
|
||||
name="tt"
|
||||
latched-name="Tt"
|
||||
locked-name="TT">
|
||||
<key code="29">
|
||||
<base value="a" />
|
||||
<base value="b" />
|
||||
<base value="c" />
|
||||
<base value="d" />
|
||||
<base value="e" />
|
||||
<base value="f" />
|
||||
<base value="g" />
|
||||
<base value="h" />
|
||||
<base value="i" />
|
||||
<base value="j" />
|
||||
<base value="k" />
|
||||
<base value="l" />
|
||||
<base value="o" />
|
||||
</key>
|
||||
</keymap>
|
||||
<!-- English -->
|
||||
<keymap
|
||||
name="en"
|
||||
|
||||
Reference in New Issue
Block a user