Android 10 使用 drawable xml 設置漸變背景的坑

1 背景

先說明一下問題的背景。

之前項目有個登錄按鈕,正常時其背景如下圖所示,背景漸變色方向爲從左到右。
`圖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 的手機上,登錄按鈕的背景顏色就變成了這樣:
圖2

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 源碼一致)。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;  // 註釋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 了,anglest.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 及之前版本的默認方向從左到右。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章