之前整理过一篇全局字体设置 || 老年模式的文章,提到过4种方法,各有利弊。
最后推荐了方法4,自定义binding属性来实现。
这里扩展一篇。
自定义binding实现的确不错,最大的优点就是可以实时改变全局字体。
但是也有它的缺点,那就是麻烦,不但要在本地处理一些数据,还要在每个xml中加入binding属性,每个控件都绑定了一个监听,虽然通过livedata进行了优化,但是不可否认的是,它会使内存增大。
app运行内存过大,进入后台就很容易被系统回收,体验非常不好。
所以如果从这个角度去考虑的话,系统自带的方法无疑是最合适的选择。
前面提到过的四种方法:
1.通过AppTheme主题设置
通过配置不同的字体主题,然后设置更换主题来改变全局的字体样式,主题中可配置自定义字体大小等;xml布局中也需要添加style主题,设置主题后需要recreate ui,体验不好。
2.修改系统fontScale,缩放字体大小
百度字体设置等,很多方案都是这种,主要重写Application的onConfigurationChanged监听系统字体大小变化,然后重启app或者activity才会刷新同步,而且对数值不可控,还会影响一些默认配置以及存在适配问题,不作深入研究,直接抛弃。
3.自定义view
每个view中会有一个监听,修改字体后触发监听更改字体;每次创建读取本地缓存,设置字体;需要在显示的地方更换为自定义的view。
看着略显麻烦,但是不得不说,很灵活,并且方便扩展需求,但是麻烦,除了自定义view还得处理监听。
4.自定义binding属性
类似于方案3,在binding方法中去初始化跟设置字体,扩展性好,并且不需要替换textview,流程就跟方案1类似,需要在xml中配置类型属性,然后binding会自动根据配置的属性去设置字体。
所以,如果产品能接受的前提下(recreate ui,重新加载布局),选择方案1跟方案2无疑是最好的。
方案1的优点,显然就是可以自定义字体大小,固定数值,不过也需要xml中配置style,但是对比自定义binding不会产生大量监听。
方案2的优点,一个字:快,缺点也很明显,无法具体自定义字体大小,因为是按缩放处理的。
居然自定义字体大小已经实现过一套方案了,这里就扩展方案2。//(你以为是因为偷懒吗?确实是。)
下面是预览图,太大了加载比较慢,压缩了一下,有点糊。
男人就是要快,居然要快,那就很简单了,直接在BaseActivity中重写getResources以及attachBaseContext方法。
@Override public Resources getResources() { Resources resources = super.getResources(); if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { if (resources != null && resources.getConfiguration().fontScale != FontUtils.getAppFontScale()) { Configuration configuration = resources.getConfiguration(); configuration.fontScale = FontUtils.getAppFontScale(); resources.updateConfiguration(configuration, resources.getDisplayMetrics()); } } return resources; } @Override protected void attachBaseContext(Context newBase) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N){ Resources resources = newBase.getResources(); Configuration configuration = resources.getConfiguration(); configuration.fontScale = FontUtils.getAppFontScale(); super.attachBaseContext(newBase.createConfigurationContext(configuration)); }else { super.attachBaseContext(newBase); } }
高版本系统执行attachBaseContext方法,低版本执行getResources方法。
最后结果都是把我们本地保存的缩放值getAppFontScale设置到系统属性中。
recreate才会生效,所以会存在页面重新加载的情况。
FontUtils类非常简单,单纯的记录缩放值,缓存在本地。
public class FontUtils { private static final String TAG = "FontUtils"; private static final String KEY_APP_FONT_STYLE = "key_app_font_style"; public static final float NORMAL_FONT_SCALE = 1.0f; public static final float BIG_FONT_SCALE = 1.2f; public static final float LARGE_FONT_SCALE = 1.4f; public static float getAppFontScale() { return SPUtils.getInstance().getFloat(KEY_APP_FONT_STYLE, NORMAL_FONT_SCALE); } public static void saveAppFontScale(float appFontScale) { SPUtils.getInstance().put(KEY_APP_FONT_STYLE, appFontScale); } }
不过这里也有一个需要注意的地方,就是我们xml中的textSize属性值单位必须为sp,有的可能会设置成dp,虽然显示没影响,但是缩放值fontScale只针对sp才会生效。
/** * 用户 - 设置字体 */ public class SettingFontActivity extends BaseActivity { public static void startSettingFontActivity() { XActivityUtils.startActivity(SettingFontActivity.class); } private float pointScale = 0; @Override public int getLayoutId() { return R.layout.activity_setting_font; } @Override public void initView(Bundle savedInstanceState) { mBinding.setClick(new ClickProxy()); mBinding.includeStatusBar.tvTitle.setText("字体设置"); pointScale = FontUtils.getAppFontScale();//记录初始值 loadFontSize(pointScale); } @Override public void onBackPressed() { //更改过设置 if (pointScale != FontUtils.getAppFontScale()) { LiveEventBusUtils.postLiveEventBus(LiveEventBusStrKey.FONT_STYLE, new LiveEventBusData.Builder().build()); } super.onBackPressed(); } private void loadFontSize(float scale){ //单位为dp,默认是sp,会触发缩放效果 mBinding.tvFontTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP,18 * scale); mBinding.tvFontContent.setTextSize(TypedValue.COMPLEX_UNIT_DIP,16 * scale); } /** * 配置点击事件 */ public class ClickProxy { public ObservableBoolean isNormalField = new ObservableBoolean(FontUtils.getAppFontScale() == FontUtils.NORMAL_FONT_SCALE); public int normalResId = R.mipmap.icon_setting_font_normal_check; public int unNormalResId = R.mipmap.icon_setting_font_normal_un; public ObservableBoolean isBigField = new ObservableBoolean(FontUtils.getAppFontScale() == FontUtils.BIG_FONT_SCALE); public int bigResId = R.mipmap.icon_setting_font_big_check; public int unBigResId = R.mipmap.icon_setting_font_big_un; public ObservableBoolean isLargeField = new ObservableBoolean(FontUtils.getAppFontScale() == FontUtils.LARGE_FONT_SCALE); public int largeResId = R.mipmap.icon_setting_font_large_check; public int unLargeResId = R.mipmap.icon_setting_font_large_un; //setting public void setting(int type){ switch (type){ case 1: isNormalField.set(true); isBigField.set(false); isLargeField.set(false); FontUtils.saveAppFontScale(FontUtils.NORMAL_FONT_SCALE); break; case 2: isBigField.set(true); isNormalField.set(false); isLargeField.set(false); FontUtils.saveAppFontScale(FontUtils.BIG_FONT_SCALE); break; case 3: isLargeField.set(true); isBigField.set(false); isNormalField.set(false); FontUtils.saveAppFontScale(FontUtils.LARGE_FONT_SCALE); break; } loadFontSize(FontUtils.getAppFontScale()); } } }
fontScale属性是需要recreate才能生效,如果每次选择都要recreate一下页面,体验太不好了。
这里就需要我们手动去设置textSize,居然是手动设置,就不能用sp为单位了,否则在次回到页面你会发现本来就大号字体又缩放了,变成了超巨大号字体,预览错乱。
代码中可以看到,这边做了一个行为记录,只有在用户改变过字体的时候才会去触发监听,否则每次都更新一遍,recreate一下,显然不是想要的结果。
监听在MainActivity中,这里又有一个注意的点,launchMode设置为singleTop,因为中间存在存活的Activity,所以需要Main来让他们退出栈区,当然,也可以自定义控制,看个人需求。
<activity android:name=".ui.main.MainActivity" android:configChanges="screenLayout|keyboardHidden|orientation|screenSize|smallestScreenSize" android:label="主页" android:launchMode="singleTop" android:screenOrientation="portrait" />
LiveEventBusUtils.registerLiveEventBus(LiveEventBusStrKey.FONT_STYLE, thisActivity, liveEventBusData -> { MainActivity.startMainActivity();//recreate ui overridePendingTransition(0, 0); });
已知有兼容性问题,说是在app刚好运行时设置系统字体,会影响我们的保存设置,方案是在Application中重写onConfigurationChanged监听,设置了字体会触发监听,重新进入一次。
@Override public void onConfigurationChanged(@NonNull Configuration newConfig) { //防止app在运行的时候系统设置里更改了字体大小 if (newConfig.fontScale > 1){ MainActivity.startMainActivity(); } super.onConfigurationChanged(newConfig); }
试过,没复现,但是需要注意。
小扩展。
假设有的SplashActivity是在MainActivity中启动的,需要规避一下,因为会导致重新加载SplashActivity,所以在startMainActivity时可以传入一个参数。
不过启动页用fragment来控制会比较好一些,官方也推荐过这种做法。
因为这样就能在加载启动页时开始初始化一些数据,并且不用跳转页面,大大降低了启动时间。