Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義控件知識儲備-LayoutParams的那些事

自定義控件知識儲備-LayoutParams的那些事

編輯:關於Android編程

在上一篇文章裡,我總結了一下自定義控件需要了解的基礎知識:View的繪制流程——《自定義控件知識儲備-View的繪制流程》。其中,在View的測量流程裡,View的測量寬高是由父控件的MeasureSpec和View自身的LayoutParams共同決定的。MeasureSpec是什麼,上一篇文章裡已經說得很清楚了(啥,沒看過?快去路克路克,(??????)??)。而LayoutParams呢?是時候在這裡做個了斷了。

LayoutParams是什麼?

LayoutParams,顧名思義,就是Layout Parameters :布局參數。
很久很久以前,我就知道LayoutParams了,並且幾乎天天見面。那時候在布局文件XML裡,寫的最多的肯定是android:layout_width = "match_parent"之類的了。比如:

 

我們都知道layout_widthlayout_height這兩個屬性是為View指定寬度的。不過,當時年輕的我心裡一直有個疑問:為什麼要加上”layout_”前綴修飾呢?其它的描述屬性,如textColorbackground,都很正常啊!講道理,應該用widthheight描述寬高才對啊?

後來呀,我遇到了LayoutParams,它說layout_width是它的屬性而非View的,並且不只是針對這一個,而是所有以”layout_”開頭的屬性都與它有關!所以,它的東西當然要打上自己的標識”layout_”。(呵呵,囂張個啥,到頭來你自己還不是屬於View的一部分( ̄┰ ̄*))

既然layout_width這樣的屬性是LayoutParams定義的,那為何會出現在描述View的xml屬性裡呢?View和LayoutParams之間有什麼恩怨糾纏呢?

不吹不黑,咱們來看看官方文檔是怎麼說的:

LayoutParams are used by views to tell their parents how they want to be laid out.
– LayoutParams是View用來告訴它的父控件如何放置自己的。

The base LayoutParams class just describes how big the view wants to be for both width and height.
– 基類LayoutParams(也就是ViewGroup.LayoutParams)僅僅描述了這個View想要的寬度和高度。

There are subclasses of LayoutParams for different subclasses of ViewGroup.
– 不同ViewGroup的繼承類對應著不同的ViewGroup.LayoutParams的子類。

看著我妙到巅峰的翻譯,想必大家都看懂了<( ̄▽ ̄)/。看不懂?那我再來畫蛇添足稍微解釋一下:

上面我們提到過,描述View直接用它們自己的屬性就好了,如textColorbackground等等,為什麼還需要引入LayoutParams呢?在我看來,textColorbackground這樣的屬性都是只與TextView自身有關的,無論這個TextView處於什麼環境,這些屬性都是不變的。而layout_widthlayout_marginLeft這樣的屬性是與它的父控件息息相關的,是父控件通過LayoutParams提供這些”layout_”屬性給孩子們用的;是父控件根據孩子們的要求(LayoutParams)來決定怎麼測量,怎麼安放孩子們的;是父控件……(寫不下去了,我都快被父控件感動了,不得不再感慨一句,當父母的都不容易啊(′⌒`)) )。所以,View的LayoutParams離開了父控件,就沒有意義了。

基類LayoutParams是ViewGroup類裡的一個靜態內部類(看吧,這就證明了LayoutParams是與父控件直接相關的),它的功能很簡單,只提供了widthheight兩個屬性,對應於xml裡的layout_widthlayout_height。所以,對任意系統提供的容器控件或者是自定義的ViewGroup,其chid view總是能寫layout_widthlayout_height屬性的。

自從有了ViewGroup.LayoutParams後,我們就可以在自定義ViewGroup時,根據自己的邏輯實現自己的LayoutParams,為孩子們提供更多的布局屬性。不用說,系統裡提供給我們的容器控件辣麼多,肯定也有很多LayoutParams的子類啦。let us see see:

ViewGroup.LayoutParams的截圖vcHLuty24Dxjb2RlPlZpZXdHcm91cC5MYXlvdXRQYXJhbXM8L2NvZGU+tcTX08Dgo6zA78PmtPOyv7fWztLDx9OmuMO2vLHIvc/K7M+koaPI57n7xOO+9bXDus3L/MPHsrvK7KOsxMe+zcrHxOPSu8/hx+nUuMCyo6zE49Tnvs0mbGRxdW87zbXNtcP+w/4mcmRxdW87tcTTw7n9y/zDx7rDtuC0zsHLJnJhcnI7XyZyYXJyOzwvcD4NCjxoMyBpZD0="viewgroupmarginlayoutparams">ViewGroup.MarginLayoutParams

我們首先來看看ViewGroup.MarginLayoutParams,看名字我們也能猜到,它是用來提供margin屬性滴。margin屬性也是我們在布局時經常用到的。看看這個類裡面的屬性:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {

        public int leftMargin;

        public int topMargin;

        public int rightMargin;

        public int bottomMargin;

        private int startMargin = DEFAULT_MARGIN_RELATIVE;

        private int endMargin = DEFAULT_MARGIN_RELATIVE;

        ...
    }    

前面4個屬性是我們以前在布局文件裡常用的,而後面的startMarginendMargin是為了支持RTL設計出來代替leftMarginrightMargin的。

一般情況下,View開始部分就是左邊,但是有的語言目前為止還是按照從右往左的順序來書寫的,例如阿拉伯語。在Android 4.2系統之後,Google在Android中引入了RTL布局,更好的支持了從右往左文字布局的顯示。為了更好的兼容RTL布局,google推薦使用MarginStart和MarginEnd來替代MarginLeft和MarginRight,這樣應用可以在正常的屏幕和從右往左顯示文字的屏幕上都保持一致的用戶體驗。

我們除了在布局文件裡用layout_marginLeftlayout_marginTop這樣的屬性來指定單個方向的間距以外,還會用layout_margin來表示四個方向的統一間距。我們來通過源碼看看這一過程:

 public MarginLayoutParams(Context c, AttributeSet attrs) {
            super();

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_MarginLayout_layout_width,
                    R.styleable.ViewGroup_MarginLayout_layout_height);

            int margin = a.getDimensionPixelSize(
                    com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
            if (margin >= 0) {
                leftMargin = margin;
                topMargin = margin;
                rightMargin= margin;
                bottomMargin = margin;
            } else {
                leftMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                        UNDEFINED_MARGIN);
                if (leftMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                    leftMargin = DEFAULT_MARGIN_RESOLVED;
                }
                ... 
            }
            ...
    }        

在這個MarginLayoutParams的構造函數裡,將獲取到的xml布局文件裡的屬性轉化成了leftMagrinrightMagrin等值。先獲取xml裡的layout_margin值,如果未設置,則再去獲取layout_marginLeftlayout_marginRight等值。所以從這裡可以得出一個小結論:

在xml布局裡,layout_margin屬性的值會覆蓋layout_marginLeftlayout_marginRight等屬性的值。

以前我還很傻很天真的猜測,屬性寫在後面,就會覆蓋前面的屬性。雖然經過實踐,也能發現上述的結論,但是自己了解了背後的原理,再去看看源碼實現,自然就有更深刻的印象了。<( ̄ˇ ̄)/

揭開隱藏的LayoutParams

在上文中提到,我們初學Android的時候經常在“偷偷摸摸”的使用著LayoutParams,而自己卻還一臉懵逼。
因為我們常用它的方式是在XML布局文件裡,使用容器控件的LayoutParams裡的各種屬性來給孩子們布局。這種方式直觀方便,直接就能在預覽界面看到效果,但是同時布局也被我們寫死了,無法動態改變。想要動態變化,那還是得不怕麻煩,使用代碼來寫。(實際上,我們寫的XML布局最終也是通過代碼來解析滴)

好的,那還是讓我們通過源碼來揭開隱藏在ViewGroup裡的LayoutParams吧!<( ̄︶ ̄)↗[GO!]……等會,我們該從哪裡開始看源碼呢?我認為有句名言說的在理:

脫離場景談源碼,都是在耍流氓 ——英明神武蘑菇君

上文提到,LayoutParams其實是父控件提供給child view的,好讓child view選擇如何測量和放置自己。所以肯定在child view添加到父控件的那一刻,child view就應該有LayoutParams了。我們來看看幾個常見的添加View的方式:


LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
// 1.直接添加一個“裸”的TextView,不主動指定LayoutParams
TextView textView = new TextView(this);
textView.setText("紅色蘑菇君");
textView.setTextColor(Color.RED);
parent.addView(textView);

// 2.先手動給TextView設定好LayoutParams,再添加
textView = new TextView(this);
textView.setText("綠色蘑菇君");
textView.setTextColor(Color.GREEN);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(300,300);
textView.setLayoutParams(lp);
parent.addView(textView);

// 3.在添加的時候傳遞一個創建好的LayoutParams
textView = new TextView(this);
textView.setText("藍色蘑菇君");
textView.setTextColor(Color.BLUE);
LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,300);
parent.addView(textView, lp2);

上面代碼展示的是3種往LinearLayout裡動態添加TextView的方式,其中都涉及到了addView這個方法。我們來看看addView的幾個重載方法:

//這3個方法都來自於基類ViewGroup

 public void addView(View child) {
        addView(child, -1);
    }

 /*
  * @param child the child view to add
  * @param index the position at which to add the child    
  /
public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }

public void addView(View child, LayoutParams params) {
        addView(child, -1, params);
    }    

可以看出addView(View child)是調用了addView(View child, int index)方法的,在這個裡面對child的LayoutParams做了判斷,如果為null的話,則調用了generateDefaultLayoutParams方法為child生成一個默認的LayoutParams。這也合情合理,畢竟現在這個社會呀,像蘑菇君我這麼懶的人太多,你要是不給個默認的選項,那別說友誼的小船了,就算泰坦尼克,那也說翻就翻!<( ̄︶ ̄)>……好的,那讓我們看看LinearLayout為我們這群懶人生成了怎樣的默認LayoutParams:

@Override
protected LayoutParams generateDefaultLayoutParams() {
        if (mOrientation == HORIZONTAL) {
            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        } else if (mOrientation == VERTICAL) {
            return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        }
        return null;
}

顯然,LinearLayout是重寫了基類ViewGroup裡的generateDefaultLayoutParams方法的:如果布局是水平方向,則孩子們的寬高都是WRAP_CONTENT,而如果是垂直方向,高仍然是WRAP_CONTENT,但寬卻變成了MATCH_PARENT。所以,這一點大家得注意,因為很有可能因為我們的懶,導致布局效果和我們理想中的不一樣。因此呢,第1種添加View的方式是不推薦滴,像第2或第3種方式,添加的時候指定了LayoutParams,不僅明確,而且易修改。(果然還是勤勞致富呀…)

上面三個重載的addView方法最終都調用了addView(View child, int index, LayoutParams params)這個參數最多的方法:

public void addView(View child, int index, LayoutParams params) {
        ...
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {
        ...

        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }

        if (preventRequestLayout) {
            child.mLayoutParams = params;
        } else {
            child.setLayoutParams(params);
        }

        ...
    }    

addView方法又調用了方法addViewInner,在這個私有方法裡,又干了哪些偷偷摸摸的事呢?接著來看看:

//這兩個方法都重寫了基類ViewGroup裡的方法
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LinearLayout.LayoutParams;
}

@Override
  protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p);
}

checkLayoutParams方法的作用是檢查傳遞進來的LayoutParams是不是LinearLayout的LayoutParam。如果不是呢?再通過generateLayoutParams方法根據你傳遞的LayoutParams的屬性構造一個LinearLayout的LayoutParams。不得不再次感慨父容器控件的不容易:我們懶得設置child view的LayoutParams,甚至是設置了錯誤的LayoutParams,父控件都在竭盡所能的糾正我們的錯誤,只為了給孩子提供一個舒適的環境。(╥╯^╰╥)

不過呀,雖然父控件可以在添加View時幫我們糾正部分錯誤,但我們在其他情況下錯誤的修改child View的LayoutParams,那父控件也愛莫能助了。比如下面這種情況:

LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
textView = new TextView(this);
textView.setText("此處有BUG");
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(200,200);
parent.addView(textView, lp);

textView.setLayoutParams(new ViewGroup.LayoutParams(100,100));

會直接報ClassCastException

java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.widget.LinearLayout$LayoutParams

上面這種異常熟悉麼?反正我是相當熟悉的〒▽〒……原因就是上面代碼裡的textView是LinearLayout的孩子,而我們調用textView的setLayoutParams方法強行給它設置了一個ViewGroup的LayoutParams,所有在LinearLayout重新進行繪制流程的時候,在onMeasure方法裡,會進行強制類型轉換操作:

LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

所以App斯巴達了。也許你會說,我才不會這麼傻,我知道textView的父控件是LinearLayout了,我肯定會給它設置相應的LayoutParams的!這是當然的啦,在這種明確的情況下,我們當然不會這麼傻。但是,很不幸的是,有很多時候我們並不能一眼就看出來一個View的LayoutParams是什麼類型的LayoutParams,這就需要動用你的智慧去分析分析啦,希望這篇文章能給你一些幫助。?(^?^●)?

自定義LayoutParams

在本文的開頭就提到過:每個容器控件幾乎都會有自己的LayoutParams實現,像LinearLayout、FrameLayout和RelativeLayout等等。所以,我們在自定義ViewGroup時,幾乎都要自定義相應的LayoutParams。這一節呢,就是對如何自定義LayoutParams進行一個總結。

我以一個簡單的流布局FlowLayout為例,流布局的簡單定義如下:

FlowLayout:添加到此容器的控件自左往右依次排列,如果當前行的寬度不足以容納下一個控件,就會將此控件放置到下一行。

假設這個FlowLayout可以給它的孩子們提供一個gravity屬性,效果就是讓孩子能在某一行的垂直方向上選擇三個位置:top(處於頂部)、center(居中)、bottom(處於底部)。咦?這個效果是不是和LinearLayout提供給孩子的layout_gravity屬性很像?那好,我們來參考一下LinearLayout裡的LayoutParams源碼:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {

        public float weight;

        public int gravity = -1;

        public LayoutParams(Context c, AttributeSet attrs) {

            super(c, attrs);
            TypedArray a =
            c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
            weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
            gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

            a.recycle();
        }


        public LayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }


        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
        }  

         public LayoutParams(LayoutParams source) {
            super(source);

            this.weight = source.weight;
            this.gravity = source.gravity;
        }
    }    

首先,LinearLayout裡的靜態內部類LayoutParams是繼承ViewGroup.MarginLayoutParams的,所以它的孩子們都可以用margin屬性。事實上,絕大部分容器控件都是直接繼承ViewGroup.MarginLayoutParams而非ViewGroup.LayoutParams。所以我們的FlowLayout也直接繼承ViewGroup.MarginLayoutParams

其次,LinearLayout支持兩個屬性weightgravity,這兩個屬性在xml對應的就是layout_weightlayout_gravity。在它的構造函數LayoutParams(Context c, AttributeSet attrs)裡,將獲取到的xml布局文件裡的屬性轉化成了weightgravity的值。不過com.android.internal.R.styleable.LinearLayout_Layout這個東西是什麼鬼?其實這是系統在xml屬性文件裡配置的declare-styleable,好讓系統知道LinearLayout能為它的孩子們提供哪些屬性支持。我們在布局的時候IDE也會給出這些快捷提示。而對於自定義的FlowLayout來說,模仿LinearLayout的寫法,可以在attrs.xml文件裡這麼寫:


        
    

而剩下的幾個構造方法起的作用就是從傳遞的LayoutParams參數裡克隆屬性了。

依葫蘆畫瓢,FlowLayout的LayoutParams如下:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {

        public int gravity = -1;

        public LayoutParams(Context c, AttributeSet attrs) {

            super(c, attrs);
            TypedArray a =
            c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
            weight = a.getFloat(R.styleable.FlowLayout_Layout, 0);
            gravity = a.getInt(R.styleable.FlowLayout_Layout_android_layout_gravity, -1);

            a.recycle();
        }

         public LayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }


        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
        }  


         public LayoutParams(LayoutParams source) {
            super(source);
            this.gravity = source.gravity;
        }

    }    

看起來還是挺簡單的吧?好,那我們這篇文章到此結束……等一等!好像忘記了點什麼……
如果對上面分析ViewGroup的addView方法的流程還有印象,可能你會注意ViewGroup裡的這幾個方法:

 public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return p;
    }

protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return  p != null;
    }

為了能在添加child view時給它設置正確的LayoutParams,我們還需要重寫上面幾個方法(還問為啥要重寫?快翻到前面再see see)。同樣的,我們還是先來看看LinearLayout是怎麼處理的吧:

 @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LinearLayout.LayoutParams(getContext(), attrs);
    }


    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        if (mOrientation == HORIZONTAL) {
            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        } else if (mOrientation == VERTICAL) {
            return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        }
        return null;
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }


    // Override to allow type-checking of LayoutParams.
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LinearLayout.LayoutParams;
    }

那FlowLayout該如何重寫上面的幾個方法呢?相信聰明的你已經知道了。(??????)??

總結

這一篇文章從自定義控件的角度,並結合源碼和表情包生動形象的談了談我所理解的LayoutParams。(生動,形象?真不要臉…(ˉ﹃ˉ))。不得不說,結合源碼來學習某個知識點,的確是能起到事半功倍的作用。蘑菇君初來乍到,文章裡如有錯誤和疏漏之處,歡迎指正和補充。

預告

下一篇文章打算記錄一個簡單的自定義ViewGroup:流布局FlowLayout的實現過程,將自定義控件知識儲備-View的繪制流程裡的知識點和本篇文章的LayoutParams結合起來。

PS:寫博客的初始階段果然是有些艱辛,腦海裡想寫的很多,而真到了要以文字表達出來時,卻有一種“愛你在心口難開”的尴尬。不過,感覺到艱難也就意味著自己在走上坡路,堅持下去,希望能給自己和大家帶來更多的幫助。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved