如何确定视图的当前方向(RTL/LTR)? [英] How to detemine the current direction of a View (RTL/LTR)?

查看:165
本文介绍了如何确定视图的当前方向(RTL/LTR)?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景

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

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?

解决方案

默认情况下怎么可能是LTR方向,但实际上在区域设置发生变化的情况下它会向右对齐?

差异是及时的.创建视图时,将为其分配默认值,直到解析出实际值为止.实际上,有两个值保持不变:

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

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

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

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

将视图层次结构附加到视图根目录时,会从Configuration对象分配其布局方向.换句话说,只有在将视图层次结构附加到窗口之后,才能读取已解析的布局方向.

无论开发者为它设置(或未设置)什么内容,如何检查给定视图的方向是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:将其发布到主线程消息队列中.我们假设在运行时,视图层次结构已附加到窗口.

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()返回 resolved 的布局方向,即LOCATION_DIRECTION_LTR(也是默认值,直到实际解决)或LOCATION_DIRECTION_RTL.此方法不返回任何其他值.只有在视图是附加到视图根的视图层次结构的一部分中进行测量后,返回值才有意义.

为什么LAYOUT_DIRECTION_LTR是默认值?

从历史上看,Android根本不支持从右到左的脚本(请参见

layout direction of a view, if the developer has set it:

val layoutDirection = ViewCompat.getLayoutDirection(someView)

The problem

The default layoutDirection of a view isn't based on its locale. It's actually LAYOUT_DIRECTION_LTR .

When you change the locale of the device from LTR (Left-To-Right) locale (like English) to RTL (Right-To-Left) locale (like Arabic or Hebrew) , the views will get aligned accordingly, yet the values you get by default of the views will stay LTR...

This means that given a view, I don't see how it's possible to determine the correct direction it will go by.

What I've tried

I've made a simple POC. It has a LinearLayout with a 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>

In code, I write the direction of the locale, and of the views:

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"
            }
}

The result is that even when I switch to RTL locale (Hebrew - עברית), it prints this in logs:

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

And of course, the textView is aligned to the correct side, according to the current locale:

If it would have worked as I would imagine (meaning LAYOUT_DIRECTION_LOCALE by deafult), this code would have checked if a view is in RTL or not:

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
}

But it can't, because LTR is the default one, and yet it doesn't even matter...

So this code is wrong.

The questions

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

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

解决方案

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?

The difference is in time. When the view is created it's assigned a default value until the real value is resolved. Actually there are two values maintained:

  • getLayoutDirection() returns the default LAYOUT_DIRECTION_LTR,
  • getRawLayoutDirection() (hidden API) returns LAYOUT_DIRECTION_INHERIT.

When raw layout direction is LAYOUT_DIRECTION_INHERIT the actual layout direction is resolved as part of the measure call. The view then traverses its parents

  • until it finds a view which has a concrete value set
  • or until it reaches missing view root (the window, or ViewRootImpl).

In the second case, when the view hierarchy is not attached to a window yet, layout direction is not resolved and getLayoutDirection() still returns the default value. This is what happens in your sample code.

When view hierarchy is attached to view root, it is assigned layout direction from the Configuration object. In other words reading resolved layout direction only makes sense after the view hierarchy has been attached to 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 ?

First check, whether layout direction is resolved. If it is, you may work with the value.

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

Note that the method always returns false below Kitkat.

If layout direction is not resolved, you'll have to delay the check.

Option 1: Post it to the main thread message queue. We're assuming that by the time this runs, the view hierarchy has been attached to window.

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

Option 2: Get notified when the view hierarchy is ready to perform drawing. This is available on all API levels.

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
            }
        })

Note: You actually can subclass any View and override its onAttachedToWindow method, because layout direction is resolved as part of super.onAttachedToWindow() call. Other callbacks (in Activity or OnWindowAttachedListener) do not guarantee that behavior, so don't use them.

More answers to more questions

Where does it get the value of getLayoutDirection and getRawLayoutDirection ?

View.getRawLayoutDirection() (hidden API) returns what you set via View.setLayoutDirection(). By default it's LAYOUT_DIRECTION_INHERIT, which means "inherit layout direction from my parent".

View.getLayoutDirection() returns the resolved layout direction, that's either LOCATION_DIRECTION_LTR (also default, until actually resolved) or LOCATION_DIRECTION_RTL. This method does not return any other values. The return value only makes sense after a measurement happened while the view was part of a view hierarchy that's attached to a view root.

Why is LAYOUT_DIRECTION_LTR the default value ?

Historically Android didn't support right-to-left scripts at all (see here), left-to-right is the most sensible default value.

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

All views inherit their parent's layout direction by default. So where does the topmost view get the layout direction before it's attached? Nowhere, it can't.

When a view hierarchy is attached to window something like this happens:

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

Default configuration is set up with system locale and layout direction is taken from that locale. Root view is then set to use that layout direction. Now all its children with LAYOUT_DIRECTION_INHERIT can traverse and be resolved to this absolute value.

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

As explained in great detail above, sadly, no.

Edit: Your small function would look a little more like this:

@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()
        }

Now call View.getRealLayoutDirection() and get the value you were looking for.

Please note that this approach relies heavily on accessing hidden API which is present in AOSP but may not be present in vendor implementations. Test this thoroughly!

这篇关于如何确定视图的当前方向(RTL/LTR)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆