Android右到左布局适配方案

"不曾停止"

Posted by Weiwq on December 10, 2018

“好久没有写blog了,感觉工作了都没有自己的时间了~~”

  呜呼,伊朗的项目终于做完了,大部分都是在整理右到左布局的需求。好在android sdk 从API17(Android4.2)开始支持右到左布局的需求,但是会有很多坑需要去填。

  Android中的大部分组件是支持右到左布局的,只需要在Androidmanifest中配置如下:

    <application
         ....
        android:supportsRtl="true">
    </application>

我们先看一个demo,MainActivity对应的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.example.test.activity.MainActivity">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:background="@color/fastlane_background"
        />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center_vertical"
        android:text="hello world "
        android:background="@color/selected_background"/>
</LinearLayout>

我们可以动态设置系统的语言来模仿用户环境,如下将app的配置设置为波斯语,当然,最简单的是到手机设置中直接设置当前语言为波斯语(设置有风险,语言需谨慎~~):

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Locale locale = new Locale("fa_IR");//fa_IR是波斯语(伊朗)的代码
        Locale.setDefault(locale);
        Configuration config = new Configuration();
        config.locale = locale;
        DisplayMetrics dm = getResources().getDisplayMetrics();
        getResources().updateConfiguration(config, dm);
        setContentView(R.layout.activity_main);
    }

以下是没有加android:supportsRtl=”true”的布局,很正常不过的布局吧?

在这里插入图片描述

那么加了之后呢?

在这里插入图片描述

显然toolbar的内容已经反过来了。且慢,textview好像没有任何变化?_?,难道textview不支持右到左布局?

1、textview gravity的右到左布局适配

大家知道textview有一个gravity的属性,其中可以通过以下两种方式指定textview的内容在左边还是在右边

  1) left,right:绝对位置,指定内容显示在textview的左边还是右边,这个比较好理解

  2) start,end:这个大家应该见过,该值作用的结果与系统布局方向有关,如下所示:

在这里插入图片描述

  举一反三,drawableStart,drawableEnd和layout_marginStart,layout_marginEnd以及layout_toStartOf,layout_toEndOf等也是同理,都是相对布局方向而言的。

  有同学马上就会想到,直接指定textview的gravity为start不就解决上面的问题了嘛!修改后的textview如下:

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center_vertical|start"
        android:text="hello world "
        android:background="@color/selected_background"/>

效果如下:

在这里插入图片描述

不对啊,这不按套路出牌呀。内容怎么还是在左边。。。

同学们发现没有,内容是英文的,而伊朗是用波斯语言,会不会与语言有关呢?我们再加一个textView显示波斯语言的:

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/fti_left_color"
        android:gravity="center_vertical|start"
        android:text="فارسی" />

结果如下:

在这里插入图片描述

是不是反过来了?由于波斯语言是右到左显示的,所以其内容也是默认居右边的。这说明,textview 的内容布局位置与文字显示的方向相关的。为了与伊朗用户的用户习惯保持一致,就算textview的内容是英文的,也应该居右边显示,那么如何做到呢?

Android提供了Java代码动态获取当前布局方向,如下:

    /**
    * 
    * @param context
    * @return 如果是右到左布局,返回true
    */
   public boolean isRtl(Context context) {
       return context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
   }

然后我们可以通过该方法设置textView的gravity

  textView.setGravity(isRtl(this) ? Gravity.RIGHT:Gravity.LEFT);

结果大家显然都知道了,这里就不列出来了。

但是这样还是觉得很麻烦,有没有更便捷的属性?有!

name description 描述
android:layoutDirection attribute for setting the direction of a component’s layout 设置组件的布局排列方向
android:textDirection attribute for setting the direction of a component’s text 设置组件的文字排列方向
android:textAlignment attribute for setting the alignment of a component’s text 设置文字的对齐方式
getLayoutDirectionFromLocale() method for getting the Locale-specified direction 获取指定地区的惯用布局方式

其中,android:layoutDirection和android:textDirection的值有:

value type description
inherit int Horizontal layout direction is inherited
继承水平方向
rtl int Horizontal layout direction is from Right to Left
布局方向是右到左
ltr int Horizontal layout direction is from Left to Right
布局方向是左到右
locale int Horizontal layout direction is deduced from the default language script for the locale
从区域设置的默认语言脚本推导出水平布局方向

android:textAlignment的值有:

Constant value Description
center 4 Center the paragraph, for example: ALIGN_CENTER.
居中
gravity 1 Default for the root view. The gravity determines the alignment, ALIGN_NORMAL, ALIGN_CENTER, or ALIGN_OPPOSITE, which are relative to each paragraph’s text direction.
由view通过gravity属性定义对齐方式
inherit 0 Default.
默认
textEnd 3 Align to the end of the paragraph, for example: ALIGN_OPPOSITE.
与段落的末尾对齐
textStart 2 Align to the start of the paragraph, for example: ALIGN_NORMAL.
与段落的开头对齐
viewEnd 6 Align to the end of the view, which is ALIGN_RIGHT if the view’s resolved layoutDirection is LTR, and ALIGN_LEFT otherwise.
与view的末尾对齐,
viewStart 5 Align to the start of the view, which is ALIGN_LEFT if the view’s resolved layoutDirection is LTR, and ALIGN_RIGHT otherwise.
与view的开头对齐

还是原来的布局,这里多加一行

  <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/selected_background"
        android:gravity="center_vertical|start"
        android:textDirection="locale"
        android:text="hello world " />

效果如下

在这里插入图片描述

要给辣么多个textview添加该属性,多麻烦!没事,有全局的设置~~

<resources>

    <style name="AppTheme" parent="@style/Theme.AppCompat.Light.NoActionBar" >
        <item name="android:textViewStyle">@style/TextDirection</item>
    </style>
    <style name="TextDirection" parent="android:Widget.TextView">
        <item name="android:textDirection">locale</item>
    </style>
</resources>

然后在Androidmanifest文件中引用该style

    <application
        ......
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        .......
    </application>

类似的还有EditText

    <style name="AppTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
         .....
        <item name="editTextStyle">@style/EditTextStyle</item>
    </style>

    <style name="EditTextStyle" parent="@android:style/Widget.EditText">
        <item name="android:textAlignment">viewStart</item>
        <item name="android:gravity">start</item>
        <item name="android:textDirection">locale</item>
    </style>

2、组件之间的相对位置

在处理组件之间的相对位置,一般会用到layout_marginLeft,layout_marginRight或者layout_toLeftOf,layout_toRightOf等。那么如果你的app需要兼顾右到左布局的时候,这些布局约束就不合适了,需要用Start,End来代替Left,Right。天呐,如果有几十上百个布局文件,不是要改死程序员了么?别怕,Android studio已经为我们提供了一键适配右到左布局的需求,如下图所示

在这里插入图片描述

然后会弹出一个对话框,直接点击“run”

在这里插入图片描述

接着会提示你哪些需要添加适配属性的,直接点击“Do Refactor”

在这里插入图片描述

如果有不需要修改的,可以选中不修改的文件,或者某一行代码。选择Remove即可

在这里插入图片描述

这样我们的布局文件就自定添加start、end属性。 如果布局中使用了drawableStart或者drawableEnd属性就需要注意了,需要考虑图标是否需要跟随布局方向变化而变化,例如作为方向标示的就不应该变化位置,因此需要依旧使用drawableLeft和drawableRight

3、start,end?

start,end不是已经讲过了吗?很简单,是相对布局方向的。真的是这样的吗?我们来看一段代码

<?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"
    tools:context=".activity.RelativeActivity">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="button1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginStart="100dp"
        android:layout_marginEnd="20dp"
        android:layout_toEndOf="@+id/button1"
        android:layoutDirection="ltr"
        android:paddingStart="50dp"
        android:text="button2"/>

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginStart="20dp"
        android:layout_toEndOf="@+id/button2"
        android:text="button3" />

</RelativeLayout>

预览效果如下

在这里插入图片描述

button2同时设置了layout_marginStart和paddingStart,并且指定layoutDirection的方向为ltr。一切是如此和谐~~跑出来的效果如下:

在这里插入图片描述

哎,怎么同是start,作用的位置与预期不一致呢?这里画一张图就明白了:

在这里插入图片描述

当然,如果不手动指定layoutDirection的方向,也就不会出现这个情况了。

从这里可以看出来:start,end是优先考虑view本身的布局方向,其次才是受父viewgroup的布局方向限制

4、布局约束的右到左布局适配

在开发中,有时需要动态移动view的位置,一般的做法是修改view的layout,设置x,y轴或者设置marginLayoutParams等,当然还有一般的动画。 还是我们的mainActivity,对应的布局有两个textview1,textview2,点击textview2会修改textview1的位置,对应的代码如下:

    private TextView textview1, textview2;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        Locale locale = new Locale("fa_IR");
//        Locale.setDefault(locale);
//        Configuration config = new Configuration();
//        config.locale = locale;
//        DisplayMetrics dm = getResources().getDisplayMetrics();
//        getResources().updateConfiguration(config, dm);
        setContentView(R.layout.activity_main);
        textview1 = findViewById(R.id.textview1);
        textview2 = findViewById(R.id.textview2);
    }
    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.textview2) {
            ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) textview1.getLayoutParams();
            marginLayoutParams.leftMargin += 20 ;
            marginLayoutParams.topMargin += 10;
            textview1.setLayoutParams(marginLayoutParams);
        }
    }

效果如果,我们成功的移动了textview1

在这里插入图片描述

我们再解除被注释掉的代码,再试试看:

在这里插入图片描述

怎么没有往右下角移动?_? 不是已经设置了leftMargin了么,怎么只有topMargin有效?

在右到左布局下,Android的会从右到左依次绘制view,虽然我们设置了leftMargin,但是,系统在绘制view的时候会以rightMargin约束条件为准,如下图所示,所以就导致了leftMargin无效的问题。

需要注意的是,在右到左布局下,系统坐标并没有改变,getLeft,getTop,getX,getY等值没有发生变化。

在这里插入图片描述

所以,我们需要稍作修改

  @Override
    public void onClick(View v) {
        if (v.getId() == R.id.textview2) {
            ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) textview1.getLayoutParams();
            marginLayoutParams.rightMargin += 20 ;
            marginLayoutParams.topMargin += 10;
            textview1.setLayoutParams(marginLayoutParams);
        }
    }

结果如下:

在这里插入图片描述

那如果在右到左布局下,textview的移动动画与左到右布局下一致呢?很简单,同时设置leftMargin和rightMargin即可:

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.textview2) {
            int screenWidth = getWindowManager().getDefaultDisplay().getWidth() ;
            ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) textview1.getLayoutParams();
            marginLayoutParams.leftMargin += 20;
            marginLayoutParams.rightMargin = screenWidth
                - textview1.getWidth()-marginLayoutParams.leftMargin ;
            marginLayoutParams.topMargin += 10;
            textview1.setLayoutParams(marginLayoutParams);
        }
    }

效果如下

在这里插入图片描述

5、英文,波斯文,阿拉伯数字混排

在项目中,由于英文是强左到右,波斯文是强右到左的,在混排的时候,就会存在文字方向的冲突,需要unicode来强制指定文字的方向,这里做总结。

unicode 说明 翻译:代码
\u202A 将英文,阿拉伯数字等非强右到左文字放在波斯文等强RTL文字的右边 早上6点: قبل از ظهر \u202A6
\u200F 也是将英文,阿拉伯数字放在波斯文的右边,区别是与波斯文之间没有空格 早上6点:قبل از ظهر\u200F6
\u202E 获取文字的镜像文 12: \u202E21

在显示文件路径时候,如果包含波斯语,会导致路径显示错误的情况,需要在string的前后加上\u202D 和\u202C(更多参考(UTF-8 常用标点符号))

        String content = "sdcard\\فارسی\\3D\\فارسی.mp3";
        textview1.setText("\u202D"+content+"\u202C");

实际显示如下:

在这里插入图片描述

或者使用如下接口

    // 获取ltr的字符串
    public static String formatLtrString(String content) {
        return BidiFormatter.getInstance().unicodeWrap(content, TextDirectionHeuristicsCompat.LTR);
    }

总结

1、如果使用到drawableLeft、drawableRight属性,请确定图标是否需要跟随系统布局方法改变而改变,如果是,请修改为drawableStart、drawableEnd;反之不用

2、在代码中需要动态修改view的布局约束,例如marginLayoutParams,layoutParams.setMargins()等,需要同时考虑left和right,或者采用修改layout的方式。

3、获取view的位置可以通过以下代码:

     int[] location = new int[2];
     view.getLocationOnScreen(location);
     float x = location[0];
     float y = location[1]

4、在relativelayout中添加layout rule的时候,用START_OF、END_OF代替LEFT_OF、RIGHT_OF。

5、在右到左布局下,其坐标布局方式不变,getLeft,getRight等不变。变的是布局约束,view的绘制受right(start)约束。

6、一些方向图标,重新做一个相对方向的放到 mipmap-ldrtl-xxxhdpi 包下

7、textview,editText需要对内容做RTL处理,设置style,详情见第一节

8、如果需要修改view的位置,请不要使用setX,setY方式。推荐采用ViewGroup.MarginLayoutParams来设置margin,或者采用layout的方式。同时需要注意该约束是相对父view而言的。

9、判断字符串是否是强RTl,常常用于控制textDirection

   /**
     * 判断是否是强右到左字符
     * 如果包含强右到左字符,返回true,反之返回false
     * @param content 字符
     * @return 是否是右到左字符
     */
    protected boolean isRtlString(String content) {
        if (TextUtils.isEmpty(content)) {
            return false;
        }
        return TextDirectionHeuristics.ANYRTL_LTR.isRtl(content, 0, content.length());
    }
}

—— Weiwq 于 2018.12 广州