emmmm... 最近做了个优化,记录一下,毕竟网上查到的有点问题。
StackOverFlow 上看到有类似的提问:
Font size of TextView in Android application changes on changing font size from native settings
但是回答都是一样的:把单位dp
换成sp
,里面甚至有关于替换后大小不受系统字体变化影响的原理说明:
Android sp vs dp texts - what would adjust the 'scale' and what is the philosophy of support
dp
sp
的替换显然不适用于我现在的情况,尤其是这样的替换,在 webview
中是不适用的。百度能看到的答案也千篇一律,大多是这样的方法 (重写 Application 或 BaseActivity 中的方法):
/**
* 重写 getResource 方法,防止系统字体影响
*/
@Override
public Resources getResources() {
Resources resources = super.getResources();
Configuration configuration = new Configuration();
configuration.setToDefaults();
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
return resources;
}
但是上面的方法存在两个问题:
- 使用的
Configuration
是新建的对象,从代码看应该是没有问题的。但是项目上应用的时候,使用第三方 sdk 出现了空指针。错误出现在Locale.toString()
,是在resources.updateConfiguration(configuration, resources.getDisplayMetrics())
的调用过程中出现的问题。这里的Locale
应该是Configuration
中属性 locale 。应该是系统中原本自带的属性遗失了。在大多数的情况下,我们不会察觉到这样的差异,因为在Context#updateConfiguration
中对于没有的值都赋予了默认值。
public void updateConfiguration(Configuration config,
DisplayMetrics metrics, CompatibilityInfo compat) {
synchronized (mAccessLock) {
//其他代码
int configChanges = 0xfffffff;
if (config != null) {
//储存新的 config
mTmpConfig.setTo(config);
//local 默认值赋值
if (mTmpConfig.locale == null) {
mTmpConfig.locale = Locale.getDefault();
mTmpConfig.setLayoutDirection(mTmpConfig.locale);
}
//mConfiguration 赋值
configChanges = mConfiguration.updateFrom(mTmpConfig);
}
//其他代码
}
}
-
configuration.setToDefaults()
会把所有属性都设置成系统默认,对于有特殊需求的 app 来说是不合理的,应该只修改字体大小的设置。
对于第一个问题,我尝试不用新建的 Configuration
而是利用 Resource#getConfiguration
获取的话,就不再报错。对于第二个问题,我们可以只设置 configuration.fontScale
属性。
综上,得到下面的代码:
/**
* 重写 getResource 方法,防止系统字体影响
*/
@Override
public Resources getResources() {
Resources resources = super.getResources();
if (resources != null && resources.getConfiguration().fontScale != 1) {
Configuration configuration = resources.getConfiguration();
configuration.fontScale = 1;
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
}
return resources;
}
2018-4-2 补充
上面的代码在大部分逻辑上都已经走通(除了带界面的第三方 sdk)。最近发现一个新的问题,如果页面上有 TextWatcher
文本监听,字体变化似乎会触发它的监听方法,原本不需要判定是否为空的许多变量,在现在就变得不可控制了。因为当文字字体大小改变时,整个页面会从 onCreate
开始重新加载。为此,我全局检索了所有使用到 TextWatcher
的地方,加上非空判断,同时在所有页面上加上了字体变化的监听,使得在字体变化的时候调用 onConfigurationChange
方法,而不是重建页面,这一点处理和旋转屏幕类似。
//清单文件中添加 android:configChanges 属性
另外,如果通过updateConfiguration
方法进行更新,我们会发现,每一次调用getResource
方法时,Context.getResources.getConfiguration.fontScale
的值并不会变化,依然是我们系统设置中的字体大小,也就是说,新的 Configuration
并没有更新到 Context
中去。这样频繁调用 updateConfiguration
效率太差。
查看 API 可以看到 updateConfiguration 在 API 25 之后,被方法 createConfigurationContext
替代了,那么是不是可以通过 createConfigurationContext
得到更好的解决方案呢?
createConfigurationContext:
Return a new Context object for the current Context but whose resources are adjusted to match the given Configuration. Each call to this method returns a new instance of a Context object; Context objects are not shared, however common state (ClassLoader, other Resources for the same configuration) may be so the Context itself can be fairly lightweight.
重点已经给出,可以看到通过这个方法,我们可以把新的configuration
应用到 Context
中去,这种情况下,我们可以看到 Configuration.fontScale
在大部分时候已经返回 "1" 了。
public Resources getResources() {
Resources resources = super.getResources();
Configuration newConfig = resources.getConfiguration();
if (resources != null && newConfig.fontScale != 1) {
newConfig.fontScale = 1;
if (Build.VERSION.SDK_INT >= 17) {
Context configurationContext = createConfigurationContext(newConfig);
resources = configurationContext.getResources();
} else {
resources.updateConfiguration(newConfig, displayMetrics);
}
}
return resources;
}
上面的代码虽然改变了 Configuration
中的 fontScale
属性 ,但是实际显示的字体大小依旧没有变化,我们需要更新配置。怎么更新可以参考 updateConfiguration
中的代码:
public void updateConfiguration(Configuration config,
DisplayMetrics metrics, CompatibilityInfo compat) {
synchronized (mAccessLock) {
/**
* A scaling factor for fonts displayed on the display. This is the same
* as {@link #density}, except that it may be adjusted in smaller
* increments at runtime based on a user preference for the font size.
*/
mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale;
}
}
}
综上,获得下面的代码:
@override
public Resources getResources() {
Resources resources = super.getResources();
Configuration newConfig = resources.getConfiguration();
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
if (resources != null && newConfig.fontScale != 1) {
newConfig.fontScale = 1;
if (Build.VERSION.SDK_INT >= 17) {
Context configurationContext = createConfigurationContext(newConfig);
resources = configurationContext.getResources();
displayMetrics.scaledDensity = displayMetrics.density * newConfig.fontScale;
} else {
resources.updateConfiguration(newConfig, displayMetrics);
}
}
return resources;
}