Android 10 使用 drawable 背景未设置 angle 渐变方向的踩坑记录

2,515 阅读5分钟

1 背景

先说明一下问题的背景,也是笔者偶然发现的。

之前有个登录按钮,正常时其背景如下图所示,背景渐变色方向为从左到右。 `图1

设置背景的 drawable xml 也很简单(注意:gradient节点中没有设置 android:angle):

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:startColor="#FF5D86"
        android:endColor="#8359FF"/>
    <corners android:radius="50dp"/>
</shape>

在 Android 9 和 9 之前的手机上都没有问题,但是最近发现,跑在 Android 10 的手机上,登录按钮的背景颜色就变为如下: 图2

what,渐变方向怎么变成从上到下了???

通过查看对比 Android 9 和 Android 10 的源码后,发现是android:angle属性差异导致的兼容性问题。那我们一起来跟踪下源码,看看 Android 10 为什么会出现这种问题。

2 Android 10 源码分析

首先需要明确 2 点:

  • 1、 我们在 drawable 目录下新建的标签为 shape 的 XML 文件并非 ShapeDrawable,而是 GrdientDrawable

  • 2、在含有 线性渐变 的 xml 中设置角度android:angle="0",最终会反映到 GrdientDrawable的内部类变量 mAngle上。

关于第 1 点,做个 Demo 验证一下:

以上面的 xml 文件给 TextView 设置背景,然后通过其 getBackground()方法获取背景并打印:

Log.e("tag", textView.getBackground());

结果如下:

android.graphics.drawable.GradientDrawable@4f98d69

其实,Android 将 drawable XML 文件解析为对应的 Drawable 是通过 DrawableInflater 类进行的,关键方法如下:

//android.graphics.drawable#DrawableInflater

 private Drawable inflateFromTag(@NonNull String name) {
        switch (name) {
            case "selector":
                return new StateListDrawable();
            case "animated-selector":
                return new AnimatedStateListDrawable();
            case "level-list":
                return new LevelListDrawable();
            case "layer-list":
                return new LayerDrawable();
            case "transition":
                return new TransitionDrawable();
            case "ripple":
                return new RippleDrawable();
            case "adaptive-icon":
                return new AdaptiveIconDrawable();
            case "color":
                return new ColorDrawable();
            case "shape":
                return new GradientDrawable(); //注释1:解析shape标签
   
        //。。。省略其余无关代码

            default:
                return null;
        }
    }

注释 1 也再次证明了shape 标签的 Drawable 对应的是GadientDrawable

既然 xml 文件最终都转换为 GrdientDrawable 进行显示,那么就看看 Android 10 的源码做了什么操作才导致如果不设置 angle ,默认方向就变为从上到下的。

以下是基于 Android 10 的GrdientDrawable 源码进行分析的。

GradientDrawable源码链接(顺便安利下这个网站,非常好用)

回到上面的注释 1,解析 shape 标签时,会先调用 GrdientDrawable 的构造方法,进入该构造方法:

    public GradientDrawable() {
        this(new GradientState(Orientation.TOP_BOTTOM, null), null); //注释2:调用GradientState的构造方法
    }
    
    //方向枚举
    public enum Orientation {
        /** draw the gradient from the top to the bottom */
        TOP_BOTTOM,
        /** draw the gradient from the top-right to the bottom-left */
        TR_BL,
        /** draw the gradient from the right to the left */
        RIGHT_LEFT,
        /** draw the gradient from the bottom-right to the top-left */
        BR_TL,
        /** draw the gradient from the bottom to the top */
        BOTTOM_TOP,
        /** draw the gradient from the bottom-left to the top-right */
        BL_TR,
        /** draw the gradient from the left to the right */
        LEFT_RIGHT,
        /** draw the gradient from the top-left to the bottom-right */
        TL_BR,
    }

注释 2,通过 GradientState 的构造方法创建一个 GradientState 对象,其方向默认为 TOP_BOTTOM, 也就是从上到下(这个默认方法与 Android 9.0 源码一样)。GradientStateGrdientDrawable 的内部类,封装了GrdientDrawable的所有属性,比如我们现在需要关注的角度 mAngle等。再看所调用的GradientState 构造方法:

    public GradientState(Orientation orientation, int[] gradientColors) {
        setOrientation(orientation); //注释3:不一样的关键点
        setGradientColors(gradientColors);
    }

注释 3 的setOrientation() 方法为角度改变的关键位置,该方法为 Android 10 新增

     public void setOrientation(Orientation orientation) {
        // Update the angle here so that subsequent attempts to obtain the orientation
        // from the angle overwrite previously configured values during inflation
        mAngle = getAngleFromOrientation(orientation); //注释4:此处的 orientation为TOP_BOTTOM
        mOrientation = orientation;
    }

可见,mAngle 会通过 getAngleFromOrientation(orientation)方法重新赋值,看看该方法:

        private int getAngleFromOrientation(@Nullable Orientation orientation) {
            if (orientation != null) {
                switch (orientation) {
                    default:
                    case LEFT_RIGHT:
                        return 0;
                    case BL_TR:
                        return 45;
                    case BOTTOM_TOP:
                        return 90;
                    case BR_TL:
                        return 135;
                    case RIGHT_LEFT:
                        return 180;
                    case TR_BL:
                        return 225;
                    case TOP_BOTTOM:
                        return 270;
                    case TL_BR:
                        return 315;
                }
            } else {
                return 0;
            }
        }

到这里,也就很明朗了,通过一系列方法调用,GradientDrawable 的内部类GradientState的变量mAngle 的值被初始化为 270 了。

显示出来的angle会通过updateGradientDrawableGradient()方法解析:

 private void updateGradientDrawableGradient(Resources r, TypedArray a) {
        final GradientState st = mGradientState; //注释5:
     //...省略一些无关代码
      int angle = (int) a.getFloat(R.styleable.GradientDrawableGradient_angle, st.mAngle); //注释6
      st.mAngle = ((angle % 360) + 360) % 360; // offset negative angle measures
    //...再省略一些无关代码
 }

注释 5 处,将对象 mGradientState 赋给 st,这个 mGradientState 是怎么来的呢?就是 注释2 处的构造方法:

    private GradientDrawable(@NonNull GradientState state, @Nullable Resources res) {
        mGradientState = state; //注释7:得到 mGradientState 对象,其属性mAngle=270

        updateLocalState(res);
    }

在注释 7 处,通过默认构造方法,得到了 mGradientState 对象,其默认角度属性 mAngle = 270

因此,在注释 6 处,在获取角度时,由于我们在 drawable 的 xml 中没有设置角度,角度取值st.mAngle,也就是 270,方向为从上到下,即TOP_BOTTOM

回到最开始的问题,只需要增加 android:angle="0"即可解决显示的问题:

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="0"
        android:startColor="#FF5D86"
        android:endColor="#8359FF"/>
    <corners android:radius="50dp"/>
</shape>

如此,updateGradientDrawableGradient()中,st.mAngle即被赋值为 0 了。

3 Android 9.0 为什么没问题

Android 9.0 及之前显示不同,原因是没有setOrientation(orientation),即没有根据方向对角度进行修正,导致其默认方向为从上到下,即TOP_BOTTOM,而默认角度 mAngle为 0。

看下Android 9.0 的 updateGradientDrawableGradient()方法解析是如何解析 angle的:

 private void updateGradientDrawableGradient(Resources r, TypedArray a) throws XmlPullParserException {
        final GradientState st = mGradientState;  //注释8:
     //...省略一些无关代码
     if (st.mGradient == LINEAR_GRADIENT) {
            int angle = (int) a.getFloat(R.styleable.GradientDrawableGradient_angle, st.mAngle); //注释9:
            angle %= 360;

            if (angle % 45 != 0) {
                throw new XmlPullParserException(a.getPositionDescription()
                        + "<gradient> tag requires 'angle' attribute to "
                        + "be a multiple of 45");
            }

            st.mAngle = angle;

            switch (angle) {
                case 0:
                    st.mOrientation = Orientation.LEFT_RIGHT;
                    break;
                case 45:
                    st.mOrientation = Orientation.BL_TR;
                    break;
                case 90:
                    st.mOrientation = Orientation.BOTTOM_TOP;
                    break;
                case 135:
                    st.mOrientation = Orientation.BR_TL;
                    break;
                case 180:
                    st.mOrientation = Orientation.RIGHT_LEFT;
                    break;
                case 225:
                    st.mOrientation = Orientation.TR_BL;
                    break;
                case 270:
                    st.mOrientation = Orientation.TOP_BOTTOM;
                    break;
                case 315:
                    st.mOrientation = Orientation.TL_BR;
                    break;
            }
    //...再省略一些无关代码
 }

注释 8 处,由于没有重新对 mAngle 赋值,因此,mGradientState 的属性值 mAngle = 0。因此,注释 9 处得到的 angle 默认值也为 0 。不过,最后还是会根据角度 0 设置其方向为从左到右。

4 总结

通过源码,可以得出以下结论:

  • android:angle 设置渐变角度的时候,当 android:angle=“0” 时,方向是从左到右,按照开始颜色到结束颜色来进行渲染的;android:angle=“90” 是从下到上渲染;android:angle=“270” 是从上到下渲染;android:angle=“180” 是从右到左渲染。

  • 所设置的角度只能为 45 的倍数;由于会对 360 取余,android:angle=“360”android:angle=“0”是一样的。

  • 对于线性渐变,xml 中不设置android:angle时,Android 10 的默认角度为 270,即方向从上到下;而 Android 9 及之前版本的默认角度为 0,即方向从左到右。所以,项目中如果没有设置android:angle属性的,为了兼容起见,最好都补上。