在海外发行App,对App进行多言语适配是必不可少的。多言语的适配其实不仅仅仅仅将文本内容进行翻译这么简略,在运用阿拉伯语或希伯来语的区域,用户的阅读习气是从右到左,为了更好地用户体验,App还应该对布局完成RTL(Right-to-Left)适配。本文简略介绍怎么进行RTL适配。

敞开RTL支持

AndroidManifest中增加配置android:supportsRtl,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        ......
        android:supportsRtl="true">
    </application>
</manifest>

控件适配

从Android 4.2开端,大部分安卓提供的控件现已自动适配了RTL,咱们需求做的是在布局文件中,将原本运用left或right声明的特点改为start或end。

示例:

<androidx.constraintlayout.widget.ConstraintLayout 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">
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv_use_left_right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:layout_marginTop="20dp"
        android:background="@color/color_00A5FF"
        android:padding="10dp"
        android:text="use left or right"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv_use_start_end"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:layout_marginTop="20dp"
        android:background="@color/color_49E284"
        android:padding="10dp"
        android:text="use start or end"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_use_left_right" />
</androidx.constraintlayout.widget.ConstraintLayout>

作用如图:

LTR RTL
Android — RTL适配笔记
Android — RTL适配笔记

文本适配

数字文本

某些文本或许仅包括纯数字(例如消息数量),能够运用String.format()转换为对应言语的数字文本。

示例:

class AdapterRtlExampleActivity : AppCompatActivity() {
    private lateinit var binding: LayoutAdapterRtlExampleActivityBinding
    private val exampleInt = 100102
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutAdapterRtlExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }
        binding.tvNumberExample.text = "$exampleInt"
        binding.tvNumberFormatExample.text = String.format("%d", exampleInt)
    }
}

作用如图:

英语 阿语
Android — RTL适配笔记
Android — RTL适配笔记

混合言语文本

显现的文本或许包括多种言语,能够通过BidiFormatter.unicodeWrap()进行格式化。

示例:

要显现”收货地址为:15 Bay Street, Laurel, CA”。

class AdapterRtlExampleActivity : AppCompatActivity() {
    private lateinit var binding: LayoutAdapterRtlExampleActivityBinding
    private val exampleText = "15 Bay Street, Laurel, CA"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutAdapterRtlExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }
        binding.tvMultiLanguage.text = getString(R.string.adapter_rlt_test, exampleText)
        binding.tvMultiLanguageFormat.text = getString(R.string.adapter_rlt_test, BidiFormatter.getInstance().unicodeWrap(exampleText))
    }
}

体系言语为阿语时,作用如图:

  • 蓝底 —— 未运用BidiFormatter
  • 绿底 —— 运用BidiFormatter
Android — RTL适配笔记

自定义View适配

自定义View通常会通过onLayout或许onDraw方法来制作View,自定义的制作需求对RTL进行适配。本文以之前文章中完成的ExpandableFlowLayout为例,适配后代码如下:

  • ExpandableFlowLayout
class ExpandableFlowLayout : ViewGroup {
    private val defaultVerticalSpace = paddingTop + paddingBottom
    private val defaultHorizontalSpace = paddingStart + paddingEnd
    private var defaultShowRow = 2
    private var measureNeedExpandView = false
    var expand = false
    private var expandView: View
    private var elementDividerVertical: Int = DensityUtil.dp2Px(8)
    private var elementDividerHorizontal: Int = DensityUtil.dp2Px(8)
    private var isRtl = false
    var elementClickCallback: ((content: String) -> Unit)? = null
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        context.obtainStyledAttributes(attrs, R.styleable.ExpandableFlowLayout).run {
            defaultShowRow = getInt(R.styleable.ExpandableFlowLayout_default_show_row, 2)
            expand = getBoolean(R.styleable.ExpandableFlowLayout_default_expand_status, false)
            elementDividerVertical = getDimensionPixelSize(R.styleable.ExpandableFlowLayout_element_divider_vertical, DensityUtil.dp2Px(8))
            elementDividerHorizontal = getDimensionPixelSize(R.styleable.ExpandableFlowLayout_element_divider_horizontal, DensityUtil.dp2Px(8))
            recycle()
        }
        expandView = AppCompatImageView(context).apply {
            layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, DensityUtil.dp2Px(30))
            setImageResource(R.mipmap.icon_triangular_arrow_down)
            rotation = if (!expand) 0f else 180f
            setOnClickListener {
                expand = !expand
                rotation = if (!expand) 0f else 180f
                requestLayout()
            }
        }
        // 判别当时是否为RTL
        isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val rootWidth = MeasureSpec.getSize(widthMeasureSpec)
        var usedWidth = defaultHorizontalSpace
        var usedHeight = defaultVerticalSpace
        measureChild(expandView, widthMeasureSpec, heightMeasureSpec)
        var rowCount = 1
        for (index in 0 until childCount - 1) {
            val childView = getChildAt(index)
            if (childView != null) {
                // 丈量当时子控件的宽高。
                measureChild(childView, widthMeasureSpec, heightMeasureSpec)
                val realChildViewUsedWidth = childView.measuredWidth + elementDividerHorizontal
                val realChildViewUsedHeight = childView.measuredHeight + elementDividerVertical
                if (usedHeight == defaultVerticalSpace) {
                    usedHeight += realChildViewUsedHeight
                }
                // 当时子控件宽度加上之前已用宽度大于根布局宽度,需求换行。
                if (usedWidth + realChildViewUsedWidth > rootWidth) {
                    // 换行
                    rowCount++
                    // 当时为未打开状况,并且此刻行数现已超过了默许显现行数,越过后续的丈量。
                    if (!expand && rowCount > defaultShowRow) {
                        break
                    }
                    // 重置已用宽度
                    usedWidth = defaultHorizontalSpace
                    // 增加已用高度
                    usedHeight += realChildViewUsedHeight
                }
                usedWidth += realChildViewUsedWidth
                if (index == childCount - 2 && expand && rowCount > defaultShowRow) {
                    // 打开状况下的最终一个元素,
                    // 此刻判别能否再放下打开控件,不能则需求增加一行用于显现打开控件。
                    if (usedWidth + expandView.measuredWidth > rootWidth) {
                        usedHeight += expandView.measuredHeight + elementDividerVertical
                    }
                }
            }
        }
        measureNeedExpandView = rowCount > defaultShowRow
        setMeasuredDimension(rootWidth, usedHeight)
    }
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        val availableWidth = right - left
        var usedWidth = defaultHorizontalSpace
        // RTL形式下,从右侧开端增加View
        // 需求注意的是,LTR和RTL形式下,marginStart、marginEnd、paddingStart和PaddingEnd获取的值是共同的
        // 因而需求自己处理不同形式下的边距
        var positionX = if (isRtl) availableWidth - paddingEnd else paddingStart
        var positionY = paddingTop
        var rowCount = 1
        for (index in 0 until childCount - 1) {
            val childView = getChildAt(index)
            if (childView != null) {
                val realChildViewUsedWidth = childView.measuredWidth + elementDividerHorizontal
                val realChildViewUsedHeight = childView.measuredHeight + elementDividerVertical
                val changeRowCondition = if ((!expand && rowCount == defaultShowRow)) {
                    // 未打开状况,并且当时行现已是默许显现行,已用空间需求加上打开控件的空间
                    usedWidth + realChildViewUsedWidth + (if (measureNeedExpandView) expandView.measuredWidth else 0) > availableWidth
                } else {
                    usedWidth + realChildViewUsedWidth > availableWidth
                }
                if (changeRowCondition) {
                    // 换行
                    rowCount++
                    // 当时为未打开状况,并且此刻行数现已超过了默许显现行数,越过后续处理
                    if (!expand && rowCount > defaultShowRow) {
                        childView.layout(0, 0, 0, 0)
                        break
                    }
                    // 重置已用宽度
                    usedWidth = defaultHorizontalSpace
                    // 新行开端的x轴坐标重置
                    positionX = if (isRtl) availableWidth - paddingEnd else paddingStart
                    // 新行开端的y轴坐标增加
                    positionY += realChildViewUsedHeight
                }
                if (isRtl) {
                    // RTL形式下,从右侧开端增加View
                    childView.layout(positionX - childView.measuredWidth, positionY, positionX, positionY + childView.measuredHeight)
                    positionX -= realChildViewUsedWidth
                } else {
                    childView.layout(positionX, positionY, positionX + childView.measuredWidth, positionY + childView.measuredHeight)
                    positionX += realChildViewUsedWidth
                }
                usedWidth += realChildViewUsedWidth
                if (index == childCount - 2 && expand && rowCount > defaultShowRow) {
                    // 打开状况下的最终一个元素,
                    // 此刻判别能否再放下打开控件,不能则需求增加一行用于显现打开控件。
                    if (usedWidth + expandView.measuredWidth > availableWidth) {
                        positionX = if (isRtl) availableWidth - paddingEnd else paddingStart
                        // 新行开端的y轴坐标增加
                        positionY += realChildViewUsedHeight
                    }
                }
            }
        }
        if (measureNeedExpandView) {
            if (isRtl) {
                // RTL形式下,从右侧开端增加View
                expandView.layout(positionX - expandView.measuredWidth, positionY, positionX, positionY + expandView.measuredHeight)
            } else {
                expandView.layout(positionX, positionY, positionX + expandView.measuredWidth, positionY + expandView.measuredHeight)
            }
        } else {
            expandView.layout(0, 0, 0, 0)
        }
    }
    @SuppressLint("InflateParams")
    fun setData(data: List<String>) {
        removeAllViews()
        for (content in data) {
            LayoutInflater.from(context).inflate(R.layout.layout_example_flow_item, null, false).apply {
                layoutParams = MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, DensityUtil.dp2Px(30))
                findViewById<AppCompatTextView>(R.id.tv_example_flow_item_content).run {
                    text = content
                    gravity = Gravity.CENTER_VERTICAL
                    setOnClickListener {
                        elementClickCallback?.invoke(content)
                    }
                }
                addView(this)
            }
        }
        addView(expandView)
    }
}
  • 示例页面
class AdapterRtlExampleActivity : AppCompatActivity() {
    private lateinit var binding: LayoutAdapterRtlExampleActivityBinding
    private val exampleData = arrayOf("测验测验测验测验", "aadaada", "hahaha", "这是一个测验数据", "yyddd", "测验用测验用", "test data", "example", "akdjfj", "yyds")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutAdapterRtlExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }
        binding.btnAddData.setOnClickListener {
            val data = ArrayList<String>()
            // 从测验数据中随机生成8个元素
            repeat(8) {
                data.add(exampleData.random())
            }
            binding.eflExampleDataContainer.setData(data)
        }
    }
}

作用如图:

LTR RTL
Android — RTL适配笔记
Android — RTL适配笔记

示例

演示代码已在示例Demo中增加。

ExampleDemo github

ExampleDemo gitee