如何确定视图的当前方向(RTL / LTR)?

12

背景

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

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

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

val layoutDirection = ViewCompat.getLayoutDirection(someView)

问题

视图的默认布局方向不是基于其语言环境的。实际上,它是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会被对齐到正确的一侧:

enter image description here

如果它按照我的想象工作(意味着默认情况下采用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,无论开发人员为其设置了什么(或未设置)?


你可以尝试使用getResources().getConfiguration().locale替换Locale.getDefault()吗?这样能正确返回RTL吗? - Eugen Pechanec
哦,对不起,我漏了。您在清单文件中的应用程序标签上是否有 android:supportsRtl="true"?如果您查看 View.resolveLayoutDirection 的源代码,它只在这种情况下起作用。 - Eugen Pechanec
@EugenPechanec 对于 onResume 的结果是一样的。然而,如果我使用 Handler().postDelayed(...,1000) ,就能正常工作。有没有办法尽快检查它?即使在 onCreate 中?为什么默认是从左到右?这个值应该只有在需要时才强制设置,不是吗? - android developer
不错,我怀疑在调用View.getLayoutDirection之前通过反射调用View.resolveRtlPropertiesIfNeeded应该能解决问题。 - Eugen Pechanec
有没有可能立即进行,不需要任何延迟?就像在特殊情况下可以进行测量一样...? - android developer
显示剩余5条评论
1个回答

15
默认情况下,方向为LTR,但实际上,如果区域设置更改,则会对齐到右侧。这是因为视图创建时分配了默认值,直到解析出实际值。事实上,有两个维护的值:getLayoutDirection()返回默认LAYOUT_DIRECTION_LTR,getRawLayoutDirection()(隐藏API)返回LAYOUT_DIRECTION_INHERIT。当原始布局方向为LAYOUT_DIRECTION_INHERIT时,实际布局方向作为measure调用的一部分解析。然后,视图遍历其父级,直到找到具有具体值的视图或达到缺少的视图根(窗口或ViewRootImpl)。在第二种情况下,当视图层次结构尚未附加到窗口时,不会解析布局方向,getLayoutDirection()仍返回默认值。这就是您示例代码中发生的情况。当视图层次结构附加到视图根时,它会从Configuration对象中分配布局方向。换句话说,在视图层次结构附加到窗口之后读取已解析的布局方向才有意义。首先检查是否解析了布局方向。如果是,则可以使用该值。
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:将其发布到主线程消息队列。我们假设在运行此代码时,视图层次结构已附加到窗口。

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中)不保证该行为,因此请勿使用它们。

更多问题的答案

getLayoutDirection和getRawLayoutDirection的值从哪里获取?

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

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

为什么LAYOUT_DIRECTION_LTR是默认值?

从历史上看,Android根本不支持从右到左的脚本(请参见here),从左到右是最合理的默认值。

视图根会返回区域设置的内容吗?

所有视图默认继承其父视图的布局方向。那么最顶层的视图在附加之前从哪里获取布局方向呢?它无处可得。

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

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中,但可能不存在于供应商实现中。务必进行彻底测试!


哇,Kotlin 反射看起来比 Java 还糟糕。不过,有没有一种方法可以直接获取方向,而不需要反射?也许我们可以像测量视图大小一样(在某些情况下可能)获取方向? - android developer
实际上,反射甚至不到所有内容的四行,比Java更干净。getLayoutDirection有一个粗糙的历史,导致了这个混乱。|||再次强调,在视图被附加之前,没有公共API可以提供已解决的布局方向。|||我从未问过,这有什么用途?如果应用程序在您的控制下,并且您从未手动设置布局方向,则可以安全地假定它是从语言环境中推断出来的。 - Eugen Pechanec
针对RecyclerView的吸附,需要使用一个特殊的类:https://dev59.com/Cek5XIcBkEYKwwoY8ejo#47580753。它需要根据RecyclerView的正确方向来确定如何工作。也许可以稍后检查方向,而不是立即在那里检查。该类的创建者使用了`getLayoutDirection`,我告诉他这是不正确的,因为当我更改设备的语言环境时,它变得无法使用。然而,即使使用了我的解决方案,这在技术上仍然是不正确的,因为它没有考虑到开发人员设置。 - android developer
无论如何,我已将此答案标记为被接受的,尽管我希望有更好的方法。 - android developer
@androiddeveloper 如果我找到SnapToBlock的更多信息,我会告诉你。就我所知,它使用我之前提到的Option 2来解决布局方向问题。 - Eugen Pechanec
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接