源碼分析Android 中ImageView的設置src與background繪制流程

本文原文發表于vcmo博客 by Jie

眾所周知ImageView可以通過src和Background兩種方式設置顯示資源。和大家一起通過源碼來了解兩種屬性的繪制流程有何不同。

熱身案例

先上兩段小代碼:

    <ImageView
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:background="@mipmap/ic_launcher"/>
    <ImageView
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:src="@mipmap/ic_launcher"/>


21-49-56_363701.jpg

android:background="@mipmap/ic_launcher"



21-33-45.jpg

android:src="@mipmap/ic_launcher"


例子中,可以看到ImageView通過設置Background會將圖片資源拉伸至控件的寬高。通過src設置并不會有這樣的現象。這是為什么呢?下面就同大家一起在源碼中找到答案。
ImageView 是View的直接子類,我們先從ImageView的源碼入手。

src與background兩種屬性設置的方法入手

setImageResource()方法入手src屬性

在ImageView的源碼中我們很容易能找到設置ImageResource的方法:


21-49-56.jpg

方法很短

  1. 首先將兩個全局變量的值賦給了局部變量。
  2. 調用了updateDrawable(null),更新Drawable,并將null作為參數。對,是用來初始化的。
  3. 將我們傳入的resId賦值給全局變量mResource
  4. 調用resolveUri();名稱的意思是解析資源
  5. 根據新的寬高是否等于舊的值,判斷是否請求重新計算布局。
  6. invalidate(),重繪

通過上面方法,找到了resolveUri()方法,看看里面做了什么。

    private void resolveUri() {
    
    ···
    
        Drawable d = null;

        if (mResource != 0) {
            try {
                d = mContext.getDrawable(mResource);
            } catch (Exception e) {
                Log.w("ImageView", "Unable to find resource: " + mResource, e);
                // Don't try again.
                mUri = null;
            }
        }
        ···
        updateDrawable(d);
}

代碼不短,我們只關注相關的部分。可以看到在這個方法里面

  • 通過mContext.getDrawable(mResource)獲取到了mResource解析來的Drawable對象實例。
  • mResource就是上面方法第三步賦值我們傳入的resId來的。
  • 拿到Drawable對象d,在方法最后通過updateDrawable(d)方法更新Drawable資源。(沒錯這個方法在上面也用到了,傳入的參數是null初始化)。

接著查看updateDrawable()方法:

    private void updateDrawable(Drawable d) {
        ···
        mDrawable = d;

        if (d != null) {
            d.setCallback(this);
            d.setLayoutDirection(getLayoutDirection());
            if (d.isStateful()) {
                d.setState(getDrawableState());
            }
            d.setVisible(getVisibility() == VISIBLE, true);
            d.setLevel(mLevel);
            mDrawableWidth = d.getIntrinsicWidth();
            mDrawableHeight = d.getIntrinsicHeight();
            applyImageTint();
            applyColorMod();

            configureBounds();
        }
}

同樣我們看重點:

  1. 將d賦值全局變量mDrawable.
  2. 通過d.getIntrinsicWidth()拿到drawable的width賦值全局變量mDrawableWidth
  3. 通過d.getIntrinsicHeight()拿到drawable的height賦值全局變來那個mDrawableHeight
  4. 調用configureBounds()方法配置邊界。

OK,去看configureBounds()方法:


    private void configureBounds() {
        ···
        int dwidth = mDrawableWidth;
        int dheight = mDrawableHeight;

        int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        int vheight = getHeight() - mPaddingTop - mPaddingBottom;
        
        if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
            mDrawable.setBounds(0, 0, vwidth, vheight);
            mDrawMatrix = null;
        } else {
            mDrawable.setBounds(0, 0, dwidth, dheight);
            ···
        }
    }

代碼較多,但是并不復雜,我們只看與我們討論相關的部分(中間面省略的部分是與ScaleType屬相相關的代碼,下次單獨拿出來分析)

  1. 計算出view的內部寬高并賦值局部變量。
  2. 判斷drawable的寬高是否沒有意義,或者是設置ScaleType為FIX_XY,就通過mDrawable.setBounds()方法將mDrawable的邊界范圍設置為View的內部大小。
  3. 否則,將mDrawable的邊界范圍設置為drawable資源的寬高。(所以src默認不會拉伸圖片)

最后就是在ondraw()方法中將mDrawable繪制出來。到此整個的src屬性的設置到繪制的流程已經清楚。


接著我們來看Background屬性是如何作用于View的。

setBackgroundResource()方法入手Background屬性

在ImageView的源碼中我們通過搜索發現并沒有Background相關的設置方法。在它的父類—View中,可以找到我們想要的代碼。

    public void setBackgroundResource(@DrawableRes int resid) {
        if (resid != 0 && resid == mBackgroundResource) {
            return;
        }
        Drawable d = null;
        if (resid != 0) {
            d = mContext.getDrawable(resid);
        }
        setBackground(d);

        mBackgroundResource = resid;
    }

代碼簡短,同樣先根據resid拿到drawable對象,然后調用了setBackground()方法。最終調用了setBackgroundDrawable()方法。在這個方法最終將drawable賦值給了一個全局變量mBackground,在這個方法中并沒有發現設置drawable的bounds相關的方法,那么我們接著往后走,到繪制層。View的onDraw()方法是空的,我們知道onDraw()方法其實是由View的draw()方法調用的,在draw()方法中是進行View的基本繪制,onDraw()是擴展視圖自己繪制的方法。


直奔draw()方法

00-53-01.jpg

很開心,google的工程師給我們寫了很清晰的注釋。第一步就是繪制background,只調用了一個方法drawBackground()。乘勝追擊。

    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();
        ···
}

看到了,調用了一個setBackGroundBounds()方法,追

    void setBackgroundBounds() {
        if (mBackgroundSizeChanged && mBackground != null) {
            mBackground.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            rebuildOutline();
        }
    }

簡短精悍的代碼,mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop),設置了mBackground這個drawable的邊界。就是View的寬高。

  • 此處是直接使用mRight - mleft 和 mBottom - mTop,也就是說忽略padding的存在。
  • 在src屬性中,使用到容器的寬高是這樣獲得的
        int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        int vheight = getHeight() - mPaddingTop - mPaddingBottom;

padding是起作用的。通過兩個簡單的例子看看效果。

01-09-06.jpg
01-08-32.jpg


01-10-21.jpg
01-07-19.jpg

總結

通過源碼的閱讀,清楚了ImageView 與 Background的區別與聯系以及繪制的流程:

  1. 都是Drawable資源的繪制,所以他們內容是可以互通的。
  2. background是View的屬性,在View層繪制(所以Background稱為背景,因為它在自定義View的onDraw()方法被調用前就已經被調用繪制了),所有的控件都具有該屬性,src是ImageView中定義的屬性,在ImageView的onDraw方法中才被繪制。只有ImageView即其子類才有此屬性。
  3. background是縮放填充式的繪制,src可以通過ScaleType設置不同的縮放效果。
  4. padding屬性對background無效,但對ImageView的src是有效的。

第一次寫技術類的文章,如有錯誤還望多多指正。歡迎分享交流。
郵箱: liuj4563@163.com
原創博文 轉載請注明出處

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容