1 背景
先說明一下問題的背景。
之前項目有個登錄按鈕,正常時其背景如下圖所示,背景漸變色方向爲從左到右。
背景 xml 也很簡單(注意:沒有設置 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.0 和 10.0 的源碼後,發現是沒有在 xml 中寫android:shape
屬性導致的版本兼容性問題。我們來跟蹤下源碼,看看 Android 10 爲什麼就會出現這種情況來。
2 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"
即可解決顯示的問題:
<?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>
如此,由於設置了角度爲 0 , 在updateGradientDrawableGradient()
方法中,R.styleable.GradientDrawableGradient_angle
即爲 0 了,angle
和 st.mAngle
也就被賦值爲 0 了。
3 Android 9.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()
+ "<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;
}
//...再省略一些無關代碼
}
註釋 9 處,由於沒有重新對 mAngle 賦值,因此,mGradientState
的屬性值 mAngle = 0
。因此,註釋 10 處得到的 angle
默認值也爲 0 。最後會根據角度 0 設置其方向爲從左到右。
4 總結
通過源碼,可以得出以下結論:
-
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 及之前版本的默認方向從左到右。