如何判断View的当前方向(RTL/LTR)?

How to detemine the current direction of a View (RTL/LTR)?

背景

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

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

也可以获取视图的layout direction,如果开发者设置了它:

val layoutDirection = ViewCompat.getLayoutDirection(someView)

问题

视图的默认 layoutDirection 不基于其区域设置。实际上是 LAYOUT_DIRECTION_LTR .

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

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

我试过的

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

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

How could it be that by default, the direction is LTR, yet in practice it gets aligned to the right, in case the locale has changed?

区别在于时间。创建视图时,它会被分配一个默认值,直到解析出实际值。实际上维护了两个值:

  • getLayoutDirection() returns默认LAYOUT_DIRECTION_LTR,
  • getRawLayoutDirection()(隐藏API)returns LAYOUT_DIRECTION_INHERIT.

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

  • 直到找到具有具体值集的视图
  • 或直到它到达丢失的视图根(window,或ViewRootImpl)。

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

当视图层次附加到视图根时,它被分配的布局方向来自 Configuration object。换句话说,读取解析的布局方向只有在视图层次结构附加到 window.

后才有意义

How can I check if a given View's direction would be LTR or RTL , no matter what the developer has set (or not set) for it ?

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

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 下总是 returns false。

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

方案一:Post把它放到主线程消息队列中。我们假设在运行时,视图层次结构已附加到 window.

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() 的一部分解析称呼。其他回调(在 ActivityOnWindowAttachedListener 中) 保证该行为,所以不要使用它们。

更多问题的更多答案

Where does it get the value of getLayoutDirection and getRawLayoutDirection ?

View.getRawLayoutDirection()(隐藏 API)return 是您通过 View.setLayoutDirection() 设置的内容。默认情况下它是 LAYOUT_DIRECTION_INHERIT,这意味着 "inherit layout direction from my parent"。

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

Why is LAYOUT_DIRECTION_LTR the default value ?

历史上 Android 根本不支持 right-to-left 脚本(参见 here),left-to-right 是最合理的默认值。

Would the root of the views return something of the locale?

默认情况下,所有视图都继承其 parent 的布局方向。那么最顶层视图在附加之前从哪里获得布局方向?不行,不行。

当视图层次结构附加到 window 时,会发生这样的事情:

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

默认配置使用系统区域设置,布局方向取自该区域设置。然后将根视图设置为使用该布局方向。现在它所有的children和LAYOUT_DIRECTION_INHERIT都可以遍历解析到这个绝对值

Would some modifications of my small function be able to work even without the need to wait for the view to be ready?

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

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

@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,它存在于 AOSP 中,但可能不存在于供应商实现中。彻底测试这个!