前言

前段时间接到一个需求,要动态的增加多语言语种,且可以动态更新用户app上的不规范语言(比如一个英语过长导致按钮内显示不全)

服务端的逻辑和前端动态获取的逻辑就不说了

修改app内的语言参考这篇安卓多语言设置,深渊巨坑,适配7.0以上,并且解决因WebView产生的问题

正文

首先确定一下安卓中有几种获取字符串的方式

1.Context#getResources().getString()

2.Context#getString()//其实内部还是用的上一种方式,只不过少写了一点代码

3.xml#android:text="@string/xxx"

那其实算下来只有两种,一种是通过resources来获取String,另一种是在xml解析的时候获取String

第一种:通过包装并拦截resources来Hook getString()

原理其实也很简单:

通过观察源码,发现Context每次调用getString()之前都会先调用一下getResources(),这时只要重写一下Application和BaseActivity的getResources(),返回自己包装的resources,即可拦截到getString()方法,然后通过资源id获取资源名,查找服务器返回的对应key的value,如果有就返回服务器的,没有则使用应用自身的.

至于为什么不需要重写BaseFragment和BaseDialog的,因为BaseFragment的getResources()会调用requireContext(),而requireContext()返回的Context对象就是附加到的Activity对象,所以不用设置,同理Dialog需要在构造的时候传入Activity,so

ps:既然Fragment是每次调用getString()都会调用getContext(),那这样在Fragment销毁之后就不能调用getString()了,否则就会抛异常!

代码如下:

BaseActivity为例:private var resources: Resources? = nulloverride fun getResources(): Resources {if (resources == null) {resources = LanguageResources(super.getResources())}return resources!!}资源包装类LanguageResources如下:/*** creator: lt  lt.dygzs@qq.com* 通过拦截appResources来拦截字符串*/class LanguageResources(val resources: Resources): Resources(resources.assets, resources.displayMetrics, resources.configuration) {override fun getText(id: Int): CharSequence {resources.getResourceEntryName(id)?.let { thisLanguageKVMap[it]?.let { return it } }return resources.getText(id)}override fun getText(id: Int, def: CharSequence?): CharSequence? {resources.getResourceEntryName(id)?.let { thisLanguageKVMap[it]?.let { return it } }return resources.getText(id, def)}override fun getString(id: Int): String {resources.getResourceEntryName(id)?.let { thisLanguageKVMap[it]?.let { return it } }return resources.getString(id)}override fun getString(id: Int, vararg formatArgs: Any?): String {resources.getResourceEntryName(id)?.let { thisLanguageKVMap[it]?.let { return it.format(*formatArgs) } }return resources.getString(id, *formatArgs)}override fun getTextArray(id: Int): Array<CharSequence?> {//项目内不要使用arrayreturn resources.getTextArray(id)}override fun getStringArray(id: Int): Array<String?> {//项目内不要使用arrayreturn resources.getStringArray(id)}override fun getQuantityText(id: Int, quantity: Int): CharSequence {return resources.getQuantityText(id, quantity)}override fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any?): String {return resources.getQuantityString(id, quantity, *formatArgs)}override fun getQuantityString(id: Int, quantity: Int): String {return resources.getQuantityString(id, quantity)}override fun getIntArray(id: Int): IntArray {return resources.getIntArray(id)}override fun obtainTypedArray(id: Int): TypedArray {return resources.obtainTypedArray(id)}override fun getDimension(id: Int): Float {return resources.getDimension(id)}override fun getDimensionPixelOffset(id: Int): Int {return resources.getDimensionPixelOffset(id)}override fun getDimensionPixelSize(id: Int): Int {return resources.getDimensionPixelSize(id)}override fun getFraction(id: Int, base: Int, pbase: Int): Float {return resources.getFraction(id, base, pbase)}override fun getDrawable(id: Int): Drawable? {return resources.getDrawable(id)}@RequiresApi(21)override fun getDrawable(id: Int, theme: Theme?): Drawable? {return resources.getDrawable(id, theme)}override fun getDrawableForDensity(id: Int, density: Int): Drawable? {return resources.getDrawableForDensity(id, density)}@RequiresApi(21)override fun getDrawableForDensity(id: Int, density: Int, theme: Theme?): Drawable? {return resources.getDrawableForDensity(id, density, theme)}override fun getMovie(id: Int): Movie? {return resources.getMovie(id)}override fun getColor(id: Int): Int {return resources.getColor(id)}override fun getColorStateList(id: Int): ColorStateList {return resources.getColorStateList(id)}override fun getBoolean(id: Int): Boolean {return resources.getBoolean(id)}override fun getInteger(id: Int): Int {return resources.getInteger(id)}override fun getLayout(id: Int): XmlResourceParser {return resources.getLayout(id)}override fun getAnimation(id: Int): XmlResourceParser {return resources.getAnimation(id)}override fun getXml(id: Int): XmlResourceParser {return resources.getXml(id)}override fun openRawResource(id: Int): InputStream {return resources.openRawResource(id)}override fun openRawResource(id: Int, value: TypedValue?): InputStream {return resources.openRawResource(id, value)}override fun openRawResourceFd(id: Int): AssetFileDescriptor? {return resources.openRawResourceFd(id)}override fun getValue(id: Int, outValue: TypedValue?, resolveRefs: Boolean) {resources.getValue(id, outValue, resolveRefs)}override fun getValueForDensity(id: Int, density: Int, outValue: TypedValue?, resolveRefs: Boolean) {resources.getValueForDensity(id, density, outValue, resolveRefs)}override fun getValue(name: String?, outValue: TypedValue?, resolveRefs: Boolean) {resources.getValue(name, outValue, resolveRefs)}override fun obtainAttributes(set: AttributeSet?, attrs: IntArray?): TypedArray? {return resources.obtainAttributes(set, attrs)}override fun updateConfiguration(config: Configuration?, metrics: DisplayMetrics?) {if (resources == null)super.updateConfiguration(config, metrics)elseresources.updateConfiguration(config, metrics)}override fun getDisplayMetrics(): DisplayMetrics? {return resources.displayMetrics}override fun getConfiguration(): Configuration? {return resources.configuration}override fun getIdentifier(name: String?, defType: String?, defPackage: String?): Int {return resources.getIdentifier(name, defType, defPackage)}override fun getResourceName(resid: Int): String? {return resources.getResourceName(resid)}override fun getResourcePackageName(resid: Int): String? {return resources.getResourcePackageName(resid)}override fun getResourceTypeName(resid: Int): String? {return resources.getResourceTypeName(resid)}override fun getResourceEntryName(resid: Int): String? {return resources.getResourceEntryName(resid)}override fun parseBundleExtras(parser: XmlResourceParser?, outBundle: Bundle?) {resources.parseBundleExtras(parser, outBundle)}override fun parseBundleExtra(tagName: String?, attrs: AttributeSet?, outBundle: Bundle?) {resources.parseBundleExtra(tagName, attrs, outBundle)}@RequiresApi(Build.VERSION_CODES.M)override fun getColor(id: Int, theme: Theme?): Int {return resources.getColor(id, theme)}@RequiresApi(Build.VERSION_CODES.M)override fun getColorStateList(id: Int, theme: Theme?): ColorStateList {return resources.getColorStateList(id, theme)}@RequiresApi(Build.VERSION_CODES.O)override fun getFont(id: Int): Typeface {return resources.getFont(id)}@RequiresApi(Build.VERSION_CODES.Q)override fun getFloat(id: Int): Float {return resources.getFloat(id)}}

简单解释一下:resources.getResourceEntryName()是通过id获取对应的资源名,thisLanguageKVMap是从服务端获取的多语言键值对HashMap(因为查找快),而我没有拦截getStringArray()和getTextArray()是因为项目内只有一个地方用了,且多语言不好搞,且实现方式不同,我就给改成getString()的形式懒得去研究它了

ps:不能混淆资源名(腾讯的一个混淆资源框架),否则该方案无效

pps:顺便吐槽一下,为啥kotlin的类委托只支持接口,不支持类..好坑,多写了一堆模板代码

第二种:通过拦截xml解析来Hook @string/

最开始我通过参考TextView的text获取逻辑,然后找到TypedArray#getText(),然后发现传进去的是个index,然后各种乱七八糟不熟悉的东西快给我绕晕了,果断使用其他方案

然后我想起来前两年有个很火的快捷开发的方案,通过拦截View生成来在xml中写shape,当时我也模仿写了个ShapeAndSelectUtil,用到的原理也是拦截LayoutInflater.Factory2,正好能用这个来拦截xml解析

原理:

通过拦截View的创建过程,拿到会用到@string/的View的对象(比如TextView和EditText或自定义View),获取对应的id并重新从key value中获取和设置

代码如下:

//在BaseActivity的onCreate()中的super.onCreate()之前调用
layoutInflater.factory2 = LanguageLayoutInflaterFactory(this)/*** creator: lt  lt.dygzs@qq.com*/
class LanguageLayoutInflaterFactory(val activity: Activity) : LayoutInflater.Factory2 {private val APP_KEY = "http://schemas.android.com/apk/res-auto"private val ANDROID_KEY = "http://schemas.android.com/apk/res/android"//sdk的activity使用的布局生成器private val delegate: AppCompatDelegate by lazy(LazyThreadSafetyMode.NONE) { AppCompatDelegate.create(activity, null) }//获取默认的createView方法,可以在这里判断并适配换肤框架等fun checkAndCreateView(parent: View?, name: String?, context: Context, attrs: AttributeSet): View? {return when (activity) {is AppCompatActivity -> activity.delegate.createView(parent, name, context, attrs)else -> delegate.createView(parent, name, context, attrs)}}override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {return checkAndReturnView(name, context, attrs, checkAndCreateView(parent, name, context, attrs))}override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {return null}fun checkAndReturnView(name: String, context: Context, attrs: AttributeSet, view: View?): View? {val view = view ?: try {createViewGroup(name, context, attrs)} catch (e: Exception) {e.toString().e()null} ?: return nullhandlerXmlText(view, attrs)return view}private fun handlerXmlText(view: View, attrs: AttributeSet) {if (view is TextView) {val value = getStringValue(attrs, true, "text")if (value != null)view.text = valueval value = getStringValue(attrs, true, "hint")if (value != null)view.hint = value}//下面两个是自定义View使用了@string/if (view is ItemView) {val value = getStringValue(attrs, false, "left_text")if (value != null)view.setLeftText(value)}if (view is SelectView) {val value = getStringValue(attrs, false, "middle_text")if (value != null)view.setMiddleText(value)}//或者下面这种写法/*if (view is TextView) {getStringValue(attrs, true, "text")?.let(view::setText)getStringValue(attrs, true, "hint")?.let(view::setHint)}if (view is ItemView)getStringValue(attrs, false, "left_text")?.let(view::setLeftText)if (view is SelectView)getStringValue(attrs, false, "middle_text")?.let(view::setMiddleText)*/}private fun Int.toText() = activity.getString(this)private fun getStringValue(attrs: AttributeSet, isAndroidSystem: Boolean, valueName: String): String? {val value = attrs.getAttributeValue(if (isAndroidSystem) ANDROID_KEY else APP_KEY, valueName)if (value?.startsWith("@") == true)try {return value.substring(1).toIntOrNull()?.toText()} catch (e: Exception) {e.upload()}return null}private fun createViewGroup(name: String, context: Context, attrs: AttributeSet): View? {return when (name) {//下面两个是自定义View使用了@string///且下面两个是ViewGroup,如果是View则会在delegate中就创建完成//可能还需要加AppCompatTextView"com.xxx.ItemView" -> ItemView(context, attrs)"com.xxx.SelectView" -> SelectView(context, attrs)else -> null}//想省事的话直接全部生成://Class.forName(if (name.contains('.')) name else "android.widget.$name")//                .getConstructor(Context::class.java, AttributeSet::class.java)//                .newInstance(context, attrs) as View}
}

最后把两种方式一块使用,然后就ok了(应该不会有人只用xml或只用getString()吧)

扩展

多语言使用过程中可能会遇见类似阿拉伯语的从右往左的习惯,其文字也是从右往左看的,这时需要将类似paddingLeft和paddingRight改成paddingStart和paddingEnd,更多的多语言适配可以参考安卓官网:https://developer.android.com/training/basics/supporting-devices/languages?hl=zh-cn#kotlin

然后获取是否是从左到右的api:

    fun isLTR(context: Context): Boolean {return context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR}

end

Hook安卓项目内的字符串获取,用服务器的key value优先代替本地的key value相关推荐

  1. 编写两个jsp页面inputString.jsp 和computer.jsp,用内置对象获取inputString.jsp页面提交的字符串。

    编写两个jsp页面inputString.jsp 和computer.jsp,用户可以并使用inputString.jsp提供的表单输入一个字符串,并提交给computer.jsp页面,该页面通过内置 ...

  2. PC微信hook学习笔记(一)—— 获取个人信息

    PC微信hook学习笔记(一)-- 获取微信个人信息 1 起步 2. 获取基址 2.1 用CE查看个人信息 2.1.1 获取昵称基址 2.1.2 dll模块基址 2.2 用OD查看个人信息 2.3 内 ...

  3. 安卓手机内置NFC模块的使用和开发

    NfcAssistant 是一个采用 kotlin 语言原生开发的 Android 项目,可用于管理基于nfc芯片的 ID/IC/M1 等会员卡,商家可在手机上安装该app即可搭建一个简易的会员卡管理 ...

  4. 安卓项目各文件夹的含义和用处

    生成一个安卓项目后,主要有以下文件夹:src.gen.assets.bin.res. src:存放项目的源代码. gen:该文件是创建项目时候自动生成的,里面包了一个R.java的静态类,它里面包括很 ...

  5. 献给初学iOS的小盆友们——微博app项目开发之七第一次获取微博数据

    上节课我们已经用request token 换取到了access token,但是经过验证我们发现,每一次输入一样的账号和密码后,获取的access token 都是一样的,也就是我们不是每次都需要获 ...

  6. 第一次做安卓项目使用的开源框架列表

    由于以前没有安卓开发经验,虽然Java挺熟悉的,不过到了安卓还是有些不适应,毕竟是另外一套了,想着安卓应该也有许多框架可以帮助我做很多事情了,所以前前后后试了很几个,最终都不怎么理想.比如最一开始用的 ...

  7. JS点击获取验证码后60秒内禁止重新获取(防刷新)

    JS点击获取验证码后60秒内禁止重新获取(防刷新) 参考 · 阅读文章: JS实现发送短信验证后按钮倒计时功能(防止刷新倒计时失效) 注意: 场景:在登录页点击发送啊验证码按钮,开始进入倒计时,在第3 ...

  8. 【安卓R 源码】获取音频焦点和释放音频焦点

    一. 获取焦点流程 1. 电话焦点只有系统可以申请,如果是电话焦点,系统会把所有多媒体和游戏的音频流实例全部mute.同理电话焦点释放会解除mute操作 2. 系统管理的焦点栈有大小限制限制为100. ...

  9. android存储文件数据恢复,安卓手机内置储存中的照片误删怎么恢复

    科技迅速发展随之而来的是人们生活习惯的改变,手机从一个简单的交流工具,变成生活中必备品,手机购物.手机支付.手机传送文件浏览网页.手机分享视频.手机拍照.手机里面满满的簇拥了太多的东西,定期清理的必然 ...

最新文章

  1. selenium工具的安装
  2. [administrative] windows 下制作USB启动盘的工具
  3. 无责任畅想:云原生中间件的下一站
  4. 一個全世界最珍貴的故事(轉載)
  5. 清华体质优良可降5分录取;窃取密钥者奖百万;阿里投入1亿保护方言;腾讯不正当竞争被罚;这就是今天的大新闻...
  6. net core 小坑杂记之配置文件读取(不定期更新)
  7. UNIX网络编程卷1 时间获取程序server UDP 协议无关
  8. 微软TTS语音引擎实现文本朗读
  9. 2020年书法落款_书法落款时间查询表整理,太赞了
  10. MOSES的高级特征和功能
  11. 与大佬沟通,聊到四层代理和七层代理分别指的是什么这个问题时?会擦出什么火花呢
  12. View与ViewGroup
  13. CCS 修改字体大小
  14. python-docx 设置Table 边框样式、单元格边框样式
  15. coreldraw 长方体_用coreldraw 11制作铅笔_coreldraw教程
  16. 数据中心基础设施运维——设备维护
  17. 阅读软件怎么添加书源_书迷小说|手机阅读软件 千个书源 搜索换源
  18. MySQL索引重点问题总结(需要完整脑图的联系我)
  19. php中下列哪些说法是正确的,关于PHP函数,下列定义方式正确的是
  20. Android Studio中layout_gravity与gravity

热门文章

  1. tableau系列之如何将甘特图做成瀑布图
  2. 多层神经网络(BP算法)介绍
  3. 远程计算机未能及时反应,Win10无法打开软件提示“服务器没有及时响应或控制请求”怎么办...
  4. Matplotlib实例教程(二)饼状图
  5. AI算法又整新活,去海边跳一支舞!
  6. selenium教程
  7. #论文 《Deep Residual Learning for Image Recognition》
  8. 【机器学习PAI实践四】如何实现金融风控
  9. 体系化认识RPC--转
  10. Java内存模型深度解析:重排序 --转