如何全局拦截资源加载? [英] How to intercept resource loading globally?

查看:29
本文介绍了如何全局拦截资源加载?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要使用翻译 SDK(Lokalise,文档 此处) 旨在从其服务器加载字符串资源.

I need to use a translations-SDK (Lokalise, docs here) that is intended to load strings resources from their servers.

这意味着如果您使用 getString ,它将更喜欢服务器上的内容而不是应用程序上的内容.这也包括布局 XML 文件膨胀的情况.

This means that if you use getString , it will prefer what's on the server instead of what's on the app. This includes also the cases of inflation of layout XML files.

Android 似乎没有我可以使用的全局资源处理.这就是 SDK 说我应该使用其中之一的原因:

It seems that Android doesn't have a global resource handling that I can use. This is why the SDK says I should use one of these :

  1. 对于Activity,我可以覆盖attachBaseContext的回调.
  2. 对于所有其他情况,我需要获取它们的资源,我可以使用 LokaliseResources(context) .
  1. For Activity, I can override the callback of attachBaseContext.
  2. For all other cases, that I need to get the resources of them, I can use LokaliseResources(context) .

事实是,我工作的应用程序中的很多代码都不涉及 Activity.应用上的许多 UI 都是浮动的(使用 SAW 权限,也称为系统警报窗口").

Thing is, a lot of code in the app I work on doesn't involve an Activity. A lot of the UI on the app is floating (using SAW permission, AKA "System Alert Window").

这意味着仅使用 Application 类会导致视图大量膨胀.

This means that there is a lot of inflation of Views using just the Application class.

首先我为此做了一个简单的管理器:

First I made a simple manager for this:

object TranslationsManager {
    var resources: LokaliseResources? = null

    @UiThread
    fun initOnAppOnCreate(context: App) {
        Lokalise.init(context, Keys.LOCALISE_SDK_TOKEN, Keys.LOCALISE_PROJECT_ID)
        Lokalise.updateTranslations()
        resources = LokaliseResources(context)
    }

    fun getResources(context: Context): Resources {
        return resources ?: context.resources
    }
}

我尝试使用库执行各种操作,但它们崩溃了,因为这不是库的工作方式.

I tried to perform various things using the library, but they crashed as it's not how the library works.

所以这些都失败了:

  1. 对于扩展Application的类的getResources,我尝试返回SDK之一

  1. For the getResources of the class that extends Application, I tried to return the one of the SDK

使用实现应用程序的类的attachBaseContext.这会导致崩溃,因为它之前需要初始化,所以我尝试在这个回调中正确初始化它,但仍然崩溃.

Use attachBaseContext of the class that implements Application. This causes a crash since it needs to be initialized before, so I tried to initialize it right in this callback, but still got a crash.

对于 LayoutInflater,我尝试使用 LayoutInflater.from(new ContextThemeWrapper(...)) 并覆盖它的 getResources 回调,但它没有什么都不做.

For LayoutInflater, I tried to use LayoutInflater.from(new ContextThemeWrapper(...)) , and override its getResources callback, but it didn't do anything.

我尝试使用 Philology 库:

object MyPhilologyRepositoryFactory : PhilologyRepositoryFactory {
    override fun getPhilologyRepository(locale: Locale): PhilologyRepository {
        return object : PhilologyRepository {
            override fun getPlural(key: String, quantityString: String): CharSequence? {
                Log.d("AppLog", "getPlural $key")
                return TranslationsManager.resources?.getString(quantityString)
                    ?: super.getPlural(key, quantityString)
            }

            override fun getText(key: String): CharSequence? {
                Log.d("AppLog", "getText $key")
                return TranslationsManager.resources?.getString(key) ?: super.getText(key)
            }

            override fun getTextArray(key: String): Array<CharSequence>? {
                Log.d("AppLog", "getTextArray $key")
                TranslationsManager.resources?.getStringArray(key)?.let { stringArray ->
                    val result = Array<CharSequence>(stringArray.size) { index ->
                        stringArray[index]
                    }
                    return result
                }
                return super.getTextArray(key)
            }
        }
    }
}

在扩展 Application 的类上,使用这个:

And on the class that extends Application, use this:

Philology.init(MyPhilologyRepositoryFactory)
ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor).build())

但是当在应用程序中(实际上到处都是)使用了通货膨胀时,我从来没有看到有人使用过这段代码.

But when inflation was used in the app (and actually everywhere), I never saw that this code is being used, ever.

话虽如此,这就是我成功的地方:

That being said, this is what I've succeeded:

1.对于所有活动/服务,我确实添加了 attachBaseContext 的用法,正如 SDK 所说:

1.For all Activities/Services, indeed I've added usage of attachBaseContext as the SDK says:

override fun attachBaseContext(newBase: Context) {
    super.attachBaseContext(LokaliseContextWrapper.wrap(newBase))
}

2.对于所有自定义视图,我已经使用了我所做的:

2.For all custom views, I've used what I've made:

override fun getResources(): Resources {
    return TranslationsManager.getResources(context)
}

这两个都花了相当长的时间来手动查找和添加,一个接一个.

Both of these took quite some time to find and add manually, one after another.

遗憾的是,似乎还有一些重要的案例.

Sadly, still there seem to be some important cases.

我发现至少对于布局膨胀(例如在自定义视图中),布局 XML 文件不会从 SDK 中获取资源.

I've found that at least for layout inflation (in the custom views, for example), the layout XML files don't take the resources from the SDK.

我发现了一篇文章为字符串操作驯服 Android 资源和 LayoutInflater";从 2020 年开始(缓存 这里) 说我可以使用 ContextThemeWrapper 的一些技巧比我尝试的要复杂一点,但遗憾的是它缺少一些我未能使用的重要信息(例如 cloneInContext 的实现):

I've found an article "Taming Android Resources and LayoutInflater for string manipulation" from 2020 (cache here) saying I could use some trick of ContextThemeWrapper a bit more complex than what I tried, but sadly it lacks some important information (implementation of cloneInContext for example) that I've failed to use:

class CustomContextWrapper(
    private val base: Context,
    private val dynamicStringMap: Map<String, String>
) : ContextWrapper(base) {

    override fun getResources() = CustomResources(base.resources, dynamicStringMap)

    override fun getSystemService(name: String): Any? {
        if (Context.LAYOUT_INFLATER_SERVICE == name) {
            return CustomLayoutInflater(LayoutInflater.from(baseContext), this)
        }
        return super.getSystemService(name)
    }
}

class CustomLayoutInflater constructor(
    original: LayoutInflater,
    newContext: Context,
) : LayoutInflater(original, newContext) {
    override fun cloneInContext(p0: Context?): LayoutInflater {
        TODO("Not yet implemented")
    }

    override fun onCreateView(name: String, attrs: AttributeSet): View? {
        try {
            val view = createView(name, "android.widget.", attrs)
            if (view is TextView) {
                // Here we get original TextView and then return it after overriding text
                return overrideTextView(view, attrs)
            }
        } catch (e: ClassNotFoundException) {
        } catch (inflateException: InflateException) {
        }
        return super.onCreateView(name, attrs)
    }

    private fun overrideTextView(view: TextView, attrs: AttributeSet?): TextView {
        val typedArray =
            view.context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.text))
        val stringResource = typedArray.getResourceId(0, -1)
        view.text = view.resources.getText(stringResource)
        typedArray.recycle()
        return view
    }
}

但是,它说我可以使用名为ViewPump"的库.(这里,它实际上建议使用 语言学图书馆 这里) 对我有用,从 Android 30 开始,我们可以使用 ResourcesProviderResourcesLoader 类.遗憾的是,我找不到将其中任何一个用于我正在研究的目的的示例.

However, it said I could use a library called "ViewPump" (here, and it actually suggested to use Philology library here) that will do the trick for me, and that from Android 30 we could use ResourcesProvider and ResourcesLoader classes. Sadly I couldn't find an example to use any of these for the purpose I'm working on.

  1. 文章中提到的技巧真的可以使用吗?应该怎么做才能正确使用?

  1. Is it really possible to use the trick that was mentioned on the article? What should be done to use it properly?

如何使用ViewPump"/Philology"?库来实现同样的事情?

How can I use the "ViewPump"/"Philology" library to achieve the same thing?

有没有办法在全球范围内提供资源而不是使用我提到的所有资源?以便所有资源都将使用翻译 SDK,无论我在哪里以及如何获取资源?这已经花费了很多时间,因为我需要复习许多课程并自己添加资源处理...

Is there any way to offer resources globally instead of using all that I've mentioned? So that all resources will be using the translation SDK, no matter where and how I reach the resources ? This takes a lot of time already, as I need to go over many classes and add handling of resources myself...

以上任何一项是否涵盖所有情况?例如不仅是通货膨胀,还有其他情况,例如 TextView.setText(resId) ?

Will any of the above cover all cases? For example not just the inflation, but other cases such as TextView.setText(resId) ?

至于 Android API 30 的新类,因为它们非常新,我决定在一个新帖子中询问它们,此处.

As for the new classes of Android API 30, because they are very new, I've decided to ask about them in a new post, here.

与 Lokalise 支持人员交谈时,他们说他们已经在使用 ViewPump,这意味着它可能适用于与我所拥有的不匹配的情况.

Talking with Lokalise support, they said they already do use ViewPump, which means that it probably works in cases that don't match what I have.

推荐答案

我发现结合使用 ViewPump 用您的 ContextWrapper 包装正在膨胀的视图的上下文.

I've found success with a combination of using ViewPump to wrap the context of the view being inflated with your ContextWrapper.

class ContextWrappingViewInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): InflateResult {
        val request = chain.request()

        val newRequest = request.toBuilder()
            .context(MyContextWrapper.wrap(request.context))
            .build()

        return chain.proceed(newRequest)
    }
}

但是,我还没有找到强制自定义视图属性免费使用您的上下文的解决方案.问题是在内部,样式属性从已经通过 XML 文件在内部缓存的内容中获取其资源.意思是,视图的上下文根本不涉及它.

However I haven't found a solution to force custom view attributes to use your context for free. The issue is that internally, styled attributes fetch their resources from what has already been cached internally via XML files. Meaning, the view's context doesn't come into it at all.

一种解决方法是从样式属性中获取资源 ID,然后将实际资源获取委托给上下文.

A workaround for this is to fetch the resource ID from styled attributes and then delegate the actual resource fetching to context.

fun TypedArray.getStringUsingContext(context: Context, index: Int): String? {
    if (hasValue(index)) {
        return getResourceId(index, 0)
            .takeIf { it != 0 }
            ?.let { context.getString(it) }
    }
    return null
}

在自定义视图中的使用:

Usage in CustomView:

init {
    context.obtainStyledAttributes(attrs, R.styleable.CustomView).use { array ->
        val myText = array.getStringUsingContext(context, R.styleable.CustomView_myText)
        ...
    }
}

这篇关于如何全局拦截资源加载?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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