安卓字体不随系统字体变化

安卓字体不随系统字体变化_第1张图片
sticker

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;
}
谢谢观赏

你可能感兴趣的:(安卓字体不随系统字体变化)