suragch / mongol-library

A library to allow vertical script Mongolian in Android apps
MIT License
56 stars 17 forks source link

Mongol Library

[中文]

Android UI components for vertical Mongolian text

Table of Contents

This Android library is a collection of UI components that support vertical Mongolian text.

All of the native Android UI components only support horizontal text. In addition to this, Android support for Mongolian Unicode rendering is inadequate. These challenges are a big hurdle for new Mongolian app developers to overcome. This makes the Mongolian app development process very slow.

The purpose of this library is to make it easy to include vertical Mongolian text in your app. You only need to import the mongol-library module.  

Installing

This library is a part of the jCenter repository, which is the default in Android Studio.

You can import mongol-library into your project from jCenter by adding the following line to your dependencies in your app module's build.gradle file:

dependencies {
    implementation 'net.studymongolian:mongol-library:2.1.0'
}

If you not using the AndroidX support library, then use the following import instead:

implementation 'net.studymongolian:mongol-library:1.17.3'

Notes

UI Components

The following are the primary UI components in the library. See also the Demo App for an example of how they are used.

MongolTextView

The MongolTextView is a vertical text replacement for the standard Android TextView. It measures and lays out text from top to bottom and vertical lines are laid out from left to right. No mirroring is done internally so mirrored fonts are not required (if you want to add additional fonts). As much as possible the API seeks to follow the standard TextView API.

Basic usage

You can create a MongolTextView exclusively in XML or in code.

MongolTextView example

XML example

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp">

    <net.studymongolian.mongollibrary.MongolTextView
        android:id="@+id/mongol_text_view_id"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        app:text="ᠰᠠᠢᠨ ᠪᠠᠢᠨ᠎ᠠ ᠤᠤ︖"
        app:textSize="24sp"
        app:textColor="@android:color/black"/>

</LinearLayout>

If you are defining a style in xml, then don't use the android or app namespace for any of the custom attributes:

<style name="AppTheme.MongolTextStyle">
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:layout_centerInParent">true</item>
    <item name="textColor">@color/my_text_color</item>
    <item name="textSize">24sp</item>
</style>

Code example

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MongolTextView mongolTextView = findViewById(R.id.mongol_text_view_id);
        mongolTextView.setText("ᠮᠣᠩᠭᠣᠯ");
        mongolTextView.setTextColor(Color.BLUE);
    }
}

Features

Other features of MongolTextView include the following:

These can be further explored with the Demo App.

MongolTextView (Demo App)

MongolLabel

MongolLabel is a light weight view similar to MongolTextView. It is less expensive because it does not have to calculate multi-line, emoji rotation, or spans. If you need a large number of MongolTextViews and are experiencing performance problems, then this may be a solution.

Supports:

Does not support:

Here is an image of the Demo App:

MongolLabel (Demo App)

MongolEditText

The MongolEditText is a vertical text replacement for the standard Android EditText. As much as possible the API seeks to follow the standard EditText API. It subclasses MongolTextView. In addition to allowing cursor location and text selection, it also adds the API elements needed to communicate with both custom in-app keyboards and system keyboards.

Long clicking the MongolEditText will display a default MongolMenu with editing options. This menu can also be replaced with a custom menu if you implement setContextMenuCallbackListener().

Basic usage

The following image shows MongolEditText receiving text input from the Menksoft and Delehi system keyboards.

MongolLabel (Demo App)

XML example
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:app="http://schemas.android.com/apk/res-auto"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="20dp">

    <HorizontalScrollView
        android:id="@+id/hsvEditTextContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fillViewport="true">

        <net.studymongolian.mongollibrary.MongolEditText
            android:id="@+id/metExample"
            android:layout_width="wrap_content"
            android:layout_height="200dp"
            android:padding="10dp"
            android:background="@android:color/white"
            app:text=""
            app:textSize="30sp"/>

    </HorizontalScrollView>

</RelativeLayout>

Note that since MongolEditText does not support scrolling itself yet, it is good to wrap it in a HorizontalScrollView. If you need only a single line then you can wrap it in a ScrollView. A future TODO may be to add a singleLine or maxLines attribute.

Code example
MongolEditText mongolEditText = findViewById(R.id.metExample);
String text = mongolEditText.getText().toString();

Features

Keyboards

It cannot be assumed that all users will have a Mongol IME (like the Menksoft or Delehi keyboards) installed on their phone, so if you need Mongolian input in your app, you should probably include an in-app keyboard.

This library includes four keyboard layouts: two traditional Mongolian ones (AEIOU and QWERTY) and Cyrillic and Latin keyboards. Punctuation is shown by clicking the keyboard button. Keyboard layouts can be switched by long pressing the keyboard button.

AEIOU keyboard

AEIOU keyboard

The philosophy behind the AEIOU keyboard is to make input as easy as possible. The general arrangement follows the order of the Mongolian alphabet. The buttons are large by making infrequently used letters only available as longpress popups. The Unicode distinctions between O/U, OE/UE, and T/D are hidden from the user. It has been reported that countryside Mongols who have less interaction with computer keyboards prefer this layout. Users who want more controll over the Unicode input characters can use the QWERTY keyboard layout.

QWERTY keyboard

QWERTY keyboard

This keyboard copies the layout of a computer keyboard (with the addition of Mongolian Unicode ANG). Users can differentiate O/U, OE/UE, and T/D.

Latin keyboard

Latin keyboard

Cyrillic keyboard

Cyrillic keyboard

Custom keyboard

Is is possible to use your own custom keyboard layout. You just need to extend Keyboard and implement the appropriate methods. Start with a copy of the source code for one of the library keyboards and modify it to suite your needs. An example of a custom keyboard is included in the Demo App.

Basic usage

The keyboards are added to an ImeContainer to allow for keyboard switching and candidate word suggestions. This can be done programmatically or in XML. However, if you want to style your keyboard, you must do it in XML.

XML layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <HorizontalScrollView
        android:id="@+id/hsvEditTextContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_above="@id/ime_container"
        android:layout_margin="16dp"
        android:fillViewport="true">

        <net.studymongolian.mongollibrary.MongolEditText
            android:id="@+id/mongoledittext"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            android:padding="10dp"
            app:textSize="30sp"
            tools:layout_editor_absoluteX="20dp"
            tools:layout_editor_absoluteY="128dp" />

    </HorizontalScrollView>

    <net.studymongolian.mongollibrary.ImeContainer
        android:id="@+id/ime_container"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:background="#dbdbdb"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true">

        <net.studymongolian.mongollibrary.KeyboardQwerty
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            style="@style/KeyboardTheme"
            />

        <net.studymongolian.mongollibrary.KeyboardAeiou
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:primaryTextSize="30sp"
            style="@style/KeyboardTheme"
            />

    </net.studymongolian.mongollibrary.ImeContainer>

</RelativeLayout>

You could style each keyboard separately, but in the above example a single style is used.

<style name="KeyboardTheme" parent="AppTheme">
    <item name="keyColor">#ffffff</item>
    <item name="keyPressedColor">#b3b3b3</item>
    <item name="primaryTextColor">#000000</item>
    <item name="secondaryTextColor">#b3b3b3</item>
    <item name="keySpacing">3dp</item>
    <item name="keyBorderWidth">1px</item>
    <item name="keyBorderColor">#000000</item>
    <item name="keyBorderRadius">3dp</item>
    <item name="popupTextColor">#fe9a52</item>
    <item name="popupHighlightColor">#dbdbdb</item>
</style>

Code

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_keyboard);

        ImeContainer imeContainer = findViewById(R.id.ime_container);
        MongolEditText mongolEditText = findViewById(R.id.mongoledittext);

        // The MongolInputMethodManager handles communication between the 
        // ImeContainer (keyboards) and the MongolEditText (or EditText).
        MongolInputMethodManager mimm = new MongolInputMethodManager();
        mimm.addEditor(mongolEditText);
        mimm.setIme(imeContainer);
    }
}

Support keyboard candidates

You can provide word suggestions in a candidates view while the user is typing. You will need to suply your own word database, though.

keyboard candidates

To do this you need to set KeyboardCandidateView location in XML for each keyboard that is using it.

<net.studymongolian.mongollibrary.KeyboardQwerty
    ...
    app:candidatesLocation="horizontal_top"
    ...
    />

<net.studymongolian.mongollibrary.KeyboardAeiou
    ...
    app:candidatesLocation="vertical_left"
    ...
    />

The candidates view uses a RecyclerView internally, so you should include it in your app's build.gradle dependencies.

dependencies {
    implementation 'com.android.support:recyclerview-v7:27.1.1'
}

The system keyboard will popup natually so you can prevent that by hiding in the Manifest for the activity that is using the keyboard.

android:windowSoftInputMode="stateHidden"

Then implement ImeContainer.DataSource in your activity to provide the requested word suggestion list. Implement ImeContainer.OnNonSystemImeListener to hide the keyboard when requested from the navigation view.

public class MyActivity extends AppCompatActivity implements ImeContainer.DataSource, ImeContainer.OnNonSystemImeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...

        // provide words for candidate selection
        imeContainer.setDataSource(this);
        // listen for when keyboard requests to be hidden
        imeContainer.setOnNonSystemImeListener(this);

        // ...
    }

    // ImeContainer.DataSource methods

    @Override
    public void onRequestWordsStartingWith(String text) {
        // query database for words starting with `text`
        // call `imeContainer.setCandidates(wordList)` after the results come back
    }

    @Override
    public void onWordFinished(String word, String previousWord) {
        // save/update `word` 
        // optionally update `previousWord`'s following 
    }

    @Override
    public void onCandidateClick(int position, String word, String previousWordInEditor) {
        // query database for words that can follow `word`
        // call `imeContainer.setCandidates(wordList)` after the results come back
        // optionally update database word frequency and following
    }

    @Override
    public void onCandidateLongClick(int position, String word, String previousWordInEditor) {
        // user long clicked a candidate item
    }

    @Override
    public void onHideKeyboardRequest() {
        // set the ImeContainer visibility to View.GONE 
    }
}

This is what the navigation view looks like:

keyboard navigation

MongolMenu

MongolMenu is a replacement for context menus and toolbar menus.

Basic usage

MongolMenu menu = new MongolMenu(this);
menu.add(new MongolMenuItem("ᠨᠢᠭᠡ", R.drawable.ic_sun));
menu.add(new MongolMenuItem("ᠬᠤᠶᠠᠷ", R.drawable.ic_moon));
menu.add(new MongolMenuItem("ᠭᠤᠷᠪᠠ", R.drawable.ic_star));
menu.setOnMenuItemClickListener(new MongolMenu.OnMenuItemClickListener() {
    public boolean onMenuItemClick(MongolMenuItem item) {
        MongolToast.makeText(MongolMenuActivity.this, item.getTitle(), MongolToast.LENGTH_SHORT).show();
        return true;
    }
});
menu.showAsDropDown(view, 0, 0);

Instead of showAsDropDown (which anchors it to the view), you can also use showAtLocation to place it anywhere on the screen. Here is an example that places it on the toolbar.

MongolMenu example

MongolToast

MongolToast is a vertical version of Android Toast.

Basic usage

MongolToast.makeText(getApplicationContext(), "ᠰᠠᠢᠨ ᠪᠠᠢᠨ᠎ᠠ ᠤᠤ︖", MongolToast.LENGTH_LONG).show();

This produces the following result:

MongolToast example

MongolAlertDialog

MongolAlertDialog is a vertical version of Android AlertDialog. It currently only supports a title, message, and up to 3 buttons.

Basic usage

// setup the alert builder
MongolAlertDialog.Builder builder = new MongolAlertDialog.Builder(this);
builder.setMessage("ᠵᠠᠮᠤᠭ ᠰᠠᠢᠲᠠᠢ ᠨᠠᠭᠤᠷ ᠲᠤ ᠵᠢᠭᠠᠰᠤ ᠤᠯᠠᠨ᠂\nᠵᠠᠩ ᠰᠠᠢᠲᠠᠢ ᠬᠦᠮᠦᠨ ᠳᠦ ᠨᠦᠬᠦᠷ ᠤᠯᠠᠨ᠃");

// add the button
builder.setPositiveButton("ᠮᠡᠳᠡᠯ᠎ᠡ", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        // do sth
    }
});

// create and show the alert dialog
 MongolAlertDialog dialog = builder.create();
 dialog.show();

This produces the following result:

MongolToast example

Horizontal RecyclerView

No special UI componants are needed from the library to create a horizontal RecyclerView. However, it is a common need for Mongolian apps, so an example is included here.

Horizontal RecyclerView example

The only thing you really need to do differently from a normal RecyclerView list is to use LinearLayoutManager.HORIZONTAL for the orientation. Check out this Stack Overflow answer for more detailed instructions.

Also see the example in the Demo App. Here is the relevant code:

Unicode

All of the UI components in this library are designed to use Unicode for all input and output. (However, since glyph rendering internally uses Menksoft code, you can also use Menksoft code for input. This is not recommended, though.)

The MongolCode rendering engine seeks to conform to the Unicode 10.0 standard (Mongolian Block). However, the standard was deviated from in the following two cases:

Todo Script is now supported and rendering is handled completely by the font.

Other issues

The Unicode standard does not specify how diphthongs should be encoded (or whether diphthongs exist at all in written Mongolian). For example, the AI of SAIN is sometimes encoded as AI (\u1820\u1821) and sometimes encoded as AYI (\u1820\u1836\u1821). For this reason, both of these encodings are supported. However, this creates a problem for rendering the AI of NAIMA (eight). The solution this library takes for the NAIMA problem was discussed above in MONGOLIAN LETTER I.

Another problem is how to override the dotted N in names like CHOLMON-ODO or BAYAN-UNDUSU. Menksoft uses N + FVS1. The problem is that this deviates from Unicode 10.0 and many other fonts. For that reason, this library does not currently follow Menksoft in this. Another input/rendering solution is to insert a ZWJ after the N. Thus, CHOLMONODO would be rendered (CHOLMON(ZWJ)O(FVS1)DO). However, this is not a standard documented use of ZWJ.

It is hoped that the Unicode standard will introduce an additional control character that could be used similarly to the FVS characters. This new control character would always override the context and make the default form be shown. (This would solve the final GA problem, the NAIMA problem, and the N problem discussed above.)

See the demo app or the tests for examples of how words are rendered. If you discover any rendering errors then please report them. This is a high priority issue.

Code examples

The MongolCode class is the Unicode rendering engine. Generally you won't need to use this class directly, but you can use it to covert between Menksoft code and Unicode if needed. The MongolCode.Uni and MongolCode.Suffix inner classes may also be useful for references to get Unicode characters and strings.

Unicode <--> Menksoft code conversion
MongolCode converter = MongolCode.INSTANCE;
String unicode;
String menksoftCode;

// Unicode -> Menksoft code
unicode = "ᠮᠣᠩᠭᠣᠯ";
menksoftCode = converter.unicodeToMenksoft(unicode);

// Menksoft code -> Unicode
menksoftCode = "\uE2F2\uE289\uE2BC\uE2EC\uE289\uE2F9";
unicode = converter.menksoftToUnicode(menksoftCode);
Mongolian letters and suffixes
char unicodeLetter = MongolCode.Uni.MA;                         // '\u182E'
char unicodePunctuation = MongolCode.Uni.MONGOLIAN_FULL_STOP;   // '\u1803'
String iyerSuffix = MongolCode.Suffix.IYER;                     // "\u202F\u1822\u1836\u1821\u1837"
Static classes

There are a number of static methods that may also be useful.

Fonts

In order to keep the library size as small as possible, only one font is included by default. This is the Menksoft Qagan font. However, you may include any of the other Menksoft fonts in your project. Either TrueType or OpenType are fine. In fact, the TrueType fonts are smaller and since the OpenType rendering code is not used in this library, the TrueType version of the font may be better when available.

Some of the Menksoft fonts contain ligature errors for Latin letter combinations like fi. See this Stack Overflow question. It is hoped that Menksoft will correct these errors by removing the ligature encoding from the affected fonts.

Code example

Use MongolFont to create a TypeFace. The MongolFont class will take care of caching fonts so that you can reuse them on multiple views. You should create an assets folder and add the Menksoft font that you want to use (optionally in a fonts subfolder).

custom font example

public class MainActivity extends AppCompatActivity {

    // custom font is stored in the app's assets/fonts folder
    public static final String AMGLANG = "fonts/MAM8102.ttf";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MongolTextView mongolTextView = (MongolTextView) findViewById(R.id.mongol_text_view_id);
        mongolTextView.setText("ᠮᠣᠩᠭᠣᠯ");

        // set the custom font
        Typeface customFont = MongolFont.get(AMGLANG, getApplicationContext());
        mongolTextView.setTypeface(customFont);
    }
}

Custom fonts can also be added to spans.

How to contribute

For this library to be used widely, more testing and development is needed from other developers.

If you find a bug, open an issue report. Even better would be to add a unit or instrumentation test that shows it.

The following explanation shows how the library works internally.

MongolEditText extends and adds editing functionality to MongolTextView, which itself directly extends View. MongolTextView uses MongolCode to convert the Unicode text into the Menksoft glyph text codes that are contained in the font. This text is then passed on to MongolLayout, which measures the text and breaks it into lines that are laid out vertically from left to right. Each line of text is drawn by MongolTextLine, which handles rotating emojis and CJK characters. A text run is the smallest string of characters that are processed together (for drawing or non-linebraking word units).

MongolEditText communicates with the in-app keyboard using MongolInputMethodManager. The keyboards (both system and in-app) send input to the MongolEditText using MetInputConnection.

The keyboards are embedded in the keyboard container, which acts as a controller switching between the in-app keyboards. It also handles communication with the candidate view (TODO).

TODO

Version changes

External links

Apps that use this library

If your app uses this library, you can notify me or add it here, especially if it is open source.