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

时间:2018-01-16 08:51:38

标签: android android-layout locale right-to-left left-to-right

背景

可以使用以下方法获取当前的区域设置方向:

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。它有一个带有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

如果它可以像我想象的那样工作(意思是deafult的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. 无论开发人员为其设置(或未设置),我如何检查给定View的方向是LTR还是RTL?

1 个答案:

答案 0 :(得分:7)

  

默认情况下,方向是LTR,但实际上它是否与右边对齐,以防语言环境发生变化?

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

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

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

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

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

当视图层次结构附加到视图根时,将为Configuration对象分配布局方向。换句话说,读取已解决的布局方向仅在将视图层次结构附加到窗口后才有意义。

  

无论开发人员为其设置(或未设置),我如何检查给定View的方向是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()返回已解决的布局方向,即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()并获取您要查找的值。

请注意,此方法在很大程度上依赖于访问AOSP中存在的隐藏API,但可能不会出现在供应商实现中。彻底测试!