9

背景

可以使用以下方法获取当前的语言环境方向:

val isRtl=TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL

如果开发人员设置了它,也可以获取视图的布局方向:

val layoutDirection = ViewCompat.getLayoutDirection(someView)

问题

视图的默认 layoutDirection 不基于其语言环境。它实际上是LAYOUT_DIRECTION_LTR

当您将设备的区域设置从 LTR(从左到右)区域设置(如英语)更改为 RTL(从右到左)区域设置(如阿拉伯语或希伯来语)时,视图将相应对齐,但您的值默认情况下,视图将保持 LTR ...

这意味着给定一个视图,我看不出如何确定它经过的正确方向。

我试过的

我做了一个简单的 POC。它有一个带有 TextView 的 LinearLayout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/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:gravity="center_vertical" tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="Hello World!"/>

</LinearLayout>

在代码中,我写了语言环境和视图的方向:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL
        Log.d("AppLog", "locale direction:isRTL? $isRtl")
        Log.d("AppLog", "linearLayout direction:${layoutDirectionValueToStr(ViewCompat.getLayoutDirection(linearLayout))}")
        Log.d("AppLog", "textView direction:${layoutDirectionValueToStr(ViewCompat.getLayoutDirection(textView))}")
    }

    fun layoutDirectionValueToStr(layoutDirection: Int): String =
            when (layoutDirection) {
                ViewCompat.LAYOUT_DIRECTION_INHERIT -> "LAYOUT_DIRECTION_INHERIT"
                ViewCompat.LAYOUT_DIRECTION_LOCALE -> "LAYOUT_DIRECTION_LOCALE"
                ViewCompat.LAYOUT_DIRECTION_LTR -> "LAYOUT_DIRECTION_LTR"
                ViewCompat.LAYOUT_DIRECTION_RTL -> "LAYOUT_DIRECTION_RTL"
                else -> "unknown"
            }
}

结果是,即使我切换到 RTL 语言环境(希伯来语 - עברית),它也会在日志中打印:

locale direction:isRTL? true 
linearLayout direction:LAYOUT_DIRECTION_LTR 
textView direction:LAYOUT_DIRECTION_LTR

当然,根据当前的语言环境,textView 与正确的一侧对齐:

在此处输入图像描述

如果它可以像我想象的那样工作(默认为 LAYOUT_DIRECTION_LOCALE),则此代码将检查视图是否处于 RTL 中:

fun isRTL(v: View): Boolean = when (ViewCompat.getLayoutDirection(v)) {
    View.LAYOUT_DIRECTION_RTL -> true
    View.LAYOUT_DIRECTION_INHERIT -> isRTL(v.parent as View)
    View.LAYOUT_DIRECTION_LTR -> false
    View.LAYOUT_DIRECTION_LOCALE -> TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL
    else -> false
}

但它不能,因为 LTR 是默认的,但它甚至不重要......

所以这段代码是错误的。

问题

  1. 怎么可能默认情况下,方向是 LTR,但实际上它会向右对齐,以防语言环境发生变化?

  2. 无论开发人员为它设置(或未设置)什么,我如何检查给定视图的方向是 LTR 还是 RTL ?

4

1 回答 1

12

怎么可能默认情况下,方向是 LTR,但实际上它会向右对齐,以防语言环境发生变化?

区别在于时间。创建视图时,会为其分配一个默认值,直到解析出实际值。实际上有两个值保持:

  • getLayoutDirection()返回默认值LAYOUT_DIRECTION_LTR
  • getRawLayoutDirection()(隐藏 API)返回LAYOUT_DIRECTION_INHERIT.

当原始布局方向是LAYOUT_DIRECTION_INHERIT实际布局方向时,将作为measure调用的一部分进行解析。然后视图遍历其父视图

  • 直到找到具有具体值集的视图
  • 或直到它到达缺少的视图根(窗口,或ViewRootImpl)。

在第二种情况下,当视图层次结构尚未附加到窗口时,布局方向未解析并getLayoutDirection()仍返回默认值。这就是您的示例代码中发生的情况。

当视图层次附加到视图根时,它会从Configuration对象分配布局方向。换句话说,只有在视图层次结构附加到 window 之后,读取已解析的布局方向才有意义

无论开发人员为它设置(或未设置)什么,我如何检查给定视图的方向是 LTR 还是 RTL ?

首先检查布局方向是否解析。如果是,您可以使用该值。

if (ViewCompat.isLayoutDirectionResolved(view)) {
    val rtl = ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL
    // Use the resolved value.
} else {
    // Use one of the other options.
}

请注意,该方法在 Kitkat 下始终返回 false。

如果布局方向未解决,您将不得不延迟检查。

选项1:将其发布到主线程消息队列。我们假设在运行时,视图层次结构已附加到 window.xml 中。

view.post {
    val rtl = ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL
    // Use the resolved value.
}

选项 2:在视图层次结构准备好执行绘图时收到通知。这适用于所有 API 级别。

view.viewTreeObserver.addOnPreDrawListener(
        object : ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                view.viewTreeObserver.removeOnPreDrawListener(this)
                val rtl = ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL
                // Use the resolved value.
                return true
            }
        })

注意:您实际上可以继承任何方法View并覆盖其onAttachedToWindow方法,因为布局方向是作为super.onAttachedToWindow()调用的一部分解析的。其他回调(在Activity或中OnWindowAttachedListener保证这种行为,所以不要使用它们。

更多问题的更多答案

它从哪里获得 getLayoutDirection 和 getRawLayoutDirection 的值?

View.getRawLayoutDirection()(隐藏 API)返回您通过View.setLayoutDirection(). 默认情况下它是LAYOUT_DIRECTION_INHERIT,这意味着“从我的父级继承布局方向”。

View.getLayoutDirection()返回已解决的布局方向,即LOCATION_DIRECTION_LTR(也是默认值,直到实际解决)或LOCATION_DIRECTION_RTL. 此方法不返回任何其他值。只有在视图是附加到视图根的视图层次结构的一部分时发生测量后,返回值才有意义。

为什么 LAYOUT_DIRECTION_LTR 是默认值?

从历史上看,Android 根本不支持从右到左的脚本(参见此处),从左到右是最明智的默认值。

视图的根会返回一些语言环境吗?

默认情况下,所有视图都继承其父级的布局方向。那么最顶层视图在附加之前在哪里获得布局方向?无处可去。

当视图层次结构附加到窗口时,会发生以下情况:

final Configuration config = context.getResources().getConfiguration();
final int layoutDirection = config.getLayoutDirection();
rootView.setLayoutDirection(layoutDirection);

默认配置使用系统语言环境设置,布局方向取自该语言环境。然后将根视图设置为使用该布局方向。现在它的所有子节点都LAYOUT_DIRECTION_INHERIT可以遍历并解析到这个绝对值。

即使不需要等待视图准备好,我的小功能的一些修改也能工作吗?

正如上面详细解释的那样,遗憾的是,没有。

编辑:你的小函数看起来更像这样:

@get:RequiresApi(17)
private val getRawLayoutDirectionMethod: Method by lazy(LazyThreadSafetyMode.NONE) {
    // This method didn't exist until API 17. It's hidden API.
    View::class.java.getDeclaredMethod("getRawLayoutDirection")
}

val View.rawLayoutDirection: Int
    @TargetApi(17) get() = when {
        Build.VERSION.SDK_INT >= 17 -> {
            getRawLayoutDirectionMethod.invoke(this) as Int // Use hidden API.
        }
        Build.VERSION.SDK_INT >= 14 -> {
            layoutDirection // Until API 17 this method was hidden and returned raw value.
        }
        else -> ViewCompat.LAYOUT_DIRECTION_LTR // Until API 14 only LTR was a thing.
    }

@Suppress("DEPRECATION")
val Configuration.layoutDirectionCompat: Int
    get() = if (Build.VERSION.SDK_INT >= 17) {
        layoutDirection
    } else {
        TextUtilsCompat.getLayoutDirectionFromLocale(locale)
    }


private fun View.resolveLayoutDirection(): Int {
    val rawLayoutDirection = rawLayoutDirection
    return when (rawLayoutDirection) {
        ViewCompat.LAYOUT_DIRECTION_LTR,
        ViewCompat.LAYOUT_DIRECTION_RTL -> {
            // If it's set to absolute value, return the absolute value.
            rawLayoutDirection
        }
        ViewCompat.LAYOUT_DIRECTION_LOCALE -> {
            // This mimics the behavior of View class.
            TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
        }
        ViewCompat.LAYOUT_DIRECTION_INHERIT -> {
            // This mimics the behavior of View and ViewRootImpl classes.
            // Traverse parent views until we find an absolute value or _LOCALE.
            (parent as? View)?.resolveLayoutDirection() ?: run {
                // If we're not attached return the value from Configuration object.
                resources.configuration.layoutDirectionCompat
            }
        }
        else -> throw IllegalStateException()
    }
}

fun View.getRealLayoutDirection(): Int =
        if (ViewCompat.isLayoutDirectionResolved(this)) {
            layoutDirection
        } else {
            resolveLayoutDirection()
        }

现在调用View.getRealLayoutDirection()并获取您正在寻找的价值。

请注意,这种方法严重依赖于访问隐藏的 API,这些 API 存在于 AOSP 中,但可能不存在于供应商实现中。彻底测试这个!

于 2018-01-16T13:27:10.053 回答