How to make a traditional Mongolian script ListView in Android
Asked Answered
H

1

5

How do you make a horizontally scrolling ListView for vertical Mongolian script in Android apps?

Background

Android has fairly good support for many of the world's languages, even RTL languages like Arabic and Hebrew. However, there is no built in support for top-to-bottom languages like traditional Mongolian (which is still very much alive in Inner Mongolia and not to be confused with Cyrillic Mongolian). The following graphic shows the text direction with English added for clarity.

enter image description here

Since this functionality is not built into Android, it makes almost every single aspect of app development extremely difficult. This is expecially true with horizontal ListViews, which are not supported out of the box in Android. There is also very, very little information available online. There are a number of app developers for traditional Mongolian, but whether it is for commercial reasons or otherwise, they do not seem to make their code open source.

Because of these difficulties I would like to make a series of StackOverflow questions that can serve as a central place to collect answers for some of the more difficult programming problems related to traditional Mongolian app development. Even if you are not literate in Mongolian, your help reviewing the code, making comments and questions, giving answers or even up-voting the question would be appreciated.

Mongolian Horizontally Scrolling ListView with Vertical Script

A Mongolian ListView needs to have the following requirements:

  • Scrolls horizontally from left to right
  • Touch events work the same as with a normal ListView
  • Custom layouts are supported the same as in a normal ListView

Also needs to support everything that a Mongolian TextView would support:

  • Supports a traditional Mongolian font
  • Displays text vertically from top to bottom
  • Line wrapping goes from left to right.
  • Line breaks occur at a space (same as English)

The image below shows the basic functionality a Mongolian ListView should have:

enter image description here

My answer is below, but I welcome other ways of solving this problem.

Other related questions in this series:

iOS:

Housefly answered 20/4, 2015 at 15:20 Comment(0)
H
3

Update

RecyclerViews have a horizontal layout. So it is relatively easy to put a Vertical Mongolian TextView inside one of these. Here is an example from mongol-library.

enter image description here

See this answer for a general solution to using a RecyclerView to make a horizontally scrolling list.

Old answer

It is quite unfortunate that horizontal ListViews are not provided by the Android API. There are a number of StackOverflow Q&As that talk about how to do them, though. Here are a couple samples:

But when I actually tried to implement these suggestions as well as incorporate Mongolian vertical text, I was having a terrible time. Somewhere in my search I found a slightly different answer. It was a class that rotated an entire layout. It did so by extending ViewGroup. In this way anything (including a ListView) can be put in the ViewGroup and it gets rotated. All the touch events work, too.

As I explained in my answer about Mongolian TextViews, it is not enough to simply rotate Mongolian text. That would be enough if every ListView item (or other text element in the ViewGroup) was only a single line, but rotating multiple lines make the line wrap go the wrong direction. However, mirroring the layout horizontally and also using a vertically mirrored font can overcome this, as is shown in the following image.

enter image description here

I adapted the rotated ViewGroup code to also do the horizontal mirroring.

public class MongolViewGroup extends ViewGroup {

    private int angle = 90;
    private final Matrix rotateMatrix = new Matrix();
    private final Rect viewRectRotated = new Rect();
    private final RectF tempRectF1 = new RectF();
    private final RectF tempRectF2 = new RectF();
    private final float[] viewTouchPoint = new float[2];
    private final float[] childTouchPoint = new float[2];
    private boolean angleChanged = true;

    public MongolViewGroup(Context context) {
        this(context, null);
    }

    public MongolViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        setWillNotDraw(false);
    }

    public View getView() {
        return getChildAt(0);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View view = getView();
        if (view != null) {
            measureChild(view, heightMeasureSpec, widthMeasureSpec);
            setMeasuredDimension(resolveSize(view.getMeasuredHeight(), widthMeasureSpec),
                    resolveSize(view.getMeasuredWidth(), heightMeasureSpec));
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (angleChanged) {
            final RectF layoutRect = tempRectF1;
            final RectF layoutRectRotated = tempRectF2;
            layoutRect.set(0, 0, right - left, bottom - top);
            rotateMatrix.setRotate(angle, layoutRect.centerX(), layoutRect.centerY());
            rotateMatrix.postScale(-1, 1);
            rotateMatrix.mapRect(layoutRectRotated, layoutRect);
            layoutRectRotated.round(viewRectRotated);
            angleChanged = false;
        }
        final View view = getView();
        if (view != null) {
            view.layout(viewRectRotated.left, viewRectRotated.top, viewRectRotated.right,
                    viewRectRotated.bottom);
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {

        canvas.save();
        canvas.rotate(-angle, getWidth() / 2f, getHeight() / 2f);
        canvas.scale(-1, 1);
        super.dispatchDraw(canvas);
        canvas.restore();
    }

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        invalidate();
        return super.invalidateChildInParent(location, dirty);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        viewTouchPoint[0] = event.getX();
        viewTouchPoint[1] = event.getY();
        rotateMatrix.mapPoints(childTouchPoint, viewTouchPoint);
        event.setLocation(childTouchPoint[0], childTouchPoint[1]);
        boolean result = super.dispatchTouchEvent(event);
        event.setLocation(viewTouchPoint[0], viewTouchPoint[1]);
        return result;
    }


}

The Mongolian vertically mirrored font still needs to be set somewhere else, though. I find it easiest to make a custom TextView to do it:

public class MongolNonRotatedTextView extends TextView {

    // This class does not rotate the textview. It only displays the Mongol font.
    // For use with MongolLayout, which does all the rotation and mirroring.

    // Constructors
    public MongolNonRotatedTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public MongolNonRotatedTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MongolNonRotatedTextView(Context context) {
        super(context);
        init();
    }

    // This class requires the mirrored Mongolian font to be in the assets/fonts folder
    private void init() {
         Typeface tf = Typeface.createFromAsset(getContext().getAssets(),
         "fonts/MongolMirroredFont.ttf");
         setTypeface(tf);

    }
}

Then the custom ListView item xml layout can look something like this:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rlListItem"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

        <com.example.MongolNonRotatedTextView
            android:id="@+id/tvListViewText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"/>

</RelativeLayout>

Known issues:

  • If you look carefully at the image below you can see faint horizontal and vertical lines around the text. Although this image comes from another developer's app, I am getting the same artifacts in my app when I use the rotated ViewGroup (but not when I use the rotated TextView). If anyone knows where these are coming from, please leave me a comment!

enter image description here

  • This solution does not deal with rendering the Unicode text. Either you need to use non-Unicode text (discouraged) or you need to include a rendering engine in your app. (Android does not support OpenType smartfont rendering at this time. Hopefully this will change in the future. iOS, by comparison does support complex text rendering fonts.) See this link for a Unicode Mongolian rendering engine example.
Housefly answered 20/4, 2015 at 15:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.