评论

收藏

[Android] 全局字体设置 || 老年模式

移动开发 移动开发 发布于:2021-08-27 11:46 | 阅读数:289 | 评论:0

通过设置字号,同步改变全局的字体。
长文干货,建议点赞收藏。
实现方式有多种:
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会自动根据配置的属性去设置字体。
全局字体设置之自定义binding
先看效果图,在设置页面,通过设置字体的类型,然后保存下来,同时刷新binding监听,更改所有显示的textview字体大小。
并且可以方便扩展,比如tablayout中选中放大等效果。
DSC0000.gif

首先新建字体设置工具类。
用于存储字体类型,获取字体样式。
DSC0001.gif DSC0002.gif
class FontUtils {
  companion object {
    private const val TAG = "FontUtils"
    private const val KEY_APP_FONT = "key_app_font"
    //标准字体
    const val NORMAL_FONT_STYLE = "normal_text_size"
    //大号字体
    const val BIG_FONT_STYLE = "big_text_size"
    //特大字体
    const val LARGE_FONT_STYLE = "large_text_size"
    private val instance by lazy { FontUtils() }
    fun getFontUtils(): FontUtils = instance
  }
  /** 字体样式类型  */
  fun getAppFontStyle(): String {
    return SPUtils.getInstance().getString(KEY_APP_FONT, NORMAL_FONT_STYLE)
  }
  fun saveAppFontStyle(appFontStyle: String?) {
    SPUtils.getInstance().put(KEY_APP_FONT, appFontStyle)
  }
  /** 获取模型  */
  fun getFontVo(fontType: String?): FontBean? {
    val fontStyle: String = getAppFontStyle()
    val fontVoList: List<FontBean>? = getRawFileList(fontStyle)
    return if (CollectionUtils.isNotEmpty(fontVoList)) {
      // LogUtils.i(TAG, "getFontVo-- fontVo:" + GsonUtils.toJson(fontVo));
      getFontByType(fontVoList, fontType)
    } else null
  }
  /**
   * 解析模型
   * @param fontType 具体字号类型
   * */
  private fun getFontByType(fontVoList: List<FontBean>?, fontType: String?): FontBean? {
    return CollectionUtils.find(
      fontVoList
    ) {
      it != null && StringUtils.equals(it.fontType, fontType)
    }
  }
  /** 字体模型  */
  private fun getRawFileList(fontStyle: String?): List<FontBean>? {
    return when {
      StringUtils.equals(NORMAL_FONT_STYLE, fontStyle) -> {
        getFontListByRaw(R.raw.font_normal)
      }
      StringUtils.equals(BIG_FONT_STYLE, fontStyle) -> {
        getFontListByRaw(R.raw.font_big)
      }
      StringUtils.equals(LARGE_FONT_STYLE, fontStyle) -> {
        getFontListByRaw(R.raw.font_large)
      }
      else -> getFontListByRaw(R.raw.font_normal)
    }
  }
  /** 读取本地模型 路径 /res/raw/resId */
  private fun getFontListByRaw(@RawRes resId: Int): List<FontBean>? {
    return GsonUtils.fromJson<List<FontBean>>(
      ResourceUtils.readRaw2String(resId),
      object : TypeToken<List<FontBean>>() {}.type
    )
  }
}
View Code下面是字体模型截图,类似方案1中的字体主题,分别对应设置页面的标准字体,大号字体,特大字体,可随意扩展。
xml中设置的字体类型就来自模型中读取的数值
DSC0003.png

接下来就是自定义binding属性,具体设置方法。

@BindingAdapter(value = ["bindFontType"], requireAll = false)
fun TextView.setBindingFontStyle(fontType: String?) {
  if (TextUtils.isEmpty(fontType)) {
    LogUtils.i("setBindingFontStyle", "IS NULL")
    return
  }
  //去除字体内边距
  includeFontPadding = false
  val fontVo: FontBean? = FontUtils.getFontUtils().getFontVo(fontType)
  if (fontVo != null) {
    textSize = fontVo.fontSize
  }
  val activity = ActivityUtils.getActivityByContext(context) as AppCompatActivity?
  if (activity != null) {
    LiveEventBus
      .get(LiveEventBusKey.FONT_STYLE, Int::class.java)
      .observe(activity, {
        val fontBean: FontBean? = FontUtils.getFontUtils().getFontVo(fontType)
        if (fontBean != null) {
          textSize = fontBean.fontSize
        }
      })
  }
}
View Code自定义binding方法中通过livedata注册了一个监听,所以跟方案3类似,实则是每一个textview都存在一个监听,而livedata可以绑定生命周期,自动创建跟销毁监听,避免内存泄漏。
在xml中绑定设置的方法。
DSC0004.png

当布局创建时,会自动执行binding方法。
binding方法中会根据xml里的字体大小类型执行工具类中的getFontVo方法。
getFontVo方法回去读取缓存在本地的字体类型,等于主题类型,从而读取到具体的模型数据,拿到数据设置更新。
而binding方法中的监听,绑定了当前的生命周期,所以当页面销毁或回收时会自动解除监听。
只要xml中设置了自定义的binding属性,就能同步修改更新,不影响原本的设置,如丝滑般柔顺。
当然,因为是基于binding,所以项目得基于databinding才行。
因为我后面接触过的项目都是databinding,并且也是主流。
如果不是就推荐方案3了,通过自定义view实现,大致流程也差不多。
码字不易,喜欢就赏个赞吧。
自定义扩展
因为是binding,所以有时候在无法满足需求的情况下可以额外扩展方法。
比如tablayout,实现一个选中字体放大的效果。

@BindingAdapter(value = ["bindFontType", "isCheck", "checkBuffSize"], requireAll = false)
fun TextView.setBindingFontStyle(fontType: String?, isCheck: Boolean, checkBuffSize: Int) {
  if (TextUtils.isEmpty(fontType)) {
    LogUtils.i("setBindingFontStyle", "IS NULL")
    return
  }
  //去除字体内边距
  includeFontPadding = false
  //选中字体
  val fontVo: FontBean? = FontUtils.getFontUtils().getFontVo(fontType)
  if (fontVo != null) {
    textSize = if (isCheck) {
      fontVo.fontSize + checkBuffSize
    } else {
      fontVo.fontSize
    }
  }
  val activity = ActivityUtils.getActivityByContext(context) as AppCompatActivity?
  if (activity != null) {
    LiveEventBus
      .get(LiveEventBusKey.FONT_STYLE, Int::class.java)
      .observe(activity, {
        val fontBean: FontBean? = FontUtils.getFontUtils().getFontVo(fontType)
        if (fontBean != null) {
          textSize = fontBean.fontSize
        }
      })
  }
}
View Code很简单,扩展了两个属性,一个是否选中,一个是增量。
只需要在xml中动态配置一下,然后通过逻辑控制就能同步设置。
DSC0005.png

这里有时候会碰到一些问题,比如tablayout例子。
如果是自定义tablayout,customview并没有参与绑定,无法实现监听的情况。
这里可以手动绑定一下解决,参数可以统一设置在binding方法中,也可以额外设置,不过最好统一管理,便于扩展跟维护。

/**
   * 配置选中/未选中状态
   */
  private fun setTabLayoutSelected(tab: TabLayout.Tab, isCheck: Boolean) {
    val topicBinding: TabviewTopicBinding? = DataBindingUtil.getBinding(tab.customView!!)
    if (topicBinding != null) {
      topicBinding.setIsCheck(isCheck)//通过binding统一控制
      if (isCheck) {//也可以随意
        topicBinding.tvTitle.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD))
      } else {
        topicBinding.tvTitle.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL))
      }
    }
  }
View Code只要把工具类封装好了,后续只需要设置binding属性就行。
在需求跟扩展以及刷新效果来说,这个方案是很不错的。