Android APP字体随系统字体调整造成界面布局混乱问题解决方案

一、遇到的问题

当用户调整系统字体大小的时候,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这么做的原因。

 

好文分享:当你在设置里修改字体大小的时候,到底在修改什么

Android View篇之 字体大小 调整滑杆的实现

Android 仿微信/支付宝 字体大小 调整控件

修改Android系统字体大小

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值