一、遇到的问题:
当用户调整系统字体大小的时候,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中实现全部界面字体不随系统更改。
2. 在具体的界面把不想要放大的View字体单位设置为dp
三、原理解析:
到底为什么设置为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.
*
*
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.
*
*
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 unit and value
* 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这么做的原因。