在海外发行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 |
---|---|
文本适配
数字文本
某些文本或许仅包括纯数字(例如消息数量),能够运用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)
}
}
作用如图:
英语 | 阿语 |
---|---|
混合言语文本
显现的文本或许包括多种言语,能够通过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
自定义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 |
---|---|
示例
演示代码已在示例Demo中增加。