Interface code improvements

This commit is contained in:
2024-11-01 00:57:27 +01:00
parent 004dfdbe8f
commit ec3e21d8cf
11 changed files with 178 additions and 142 deletions
+35
View File
@@ -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)
-96
View File
@@ -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);
+29
View File
@@ -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>
+14 -15
View File
@@ -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>
+25
View File
@@ -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>
+2
View File
@@ -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>
+8 -6
View File
@@ -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>
+21
View File
@@ -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"