如何在Android中制作传统的蒙古文字ListView(How to make a traditional Mongolian script ListView in Android)

如何在Android应用程序中为垂直蒙古语脚本水平滚动ListView?

背景

Android对世界上许多语言都有相当好的支持,甚至包括阿拉伯语和希伯来语等RTL语言。 然而,对于像传统蒙古语这样的自上而下的语言没有内置支持(在内蒙古仍然非常活跃,不要与西里尔语蒙古语混淆)。 为清晰起见,下图显示了添加了英语的文本方向。

在此处输入图像描述

由于此功能未内置于Android中,因此几乎使应用程序开发的每个方面都非常困难。 对于水平ListViews尤其如此,Android中不支持开箱即用。 在线提供的信息非常非常少。 有许多针对传统蒙古语的应用程序开发人员,但无论是出于商业原因还是其他原因,他们似乎都没有使他们的代码成为开源代码。

由于存在这些困难,我想提出一系列StackOverflow问题,这些问题可以作为收集与传统蒙古应用程序开发相关的一些更困难的编程问题的答案的中心位置。 即使您没有蒙古语识字,您也可以阅读帮助,查看代码,提出意见和问题,给出答案,甚至对问题进行投票。

Mongolian水平滚动ListView与垂直脚本

Mongolian ListView需要具备以下要求:

  • 从左到右水平滚动
  • 触摸事件与普通ListView的工作方式相同
  • 自定义布局的支持与普通ListView相同

还需要支持Mongolian TextView支持的所有内容:

  • 支持传统的蒙古语字体
  • 从上到下垂直显示文本
  • 换行从左到右。
  • 换行发生在一个空间(与英语相同)

下图显示了Mongolian ListView应具备的基本功能:

在此处输入图像描述

我的答案如下,但我欢迎其他方法来解决这个问题。

本系列中的其他相关问题:

iOS版:


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:

2022-04-18 19:04

满意答案

更新

RecyclerViews具有水平布局。 因此,将Vertical Mongolian TextView放在其中一个中相对容易。 mongol-library一个例子。

在此处输入图像描述

有关使用RecyclerView制作水平滚动列表的一般解决方案,请参阅此答案

老答案

非常不幸的是Android API没有提供水平ListView。 不过,有很多StackOverflow问答都谈到了如何做。 以下是几个样本:

但是,当我真正尝试实施这些建议以及合并蒙古文垂直文本时,我度过了一段可怕的时光。 在我搜索的某处,我发现了一个略有不同的答案。 这是一个旋转整个布局的类 。 它是通过扩展ViewGroup来实现的。 通过这种方式,任何东西(包括ListView)都可以放在ViewGroup中并旋转。 所有的触摸事件也都有效。

正如我在关于蒙古文字视图的回答中所解释的那样,简单地旋转蒙古文本是不够的。 如果每个ListView项(或ViewGroup中的其他文本元素)只是一行,那就足够了,但是旋转多行会使换行方向错误。 但是,水平镜像布局并使用垂直镜像字体可以克服此问题,如下图所示。

在此处输入图像描述

我调整了旋转的ViewGroup代码以执行水平镜像。

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;
    }


}

不过,Mongolian垂直镜像字体仍然需要设置在其他地方。 我发现最简单的做一个自定义TextView来做到这一点:

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);

    }
}

然后自定义ListView项目xml布局可能如下所示:

<?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>

已知的问题:

  • 如果仔细查看下图,可以看到文本周围的细微水平和垂直线条。 虽然这个图像来自另一个开发人员的应用程序,但是当我使用旋转的ViewGroup时,我在我的应用程序中获得了相同的工件(但是当我使用旋转的TextView时却没有)。 如果有人知道这些来自哪里,请给我留言!

在此处输入图像描述

  • 此解决方案不涉及呈现Unicode文本。 您需要使用非Unicode文本(不鼓励),或者需要在应用程序中包含渲染引擎。 (Android目前不支持OpenType smartfont渲染。希望将来会改变。相比之下,iOS支持复杂的文本渲染字体。)请参阅此链接以获取Unicode蒙古语渲染引擎示例。

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.

相关问答

更多

Android ListView不会滚动。(Android ListView does not scroll. Tried everything)

像这样将ListView放在RelativeLayout <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"> <ListView xmlns:andro...

Android listview不可滚动(Android listview not scrollable)

你可以用两种方式做到这一点: #1在Listview添加Header View header = inflater.inflate(R.layout.your_header_layout, null); yourlistview.addHeader(header); #2您可以根据children设置列表视图高度。使用listview调用此fxn并将所有小部件保存在scrollview中。 public static void setListViewHeightBasedOnCh...

Listview到android中的pdf(Listview to pdf in android)

您可以在recyclerView获取屏幕上显示的项目数,因此为第一组视图创建位图,之后您可以动态滚动到显示的最后一项下面的项目(即noOfItemDisplayed + 1),再次获取位图,同样全部获取所有将ArrayList<Bitmap>到ArrayList<Bitmap> ,您可以从中创建pdf文件,请参考下面的代码,从位图的arraylist创建pdf: @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void create...

如何在android中制作ListView?(How to make a ListView in android? [closed])

Android中有两种类型的列表视图:1.Basic List Views。 2.自定义列表视图。 基本列表视图这些列表视图不需要任何其他布局用于列表视图生成。 您可以使用链接实现它: http : //windrealm.org/tutorials/android/android-listview.php 自定义列表视图这种类型的列表视图需要额外的布局和自定义列表适配器来设置实现。 您可以使用以下链接实现它: http : //www.ezzylearning.com/tutorial.aspx...

如何在Android中制作传统的蒙古文字ListView(How to make a traditional Mongolian script ListView in Android)

更新 RecyclerViews具有水平布局。 因此,将Vertical Mongolian TextView放在其中一个中相对容易。 这是mongol-library一个例子。 有关使用RecyclerView制作水平滚动列表的一般解决方案,请参阅此答案 。 老答案 非常不幸的是Android API没有提供水平ListView。 不过,有很多StackOverflow问答都谈到了如何做。 以下是几个样本: 如何在Android中创建水平ListView? Android中的水平ListView...

Android:ListView具有奇怪的边距(Android: ListView has strange margins)

尝试删除 android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" 从你的RelativeLayout。 你说“所有方面”,我只能看到左/右。 但是,如果它的所有方面,你也必须删除“paddingTop”和“paddingBottom”。 Try to remove android:paddingLeft="@dimen/acti...

ListView prob - Android(ListView prob - Android)

删除ScrollView这是示例代码试试这个 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="match_parent" android:ori...

与listview android的相对布局(Relative layout with listview android)

尝试将android:layout_below="@+id/buttonFindReplacement"到ListView try adding android:layout_below="@+id/buttonFindReplacement" to your ListView

如何让Android ListView刷新(how to make Android ListView refresh)

你正在使用startActivity。 为什么不使用startActivityForResult呢? 这个问题的答案详细说明了如何调用第二个Activity。 一旦返回,您将不得不使用notifyDataSetChanged()调用。 You're using startActivity. Why don't you use startActivityForResult instead? The answer to this question details how you would call ...

相关文章

更多

Android开发学习之快速实现圆角ListView

一如既往,我们继续从微信当中寻找Android开发的思路,我们一起来看下面的这样一个效果。 这是微信里 ...

ListView具有多种item布局——实现微信对话列

这篇文章的效果也是大家常见的,各种通讯应用的对话列表都是这种方式,像微信、whatsapp、易信、米聊 ...

android的listview里怎么动态加载啊 就是先下载20个 滑到20了 在加载20个

android的listview里怎么动态加载啊 就是先下载20个 滑到20了 在加载20个????? ...

android的listview里怎么动态加载啊 就是先下载20个 滑到20了 在加载20个

android的listview里怎么动态加载啊 就是先下载20个 滑到20了 在加载20个 ...

如何在 ListView 中显示 RadioButton???

因为看到可以在 ListView 中显示 ImageView, TextView, Checkbox ...

[How to] Make custom search with Nutch(v 1.0)?(转)

http://puretech.paawak.com/2009/04/29/how-to-make-c ...

每日英语:Man Without a Country

'I'm like a proud son of China,' Taiwan-born direct ...

Becoming a data scientist

Data Week: Becoming a data scientist Data Pointed, ...

疯狂Android讲义

李刚编著的《疯狂Android讲义》全面地介绍了Android应用开发的相关知识,全书内容覆盖了And ...

Script.NET Perl解释器代码已经在GitHub开源发布

Script.NET Perl解释器的代码已经提交到GitHub网站。GitHub项目地址: http ...

最新问答

更多

Python / IPython奇怪的不可重现列表索引超出范围错误(Python/IPython strange non reproducible list index out of range error)

你得到IndexError的原因是你的输入文件显然不是完全用制表符分隔的。 这就是为什么当您尝试访问它时, splits[1]没有任何内容。 您的代码可以使用一些重构。 首先,你正在重复使用if -checks,这是不必要的。 这只是将cds0到7个字符,这可能不是你想要的。 我将以下内容放在一起,以演示如何重构您的代码,使其变得更加pythonic和干燥。 我无法保证它能够与您的数据集一起使用,但我希望它可以帮助您了解如何以不同的方式执行操作。 to_sort = [] # W

故事板从表格单元中延伸出来并显示其详细信息披露按钮(storyboard segue from a tablecell an its Detail Disclosure Button)

我不认为你可以链接一个特定的细节披露按钮瓦特/赛格。 我的故事板是非常程序化的B / C我使用了很多定制CGRect等。 所以我倾向于使用这样的东西: -(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { [self performSegueWithIdentifier:@"ViewControllerIdentifer"

我们可以将Gherkin功能文件与testcomplete集成(Can we integrate Gherkin feature files with testcomplete)

TestComplete不支持Gherkin功能文件。 但是,TestComplete具有SDK,因此可以自己为此创建扩展。 TestComplete does not support Gherkin feature files. However, TestComplete has SDK so it is possible to create an extension for this by yourself.

TrustAllCertificatesCallback被忽略(TrustAllCertificatesCallback is ignored)

我没有做过这样的事情,但看看我认为可以看出错误的例子。 试试这个代码: static class Program { static void Main() { var tcpclient = new TcpClient("remote.example.com", 443); var tcpstream = tcpclient.GetStream(); var sslstream = new SslStream(tcpstream,

返回嵌套元素的索引(Return index of nested element)

您需要获取父li元素的索引。 否则,您将获得列表项内锚点的索引,该索引始终为零。 $(this.parentNode).index(); You need to get the index of the parent li element. Otherwise you are getting the index of the anchor inside the list item, which will always be zero. $(this.parentNode).index();

在数组中重新编号元素的有效方法(Efficient way of re-numbering elements in an array)

您需要多次迭代列表,我认为没有办法解决这个问题。 毕竟,在开始更改元素(第二遍)之前,首先必须确定不同元素的数量(第一遍)。 但是请注意,由于对index的重复调用而not in列表中具有O(n),因此可能具有最多O(n ^ 2)的不同元素的数量。 或者,您可以使用dict而不是value_map的list 。 字典比列表具有更快的查找速度,因此,复杂性应该确实在O(n)的数量级上。 您可以使用(1)字典理解来确定旧值到新值的映射,以及(2)用于创建更新子项的列表理解。 value_map =

Express app定义的`static`文件夹无法正常工作(Express app defined `static` folder not working properly)

选项1 你可以改变这一行: app.use( express.static(__dirname + '/puplic')); //my public folder. 至 app.use('/oneTime', express.static(__dirname + '/puplic')); //my public folder 选项2 我假设你在公共文件夹中有一个js文件夹,然后你需要更改你的HTML代码:

使用node.js和socket.io每秒广播一次(Using node.js and socket.io to broadcast every second)

对于计时器值,请在服务器端本身每秒更新本地计时器。 每当有任何用户进来时,给他这个值以及计时器的总值。 然后客户端将根据dandavis评论在本地启动自己的计时器,但在服务器端保留一些间隔,如15或10秒,服务器将广播当前计时器值,以便客户端相应地进行同步。 简而言之,服务器将每隔10(n:你决定)秒后广播,但它将在本地每秒更新定时器变量。 每当客户端进入时,它将获得总计时器值和当前计时器值。 广播当前出价的休息功能可以以正常方式完成。 For timer value, keep updatin

如何让XMLSerializer将命名空间添加到嵌套对象中的属性?(How do I get the XMLSerializer to add namespaces to attributes in nested objects?)

IXmlSerializable也许? 注意我还添加了(对A ): [XmlElement("A", Namespace = "http://www.example.com/namespace")] public TestSoapHeaderTypeValuePair A {...} 代码如下: public class TestSoapHeaderTypeValuePair : IXmlSerializable { private string _type; private