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 的手机上,登录按钮的背景颜色就变为如下:
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 源码一样)。GradientState
为 GrdientDrawable
的内部类,封装了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
属性的,为了兼容起见,最好都补上。