先说明一下问题的背景。
之前项目有个登录按钮,正常时其背景如下图所示,背景渐变色方向为从左到右。
背景 xml 也很简单(注意:没有设置 angle):
在 Android 9 和 9 之前的手机上都没有问题,但是最近发现,跑在 Android 10 的手机上,登录按钮的背景颜色就变成了这样:
what,渐变方向怎么变成从上到下了???
通过查看对比 Android 9.0 和 10.0 的源码后,发现是没有在 xml 中写android:shape
属性导致的版本兼容性问题。我们来跟踪下源码,看看 Android 10 为什么就会出现这种情况来。
首先需要明确 2 点:
1、 我们在 drawable 目录下新建的标签为 shape 的 XML 文件并非 ShapeDrawable
,而是 GrdientDrawable
;
2、在含有 线性渐变 的 xml 中设置角度android:angle
时,最终会反映到 GrdientDrawable
的内部类变量 mAngle
上。具体地,angle = 0
时,渐变颜色的方向我从左到右,方向为 angle = 90
时,渐变颜色的方向为从下到上,以此类推。而且只能设置 45° 的整数倍角度(源码中有体现)。
关于第 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; // 注释8
//......
}
上面有省略一些无关的代码。注释 5 处,将对象 mGradientState 赋给 st,这个 mGradientState 是怎么来的呢?就是注释2 处的构造方法:
private GradientDrawable(@NonNull GradientState state, @Nullable Resources res) {
mGradientState = state; //注释7
updateLocalState(res);
}
在注释 7 处,通过默认构造方法,得到了 mGradientState 对象,其默认角度方向为从上到下,对应的属性 mAngle = 270
。
因此,在注释 6 处,在获取角度时,由于我们在 drawable 的 xml 中没有设置角度,即R.styleable.GradientDrawableGradient_angle
是没有的,angle 值取后面的值st.mAngle
,也就是 270,对应的方向为从上到下,即TOP_BOTTOM
。
回到最开始的问题,只需要增加android:angle="0"
即可解决显示的问题:
如此,由于设置了角度为 0 , 在updateGradientDrawableGradient()
方法中,R.styleable.GradientDrawableGradient_angle
即为 0 了,angle
和 st.mAngle
也就被赋值为 0 了。
Android 9.0 及之前显示不同,原因是没有setOrientation(orientation)
,即没有根据方向对角度进行修正。其默认方向为从上到下,即TOP_BOTTOM
,而默认角度 mAngle
为 0,但是最终的显示还是以 mAngle
为准,因此不设置角度时,默认变从左到右。
看下Android 9.0 的 updateGradientDrawableGradient()
方法解析是如何解析 angle
的:
private void updateGradientDrawableGradient(Resources r, TypedArray a) throws XmlPullParserException {
final GradientState st = mGradientState; //注释9:
//...省略一些无关代码
if (st.mGradient == LINEAR_GRADIENT) {
int angle = (int) a.getFloat(R.styleable.GradientDrawableGradient_angle, st.mAngle); //注释10:
angle %= 360;
if (angle % 45 != 0) {
throw new XmlPullParserException(a.getPositionDescription()
+ " 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;
}
//...再省略一些无关代码
}
注释 9 处,由于没有重新对 mAngle 赋值,因此,mGradientState
的属性值 mAngle = 0
。因此,注释 10 处得到的 angle
默认值也为 0 。最后会根据角度 0 设置其方向为从左到右。
通过源码,可以得出以下结论:
1、android:angle
设置渐变角度的时候,当 android:angle=“0”
时,方向是从左到右,按照开始颜色到结束颜色来进行渲染的;android:angle=“90”
是从下到上渲染;android:angle=“270”
是从上到下渲染;android:angle=“180”
是从右到左渲染。
2、所设置的角度只能为 45 的整数倍;由于会对 360 取余,android:angle=“360”
和 android:angle=“0”
是一样的。
3、对于线性渐变,xml 中不设置角度时,Android 10 的默认方向从上到下;而 Android 9 及之前版本的默认方向从左到右。