Android視圖繪制流程之onLayout()

measure過程結(jié)束后,視圖的大小就已經(jīng)測量好了,接下來就是layout的過程了。

正如其名字所描述的一樣,這個方法是用于給視圖進行布局的,也就是確定視圖的位置。

ViewRoot的performTraversals()方法會在measure結(jié)束后繼續(xù)執(zhí)行,并調(diào)用View的layout()方法來執(zhí)行此過程,如下所示:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

layout()方法接收四個參數(shù),分別代表著左、上、右、下的坐標(biāo),當(dāng)然這個坐標(biāo)是相對于當(dāng)前視圖的父視圖而言的??梢钥吹剑@里還把剛才測量出的寬度和高度傳到了layout()方法中。

那么我們來看下layout()方法中的代碼是什么樣的吧,如下所示:

public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
        if (mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
}

在layout()方法中,首先會調(diào)用setFrame()方法來判斷視圖的大小是否發(fā)生過變化,以確定有沒有必要對當(dāng)前的視圖進行重繪,同時還會在這里把傳遞過來的四個參數(shù)分別賦值給mLeft、mTop、mRightmBottom這幾個變量。

接下來調(diào)用onLayout()方法,正如onMeasure()方法中的默認(rèn)行為一樣,也許你已經(jīng)迫不及待地想知道onLayout()方法中的默認(rèn)行為是什么樣的了。進入onLayout()方法,咦?怎么這是個空方法,一行代碼都沒有?!

沒錯,View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定視圖在布局中所在的位置,而這個操作應(yīng)該是由布局來完成的,即父視圖決定子視圖的顯示位置。

既然如此,我們來看下ViewGroup中的onLayout()方法是怎么寫的吧,代碼如下:

    @Override
    protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然后在內(nèi)部按照各自的規(guī)則對子視圖進行布局的。

由于LinearLayout和RelativeLayout的布局規(guī)則都比較復(fù)雜,就不單獨拿出來進行分析了,這里我們嘗試自定義一個布局,借此來更深刻地理解onLayout()的過程。

自定義的這個布局目標(biāo)很簡單,只要能夠包含一個子視圖,并且讓子視圖正常顯示出來就可以了。那么就給這個布局起名叫做SimpleLayout吧,代碼如下所示:

public class SimpleLayout extends ViewGroup {
 
    public SimpleLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }
 
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
        }
    }
}

代碼非常的簡單,我們來看下具體的邏輯吧。
你已經(jīng)知道,onMeasure()方法會在onLayout()方法之前調(diào)用,因此這里在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,如果有的話就調(diào)用measureChild()方法來測量出子視圖的大小。

接著在onLayout()方法中同樣判斷SimpleLayout是否有包含一個子視圖,然后調(diào)用這個子視圖的layout()方法來確定它在SimpleLayout布局中的位置,這里傳入的四個參數(shù)依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表著子視圖在SimpleLayout中左上右下四個點的坐標(biāo)。其中,調(diào)用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。

這樣就已經(jīng)把SimpleLayout這個布局定義好了,下面就是在XML文件中使用它了,如下所示:

<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    <ImageView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        />
    
</com.example.viewtest.SimpleLayout>

可以看到,我們能夠像使用普通的布局文件一樣使用SimpleLayout,只是注意它只能包含一個子視圖,多余的子視圖會被舍棄掉。這里SimpleLayout中包含了一個ImageView,并且ImageView的寬高都是wrap_content。現(xiàn)在運行一下程序,結(jié)果如下圖所示:


Screenshot_2019-05-08-19-44-24-625_com.example.di.png

OK!ImageView成功已經(jīng)顯示出來了,并且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個參數(shù)就行了。

onLayout()過程結(jié)束后,我們就可以調(diào)用getWidth()方法和getHeight()方法來獲取視圖的寬高了。
說到這里,相信很多朋友長久以來都會有一個疑問,getWidth()方法和getMeasureWidth()方法到底有什么區(qū)別呢?
它們的值好像永遠都是相同的。
其實它們的值之所以會相同基本都是因為布局設(shè)計者的編碼習(xí)慣非常好,實際上它們之間的差別還是挺大的。

首先getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了
getWidth()方法要在layout()過程結(jié)束后才能獲取到。

另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設(shè)置的
getWidth()方法中的值則是通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的。

觀察SimpleLayout中onLayout()方法的代碼
這里給子視圖的layout()方法傳入的四個參數(shù)分別是00、childView.getMeasuredWidth()childView.getMeasuredHeight()
因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,所以此時getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你將onLayout()方法中的代碼進行如下修改:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (getChildCount() > 0) {
        View childView = getChildAt(0);
        childView.layout(0, 0, 200, 200);
    }
}

這樣getWidth()方法得到的值就是200 - 0 = 200,不會再和getMeasuredWidth()的值相同了。當(dāng)然這種做法充分不尊重measure()過程計算出的結(jié)果,通常情況下是不推薦這么寫的。getHeight()與getMeasureHeight()方法之間的關(guān)系同上,就不再重復(fù)分析了。

到此為止,我們把視圖繪制流程的第二階段也分析完了。

Home:返回首頁

Previous:onMeasure()

Next:onDraw()


作者:guolin
來源:CSDN
原文:https://blog.csdn.net/guolin_blog/article/details/16330267
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容