Android APP字体随系统字体调整造成界面布局混乱问题解决方案
遇到的问题:当用户调整系统字体大小的时候,APP的字体一般也都会跟随改变,进而导致某些界面布局排版混乱。下面先说一下关于sp单位的理解sp单位除了受屏幕密度影响外,还受到用户的字体大小影响,通常情况下,建议使用sp来跟随用户字体大小设置。除非一些特殊的情况,不想跟随系统字体变化的,可以使用dp”。按照这么说,布局宽高固定写死的地方应该统一用dp显示字体,因为一旦用户在设置中调大字体,宽高为固定值的
一、遇到的问题
当用户调整系统字体大小的时候,APP的字体一般也都会跟随改变,进而导致某些界面布局排版混乱。
下面先说一下关于sp单位的理解
sp单位除了受屏幕密度影响外,还受到用户的字体大小影响,通常情况下,建议使用sp来跟随用户字体大小设置。除非一些特殊的情况,不想跟随系统字体变化的,可以使用dp”。按照这么说,布局宽高固定写死的地方应该统一用dp显示字体,因为一旦用户在设置中调大字体,宽高为固定值的布局显示就乱了。
二、 解决方案
1. 全部界面强制使用标准字体,不跟随系统字体大小改变
强制实现所有界面都的字体都不随系统字体大小而改变,在工程的BaseActivity中添加下面的代码。利用Android的Configuration类中的fontScale属性,其默认值为1,会随系统调节字体大小而发生变化,如果我们强制让其等于默认值,就可以实现字体不随调节改变,
@Override
public Resources getResources() {
Resources resources = super.getResources();
if (resources != null) {
Configuration configuration = resources.getConfiguration();
if (configuration != null && configuration.fontScale != 1.0f) {
configuration.fontScale = 1.0f;//这里只设置字体,故不使用下面注释的方法
// configuration.setToDefaults();
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
}
}
return resources;
}
注意: Android 8.0后在Application中复写上述方法是无效的 (原因暂不清楚,有知道的大佬欢迎指出)。此外,在任意一个Activity中如上覆盖了getResources方法后,会让其它Activity的字体也变的独立于系统配置(这里的Activity只针对重新create的,如当前 Activity 的 fragment,因为没有重新onCreate,就不会重绘进而改变字体)。我的理解是,新的 Activity 会载入上面更新后的 Configuration,而现有的 Activity 则不会更新。所以此方式我只建议用在BaseActivity中实现全部界面字体不随系统更改。
如果有朋友担心重写这个方法会对界面渲染造成什么影响,那么推荐使用下面的方式:
//建议放在APP的启动Activity中的onCreate()
public void initFontScale() {
Resources resources = getResources();
Configuration configuration = resources.getConfiguration();
float fontScale = configuration.fontScale;
configuration.fontScale = 1.0f;//0.85:小号 1:标准 1.25:大号 1.4:巨无霸
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
Log.d("DisplayMetrics ", "======" + resources.getDisplayMetrics().toString() + "\t,fontScale:" + configuration.fontScale);
}
2. 在具体的界面把不想要放大的View字体单位设置为dp
3. 部分界面跟随系统字体放大,部分界面使用标准字体,不跟随系统字体方法
同一个APP中仅针对部分界面字体不做调整,其他界面跟随系统字体大小改变,下面详细说明思路
重写BaseActivity中的 getResource() 方法,然后在方法里面,对于需要做字体调整的界面调整字体,不需要调整的使用标准字体,下面放代码:
//下面代码均放在APP的 BaseActivity 中
private boolean fixscaled = false;//是否需要固定字体大小,默认不需要,即跟随系统字体大小调整
//对于需要固定字体大小的界面,重写这个方法,返回true
public boolean enableFixSacle() {
return fixscaled;
}
@Override
public Resources getResources() {
Resources resources = super.getResources();
//拿到启动页Activity中获取并保存的系统字体大小的值
float originScale = SpUtils.getPrefFloat("fontScale", 0);
Configuration configuration = resources.getConfiguration();
if (enableFixSacle()) {//需要固定字体大小界面
if (configuration != null && configuration.fontScale != 1.0f) {//如果不是标准大小字体,就改为标准大小
configuration.fontScale = 1.0f;
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
}
} else if (originScale != 0) {//随系统字体大小变换界面
if (configuration != null && configuration.fontScale != originScale) {//当前字体大小不等于系统字体大小,就改为系统大小
configuration.fontScale = originScale;
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
}
}
//这里可以显示一下是哪些界面
Log.e("fontScle", configuration.fontScale + ",\t"+getClass().getSimpleName());
return resources;
}
此外,还需要在启动界面的Activity中获取到当前系统的字体大小的值,代码如下:
//下面的方法建议放在启动页 Activity
public void getSysytemFontScale() {
float fontScaleSystem = 1.0f;//如果反射取不到系统fontScale,就取标准字体大小
//通過反射framework层的方法,获取当前系统字体大小
try {
@SuppressLint("PrivateApi")
Class<?> activityManagerNative = Class.forName("android.app.ActivityManagerNative");
try {
//调用ActivityManagerNative的getDefault()方法,获取到 IActivityManager 对象
Object defaultMethod = activityManagerNative.getMethod("getDefault").invoke(activityManagerNative);
if (defaultMethod != null) {
//调用IActivityManager中的getConfiguration()方法,获取到系统的 Configuration
Configuration getConfiguration = (Configuration) defaultMethod.getClass().getMethod("getConfiguration").invoke(defaultMethod);
if (getConfiguration != null) {
fontScaleSystem = getConfiguration.fontScale;
}
}
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
SpUtils.setPrefFloat("fontScale", fontScaleSystem);//这里应该存系统的字体大小,而不是App中通过getResources()获取的configuration.fontScale
//下面这部分代码仅做信息参考和对比学习,可以删除
Resources resources = getResources();
Configuration configuration = resources.getConfiguration();
float fontScale = configuration.fontScale;//修改并且更新或Configuration之后,再次修改系统字体大小,APP这里获取的fontScale不会再变,除非清空所有数据
Log.e("fontScle", "Splash===" + configuration.fontScale + "\tfontScaleSystem:" + fontScaleSystem);
}
至此,就可以了,然后哪些界面需要固定字体大小,那么就重新 BaseActivity中的 enableFixScale() 方法,并返回 true即可。对于 下面这部分获取系统字体大小的方法是通过反射来实现的,因为通过测试后发现,如果 SP中保存的是 getResouce.getConfiguration().fontScale获取的字体大小倍数,并通过resources.updateConfiguration(a,b)更新了一次界面字体后,那么当系统字体大小再次发生变化时,APP中再次调用该方法获取fontScale的值,还是之前改过的值,并没有随系统调整后发生改变(至于原因,不知道怎么去深究,推测可能是因为update这个配置后,在APP的内部存储中会保存这个值,当再次获取得到的就是保存的值,但是如果没有update过这个配置,那么这样获取到的fontScale还是会跟随系统字体大小发生改变,有兴趣的伙伴可以深究一下,顺便分享给我),所以如果清空一下APP的所有数据,再进来字体就又会改变一次。综上,我通过反射直接获取到了系统的fontScale并进行保存,用来对比和操作。这块请务必理解。最后,这种方式,当调整系统字体大小后,APP重启后才会生效,因为系统字体大小的值我是在启动页获取并保存的。
三、原理解析
到底为什么设置为sp,会导致字体随系统字体大小而改变?
从文字设定大小的入口看,TextView.setTextSize(float size)方法来看:
/**
* Set the default text size to the given value, interpreted as "scaled
* pixel" units. This size is adjusted based on the current density and
* user font size preference.
*
* <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
*
* @param size The scaled pixel size.
*
* @attr ref android.R.styleable#TextView_textSize
*/
@android.view.RemotableViewMethod
public void setTextSize(float size) {
setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
}
/**
* Set the default text size to a given unit and value. See {@link
* TypedValue} for the possible dimension units.
*
* <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
*
* @param unit The desired dimension unit.
* @param size The desired size in the given units.
*
* @attr ref android.R.styleable#TextView_textSize
*/
public void setTextSize(int unit, float size) {
if (!isAutoSizeEnabled()) {
setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
}
}
private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
Context c = getContext();
Resources r;
if (c == null) {
r = Resources.getSystem();
} else {
r = c.getResources();
}
setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),
shouldRequestLayout);
}
@UnsupportedAppUsage
private void setRawTextSize(float size, boolean shouldRequestLayout) {
if (size != mTextPaint.getTextSize()) {
mTextPaint.setTextSize(size);
if (shouldRequestLayout && mLayout != null) {
// Do not auto-size right after setting the text size.
mNeedsAutoSizeText = false;
nullLayouts();
requestLayout();
invalidate();
}
}
}
可以看到,如果没有设置字体单位的时候,默认会分配 TypedValue.COMPLEX_UNIT_SP ,即 sp 单位,而最终的值会通过TypedValue.applyDimension(unit, size, r.getDisplayMetrics()) 方法计算出来赋值给 setRawTextSize 方法,所以接下来看怎么计算的:
/**
* Converts an unpacked complex data value holding a dimension to its final floating
* point value. The two parameters <var>unit</var> and <var>value</var>
* are as in {@link #TYPE_DIMENSION}.
*
* @param unit The unit to convert from.
* @param value The value to apply the unit to.
* @param metrics Current display metrics to use in the conversion --
* supplies display density and scaling information.
*
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
可以看到,当单位为 COMPLEX_UNIT_SP时,取值为 value * metrics.scaleDensity;所以接下来看 metrics.scaleDensity 的取值:
/**
* 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.
*/
public float scaledDensity;
public void setTo(DisplayMetrics o) {
if (this == o) {
return;
}
widthPixels = o.widthPixels;
heightPixels = o.heightPixels;
density = o.density;
densityDpi = o.densityDpi;
scaledDensity = o.scaledDensity;
xdpi = o.xdpi;
ydpi = o.ydpi;
noncompatWidthPixels = o.noncompatWidthPixels;
noncompatHeightPixels = o.noncompatHeightPixels;
noncompatDensity = o.noncompatDensity;
noncompatDensityDpi = o.noncompatDensityDpi;
noncompatScaledDensity = o.noncompatScaledDensity;
noncompatXdpi = o.noncompatXdpi;
noncompatYdpi = o.noncompatYdpi;
}
public void setToDefaults() {
widthPixels = 0;
heightPixels = 0;
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
xdpi = DENSITY_DEVICE;
ydpi = DENSITY_DEVICE;
noncompatWidthPixels = widthPixels;
noncompatHeightPixels = heightPixels;
noncompatDensity = density;
noncompatDensityDpi = densityDpi;
noncompatScaledDensity = scaledDensity;
noncompatXdpi = xdpi;
noncompatYdpi = ydpi;
}
注释说明了,scaleDensity 不仅仅受设备的 density 影响,还受用户设定的字体尺寸影响。DisplayMetrics.scaleDensity 在 DisplayMetrics 类中,并没有初始化的地方,可它是一个 public 的字段,也就是说可以被外部赋值初始化。真正为 DisplayMetrics 中各个字段赋值的地方,在 ResourcesImpl 中,有一个 updateConfiguration()
方法,在其中,就有对 scaleDensity 进行初始化的逻辑。
public void updateConfiguration(Configuration config, DisplayMetrics metrics,
CompatibilityInfo compat) {
//省略部分代码
//...........
if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
mMetrics.densityDpi = mConfiguration.densityDpi;
mMetrics.density =
mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
}
// Protect against an unset fontScale.
mMetrics.scaledDensity = mMetrics.density *
(mConfiguration.fontScale != 0 ? mConfiguration.fontScale : 1.0f);
//省略部分代码
//...........
}
可以看到,这里又引入了一个新的计算因子,fontScale。而从 Configuration 的源码又了解到,fontScale 默认值为 1 ,这也就是为什么通常情况下,density 和 scaleDensity 的值是相等的,它们分别影响了 dp 和 sp 最终渲染出来的像素尺寸。
所以,我们要控制字体不随系统字体改变的本质,就是通过修改 fontScale 的值为1,这也就是我们方法1这么做的原因。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)