# 需求
# 最近做完了新功能,突发奇想,想着如何处理 app 的动态切换字体
# 方案
# 方案 1: 通过反射机制,修改 Typeface 类的字体库引用
object FontUtils { | |
fun setDefaultFont(context: Context,staticTypefaceFieldName:String,fontAssetName:String){ | |
val font = Typeface.createFromAsset(context.assets, fontAssetName) | |
replaceFont(staticTypefaceFieldName,font) | |
} | |
fun replaceFont(staticTypefaceFieldName: String, newTypeface: Typeface) { | |
try { | |
val staticField = Typeface::class.java.getDeclaredField(staticTypefaceFieldName) | |
staticField.isAccessible = true | |
staticField.set(null, newTypeface) | |
} catch (e: NoSuchFieldException) { | |
e.printStackTrace(); | |
} catch (e: IllegalAccessException) { | |
e.printStackTrace(); | |
} | |
} | |
} |
不过现有项目都是 kotlin 写的,不太想用反射
# 方案 2: 通过修改特定的 View 或者自定义 TextView
工作量太大,而且效果不好.
# 方案 3: 通过第三方库
Calligraphy
这个库维护到现在 5 年多了,而且每年都有更新,使用也比较简单。当然作者现在已经不维护了,但是建议我们使用另一个大佬维护的 3.0 版本 Calligraphy3
# 实现
# 接入
# 2.0 版本
api 'uk.co.chrisjenx:calligraphy:2.3.0' |
# 3.0 版本
api 'io.github.inflationx:calligraphy3:3.0.0' | |
api 'io.github.inflationx:viewpump:1.0.0' |
因为我是在 library 模块引入的,所以使用的 api 方式
# 初始化
我们可以选择在自定义的 Application 中初始化,也可以在 LauncherActivity 中,看个人喜好.
# 2.0 版本
private fun initTypeFace() { | |
val path = PreferenceUtil.getPreference("fonts")!!.getString("fontPath", "fonts/FounderBlack.ttf") | |
CalligraphyConfig.initDefault(CalligraphyConfig.Builder() | |
.setDefaultFontPath(path) | |
.setFontAttrId(R.attr.fontPath) | |
.build() | |
) | |
} |
# 3.0 版本
ViewPump.init(ViewPump.builder() | |
.addInterceptor(CalligraphyInterceptor( | |
CalligraphyConfig.Builder() | |
.setDefaultFontPath(path) | |
.setFontAttrId(R.attr.fontPath) | |
.build())) | |
.build()) |
在 onCreate() 方法中调用 initTypeFace() 即可
这段代码是初始化我们的字体,其实在这里我们就可以选择自己喜欢的字体了,不过选完以后字体就固定了,这与我们的需求不符.
# 我们先去自己的 BaseActivity 中处理一下
# 2.0 版本
override fun attachBaseContext(newBase: Context?) { | |
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)) | |
} |
# 3.0 版本
override fun attachBaseContext(newBase: Context?) { | |
super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase?:return)) | |
} |
# 因为要让字体全局生效,必然要在每个 Activity 的 attachBaseContext() 方法中绑定我们的框架,所以 BaseActivity 是最好的选择了.
其实到这里,我们的三方字体就能正常显示了,当然,如果你想针对单个控件设置特别的字体,你可以这样做
<RadioButton | |
android:id="@+id/semibold" | |
android:text="Bold" | |
fontPath="fonts/mySemibold.ttf" | |
android:padding="10dp" | |
android:gravity="center" | |
android:onClick="@{onclick}" | |
android:textSize="14sp" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
/> |
# 只需要给文本控件设置 fontPath 就好了
# 高级操作
起初,我们的字体是这样的
# 我们的目的是要让用户自己选择显示什么样的字体,比如这样:

因此我们还需要其他操作,新建一个 FontActivity, 在其伴生类中初始化所有字体
companion object { | |
private val fonts = arrayListOf( | |
FontStyle("杨任东竹石粗体", "YRDBold", "fonts/YRDBold.ttf"), | |
FontStyle("杨任东竹石细体", "TRDExtralight", "fonts/YRDExtralight.ttf"), | |
FontStyle("站酷快乐体2016", "ZhankuHappy2016", "fonts/zhankuHappy2016.ttf"), | |
FontStyle("站酷高端黑", "ZhankKuAdvancedBlack", "fonts/zhankuAdvancedBlack.ttf"), | |
FontStyle("王汉宗魏碑体", "WHZ_WB", "fonts/WHZ_WB.ttf"), | |
FontStyle("Oradano-Mincho名朝", "OradanoMincho", "fonts/Oradano-Mincho.ttf"), | |
FontStyle("阿里汉仪智能黑体", "AliSmartBlack", "fonts/AliSmartBlack.ttf"), | |
FontStyle("庞门正道标题体2.0增强版", "PMZD_Title", "fonts/PMZD-Title.ttf"), | |
FontStyle("思源柔黑体", "GenJyuuGothic", "fonts/GenJyuuGothic-Medium.ttf"), | |
FontStyle("方正黑体", "FounderBlack", "fonts/FounderBlack.ttf"), | |
FontStyle("TanukiMagic麦克笔手绘", "TanukiMagic", "fonts/TanukiMagic.ttf") | |
) | |
} |
FontStyle 是一个 data class
data class FontStyle(val name:String,val tag :String,val path:String,var inUse:Boolean =false) |
用于记录字体信息,包括字体名称,tag 标记,以及字体路径,字体的选取与否保存在 SharedPreference 当中
val string = PreferenceUtil.readString("fonts", "fontPath", "fonts/FounderBlack.ttf") | |
for (font in fonts) { | |
val radioButton = RadioButton(this@FontsActivity) | |
val layoutParams = LinearLayout.LayoutParams( | |
LinearLayout.LayoutParams.MATCH_PARENT, | |
LinearLayout.LayoutParams.WRAP_CONTENT) | |
layoutParams.gravity = Gravity.CENTER | |
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT | |
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT | |
layoutParams.bottomMargin = 30 | |
radioButton.setPadding(10, 10, 10, 10) | |
radioButton.layoutParams = layoutParams | |
radioButton.tag = font.tag | |
if (font.path == string) { | |
font.inUse = true | |
radioButton.isChecked = true | |
mCheckRadioButton=radioButton | |
} | |
radioButton.text = font.name | |
radioButton.maxLines = 1 | |
radioButton.ellipsize = TextUtils.TruncateAt.END | |
radioButton.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics) | |
CalligraphyUtils.applyFontToTextView(radioButton, Typeface.createFromAsset(assets, font.path)) | |
binding.rg.addView(radioButton) | |
radioButton.setOnClickListener { | |
if (radioButton==mCheckRadioButton) { | |
return@setOnClickListener | |
}else{ | |
mCheckRadioButton?.isChecked=false | |
radioButton.isChecked=true | |
mCheckRadioButton=radioButton | |
} | |
} | |
} |
遍历字体集合动态生成 RadioButton, 加字体 tag 设置给 radioButton, 并将已选中的字体选中
# 通过
CalligraphyUtils.applyFontToTextView(radioButton, Typeface.createFromAsset(assets, font.path)) |
# 方法动态设置字体,然后我们就得到了上面的显示效果了
选中后点击确定
val tag = findViewById<RadioButton>(binding.rg.checkedRadioButtonId)?.tag | |
for (font in fonts) { | |
if (font.tag == tag) { | |
if (font.inUse) { | |
toastShort("字体正在使用当中...") | |
} else { | |
PreferenceUtil.writeString("fonts", "fontPath", font.path) | |
PreferenceUtil.writeString("fonts", "fontName", font.name) | |
PreferenceUtil.writeString("fonts", "fontTag", font.tag) | |
BaseApplication.finishAll() | |
startActivity(Intent(this@FontsActivity, LaunchActivity::class.java)) | |
this@FontsActivity.overridePendingTransition(0, 0) | |
} | |
break | |
} | |
} |
通过选中的 radiobutton 的 tag 来判断我们想要换的是什么字体,将它的路径,name tag 都保存起来,然后调用 application 的 finishAll() 方法关闭所有的 Activity, 最后再次启动我们的 LauncherActivity, 我们的 APP 就重新启动并加载了新的字体了
# 效果对比
# 切换前

# 切换后

