當前位置:
首頁 > 知識 > 自定義組件開發七 自定義容器

自定義組件開發七 自定義容器

概述

自定義容器本質上也是一個組件,常見的 LinearLayout、FrameLayout、GridLayout、ScrollView和 RelativeLayout 等等組件都是容器,容器除了有自己的外觀,還能用來容納各種組件,以一種特定的規則規定組件應該在什麼位置、顯示多大。

一般情況下,我們更關注自定義組件的外觀及功能,但自定義容器則更關注其內的組件怎麼排列和擺放,比如線性布局 LinearLayout 中的組件只能水平排列或垂直排列,幀布局 FrameLayout中的組件可以重疊,相對布局 RelativeLayout 中的組件可以以某一個組件為參照定位自身的位置……容器還關注組件與容器四個邊框之間的距離(padding),或者容器內組件與組件之間的距離(margin)。

事實上,容器是可以嵌套的,一個容器中,既可以是普通的子組件,也可以是另一個子容器。

容器類一般要繼承 ViewGroup 類,ViewGroup 類同時也是 View 的子類,如圖所示,ViewGroup 又是一個抽象類,定義了 onLayout()等抽象方法。當然,根據需要,我們也可以讓容器類繼承自 FrameLayout 等 ViewGroup 的子類,比如 ListView 繼承自 ViewGroup,而 ScrollView水平滾動容器類則從 FrameLayout 派生。

自定義組件開發七 自定義容器

ViewGroup 類

ViewGroup 常用方法

ViewGroup 作為容器類的父類,自然有他自己鮮明的特徵,開發自定義容器必須先要了解ViewGroup。

在 ViewGroup 中,定義了一個 View[]類型的數組 mChildren,該數組保存了容器中所有的子組件,負責維護組件的添加、移除、管理組件順序等功能,另一個成員變數 mChildrenCount 則保存了容器中子組件的數量。在布局文件(layout)中,容器中的子元素會根據順序自動添加到 mChildren 數組中。

ViewGroup 具備了容器類的基本特徵和運作流程,也定義了相關的方法用於訪問容器內的

組件,主要的方法有:

public int getChildCount()

獲取容器內的子組件的個數;

1

2

public View getChildAt(int index)

容器內的所有子組件都存儲在名為 mChildren 的 View[]數組中,

該方法通過索引 index找到指定位置的子組件;

1

2

3

public void addView(View child, int index, LayoutParams params)

向容器中添加新的子組件,child 表示子組件(也可以是子容器),index 表示索引,指

定組件所在的位置,params 參數為組件指定布局參數,該方法還有兩個簡化的版本:

public void addView(View child, LayoutParams params):添加 child 子組件,並為該子組件指定布局參數;

public void addView(View child, int index):布局參數使用默認的 ViewGroup.LayoutParams,其中 layout_width 和 layout_height 均為 wrap_content;

public void addView(View child):布局參數同上,但 index 為-1,表示將 child 組件添加到 mChildren 數組的最後。

向容器中添加新的子組件時,子組件不能有父容器,否則會拋出「The specified child

already has a parent(該組件已有父容器)」的異常。

1

2

3

4

5

6

7

8

9

10

11

12

public void removeViewAt(int index)

移除 index 位置的子組件

類似的方法還有:

public void removeView(View view)

移除子組件 view;

public void removeViews(int start, int count)

移除從 start 開始連續的 count 個子組件。

1

2

3

4

5

6

7

8

9

protected void measureChild(View child,

int parentWidthMeasureSpec, int parentHeightMeasureSpec)

測量子組件的尺寸。

類似的方法還有:

protected void measureChildren(int widthMeasureSpec,

int heightMeasureSpec):

測量所有子組件的尺寸;

public final void measure(int widthMeasureSpec, int heightMeasureSpec):

該方法從 View類 中 繼 承 , 用 於 測 量 組 件 或 容 器 自 己 的 尺 寸 ,

參數 widthMeasureSpec 和heightMeasureSpec 為 0 時表示按實際大小進行測量,

將 0 傳入方法常常會有奇效。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

ViewGroup 運行的基本流程大致為:

1) 測量容器尺寸

重寫 onMeasure()方法測量容器大小,和自定義組件有所區別的是,在測量容器大小之

前,必須先調用 measureChildren()方法測量所有子組件的大小,不然結果永遠為 0。

2) 確定每個子組件的位置

重寫 onLayout()方法確定每個子組件的位置(這個其實挺麻煩,也是定義容器的難點部

分),在 onLayout()方法中,調用 View 的 layout()方法確定子組件的位置。

3) 繪製容器

重寫 onDraw()方法,其實 ViewGroup 類並沒有重寫 onDraw()方法,除非有特別的要求,自定義容器也很少去重寫。比如 LinearLayout 重寫了該方法用於繪製水平或垂直分割條,而 FrameLayout 則是重寫了 draw()方法,作用其實是一樣的。

自定義組件開發七 自定義容器

我們來看看容器類的基本結構。

public class MyViewGroup extends ViewGroup {

public MyViewGroup(Context context) {

super(context);

}

public MyViewGroup(Context context, AttributeSet attrs) {

super(context, attrs);

}

public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

}

/**

* 確定每個子組件的位置

*

* @param changed 是否有新的尺寸或位置

* @param l left

* @param t top

* @param r right

* @param b bottom

*/

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

}

/**

* 測量容器的尺寸

*

* @param widthMeasureSpec 寬度模式與大小

* @param heightMeasureSpec 高度模式與大小

*/

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//先測量所有子組件的大小

this.measureChildren(widthMeasureSpec, heightMeasureSpec);

}

/**

* 繪製容器

*

* @param canvas

*/

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

ViewGroup 的工作原理

第一節介紹過 View 的工作流程,ViewGroup 作為 View 的子類,流程基本是相同的,但

另一方面 ViewGroup 作為容器的父類,又有些差異,我們通過閱讀源碼來了解 ViewGroup 的工作原理。

前面說到,重寫 ViewGroup 的 onMeasure()方法時,必須先調用 measureChildren()方法測量子組件的尺寸,該方法源碼如下:

protected void measureChildren(int widthMeasureSpec,

int heightMeasureSpec) {

final int size = mChildrenCount;

final View[] children = mChildren;

for (int i = 0; i < size; ++i) {

final View child = children[i];

if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {

measureChild(child, widthMeasureSpec, heightMeasureSpec);

}

}

}

1

2

3

4

5

6

7

8

9

10

11

measureChildren()方法中,循環遍歷每一個子組件,如果當前子組件的可見性不為 GONE 也就是沒有隱藏則繼續調用 measureChild(child, widthMeasureSpec, heightMeasureSpec)方法測量當前子組件 child 的大小,我們繼續進入 measureChild()方法。

protected void measureChild(View child, int parentWidthMeasureSpec,

int parentHeightMeasureSpec) {

final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec =

getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight, lp.width);

final int childHeightMeasureSpec =

getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

1

2

3

4

5

6

7

8

9

10

11

measureChild()方法結合父容器的 MeasureSpec、子組件的 Padding 和 LayoutParams 三個因素 利 用 getChildMeasureSpec() 計 算 出 子 組 件 的 尺 寸 模 式 和 尺 寸 大 小 ( 可 以 跟 蹤 到getChildMeasureSpec()方法中查看),並調用子組件的 measure()方法進行尺寸測量。measure()方法的實現如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

……

onMeasure(widthMeasureSpec, heightMeasureSpec);

……

}

1

2

3

4

5

真相慢慢露出水面,measure()方法調用了 onMeasure(widthMeasureSpec, heightMeasureSpec)方法,該方法正是我們重用的用來測量組件尺寸的方法,至此,測量組件尺寸的工作已掌握到開發人員手中。

根據上面的代碼跟蹤我們發現,從根元素出發,一步步向下遞歸驅動測量,每個組件又負責計算自身大小,OOP 的神奇之處就這樣在實際應用中體現出來了。

接下來調用 onLayout()方法定位子組件,以確定子組件的位置和大小,在 onLayout()方法中,我們將調用子組件的 layout()方法,這裡要一分為二,如果子組件是一個 View,定位流程到此結束,如果子組件又是一個容器呢?我們進入 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 = isLayoutModeOptical(mParent) ?

setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED)

== PFLAG_LAYOUT_REQUIRED) {

onLayout(changed, l, t, r, b);

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

……

}

……

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

如果子組件是一個容器,又會繼續調用該容器的 onLayout()方法對孫組件進行定位,所以,onLayout()方法也是一個遞歸的過程。

onMeasure()方法和 onLayout()方法調用完成後,該輪到 onDraw()方法了,ViewGroup 類並沒有重寫該方法,但是,從第一章中我們都知道每一個組件在繪製時是會調用 View 的 draw()方法的,我們進入 draw()方法進行跟蹤。

public void draw(Canvas canvas) {

……

/*

* Draw traversal performs several drawing steps which must be executed

* in the appropriate order:

*

* 1. Draw the background

* 2. If necessary, save the canvas" layers to prepare for fading

* 3. Draw view"s content

* 4. Draw children

* 5. If necessary, draw the fading edges and restore layers

* 6. Draw decorations (scrollbars for instance)

*/

// Step 1, draw the background, if needed

int saveCount;

if (!dirtyOpaque) {

drawBackground(canvas);

}

// skip step 2 & 5 if possible (common case)

final int viewFlags = mViewFlags;

boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;

boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

if (!verticalEdges && !horizontalEdges) {

// Step 3, draw the content

if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

// Step 6, draw decorations (scrollbars)

onDrawScrollBars(canvas);

if (mOverlay != null && !mOverlay.isEmpty()) {

mOverlay.getOverlayView().dispatchDraw(canvas);

}

// we"re done...

return;

}

……

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

draw()方法中執行了語句 dispatchDraw(canvas),但是,當我們跟蹤到 View類的 dispatchDraw()方法時發現該方法是空的,但對於 ViewGroup 來說,該方法的作用非同小可,ViewGroup 重寫了dispatchDraw()方法。

protected void dispatchDraw(Canvas canvas) {

final int childrenCount = mChildrenCount;

final View[] children = mChildren;

……

for (int i = 0; i < childrenCount; i++) {

int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;

final View child = (preorderedList == null)

? children[childIndex] : preorderedList.get(childIndex);

if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE

|| child.getAnimation() != null) {

more |= drawChild(canvas, child, drawingTime);

}

}

……

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

dispatchDraw()方法的作用是將繪製請求紛發到給子組件,並調用 drawChild()方法來完成子組件的繪製,drawChild()方法的源碼如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

return child.draw(canvas, this, drawingTime);

}

1

2

3

drawChild()方法再次調用了子組件的 boolean draw(Canvas canvas, ViewGroup parent, longdrawingTime)方法,該方法定義如下:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

……

if (!layerRendered) {

if (!hasDisplayList) {

// Fast path for layouts with no backgrounds

if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {

mPrivateFlags &= ~PFLAG_DIRTY_MASK;

dispatchDraw(canvas);

if (mOverlay != null && !mOverlay.isEmpty()) {

mOverlay.getOverlayView().draw(canvas);

}

} else {

draw(canvas);

}

drawAccessibilityFocus(canvas);

} else {

mPrivateFlags &= ~PFLAG_DIRTY_MASK;

((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);

}

}

……

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

上面的方法又調用了 draw(Canvas canvas)方法,如果子組件不再是一個容器,將調用 if

(!dirtyOpaque) onDraw(canvas)語句完成組件的繪製,同樣地,onDraw(canvas)正是需要我們重寫的方法。所以,組件的繪製同樣是一個不斷遞歸的過程。

重寫 onLayout() 方法

在容器類的基本結構中,我們最陌生的是 onLayout()方法,該方法原型為: protected voidonLayout(boolean changed, int l, int t, int r, int b),其中,參數 changed 判斷是否有新的大小和位置,l 表示 left,t 表示 top,r 表示 right,b 表示 bottom,後面的 4 個參數表示容器自己相對父容器的位置以及自身的大小,通常情況下,r - l 的值等同於方法 getMeasuredWidth()方法的返回值,b - t 的值等同於 getMeasuredHeight()方法的返回值。關於 l、t、r、b 參數的值的理解如圖所示。

在 onLayout()方法中,需要調用 View 的 layout()方法用於定義子組件和子容器的位置,

layout()方法的原理如下:

public void layout(int l, int t, int r, int b)

參數 l、t、r、b 四個參數的作用與上面相同,通過這 4 個參數,基本可以確定子組件的位置與大小了。

下面我們來看一個案例,該案例一是為了說明自定義容器的定義方法,二是了解重寫ViewGroup 方法的一些細節。

public class SizeViewGroup extends ViewGroup {

public SizeViewGroup(Context context) {

this(context, null, 0);

}

public SizeViewGroup(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

public SizeViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

//創建一個按鈕

TextView textView = new TextView(context);

ViewGroup.LayoutParams layoutParams =

new ViewGroup.LayoutParams(200, 200);

textView.setText("Android");

textView.setBackgroundColor(Color.YELLOW);

//在當前容器中添加子組件

this.addView(textView, layoutParams);

//設置容器背景顏色

this.setBackgroundColor(Color.alpha(255));

}

/**

* 確定組件位置與大小

*/

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

//設置子組件(此處為 TextView)的位置和大小

//只有一個組件,索引為 0

View textView = this.getChildAt(0);

textView.layout(50, 50, textView.getMeasuredWidth() + 50,

textView.getMeasuredHeight() + 50);

}

/**

* 測量組件

*/

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//先測量所有子組件的大小

this.measureChildren(widthMeasureSpec, heightMeasureSpec);

//測量自身的大小,此處直接寫死為 500 * 500

this.setMeasuredDimension(500, 500);

}

/**

* 繪製

*/

@Override

protected void onDraw(Canvas canvas) {

//為容器畫一個紅色邊框

RectF rect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());

rect.inset(2, 2);

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setStyle(Paint.Style.STROKE);

paint.setStrokeWidth(2);

paint.setColor(Color.BLACK);

Path path = new Path();

path.addRoundRect(rect, 20, 20, Path.Direction.CCW);

canvas.drawPath(path, paint);

super.onDraw(canvas);

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

上面的代碼中,我們定義了一個容器,為了不增加難度,刻意做了簡化。在 public

SizeViewGroup(Context context, AttributeSet attrs, int defStyleAttr)構造方法中,使用代碼創建了一個 TextView 子組件,並將 SizeViewGroup 的背景設置成了透明(如果要繪製一個不規則的自定義組件,將背景設置成透明是個不錯的辦法)。onMeasure()方法用來測量容器大小,在測量容器之前,必須先調用 measureChildren()方法測量所有子組件的大小,本例中將容器的大小設置成了一個不變的值 500 * 500,所以,儘管在布局文件中將 layout_width 和 layout_height 都定義為match_parent,但事件上這個值並不起作用。onLayout()方法負責為子組件定位並確認子組件大小,因為只有一個子組件,所以先通過 getChildAt(0)獲取子組件(TextView 對象)對象,再調用子組件的 layout()方法確定組件區域,子組件的 left 為 50,top 為 50,right 為 50 加上測量寬度,bottom為 50 加上測量高度。onDraw()方法為容器繪製了一個圓角矩形作為邊框。運行效果如圖 所示。

我們知道,一個組件多大,取決於 layout_width 和 layout_height 的大小,但真正決定組件

大小的是 layout()方法,我們將 onLayout()方法改成如下的實現:

protected void onLayout(boolean changed, int l, int t, int r, int b) {

//設置子組件(此處為 TextView)的位置和大小

View textView = this.getChildAt(0);//只有一個組件,索引為 0

textView.layout(50, 50, 200, 100);

}

1

2

3

4

5

再次運行,結果如圖所示。從結果中看出,此時 TextView 組件變小了,說明一個組件的真正大小不是取決於 layout_width 和 layout_height,而是取決於 layout()方法。但是很顯然,通過layout()方法計算組件大小時,layout_width 和 layout_height 屬性是非常重要的參考因素。

CornerLayout 布局

基本原理

CornerLayout 布局是一個自定義容器,用於將子組件分別顯示在容器的 4 個角落,不接受超過 4 個子組件的情形,默認情況下,子組件按照從左往右、從上往下的順序放置,但可以為子組件指定位置(左上角 left_top、右上角 right_top、左下角 left_bottom、右下角 right_bottom)。如果說前面的案例為了簡化形式有些偷工減料,那麼本小節將以一個完整的案例來講述自定義容器的基本實現。

先畫一個草圖來幫助我們分析,如圖。

上圖中,藍色框表示 CornerLayout 布局的區域,A、B、C、D 是 CornerLayout 內的 4 個子組件,對於 CornerLayout 來說,首先要測量的是他的尺寸大小,當 layout_width 為 wrap_content 時,其寬度為 A、C 的最大寬度和 B、D 的最大寬度之和,當 layout_height 為 wrap_content 時,其高度為 A、B 的最大高度 C、D 的最大高度之和,這樣才不至於子組件出現重疊。當然,如果layout_width 和 layout_height 指定了具體值或者屏幕不夠大的情況下設置為 match_parent,子組件仍有可能會出現重疊現象。

我們先將 CornerLayout 容器的基本功能開發出來,基本功能包括:支持 0~4 個子組件;每個子組件放置在不同的角落;完美支持 layout_width 和 layout_height 屬性。

public class CornerLayout extends ViewGroup {

public CornerLayout(Context context) {

super(context);

}

public CornerLayout(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

public CornerLayout(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

}

/**

* 定位子組件

*/

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

for (int i = 0; i < getChildCount(); i++) {

View child = getChildAt(i);

if (i == 0) {

//定位到左上角

child.layout(0, 0, child.getMeasuredWidth(),

child.getMeasuredHeight());

} else if (i == 1) {

//定位到右上角

child.layout(getMeasuredWidth() - child.getMeasuredWidth(),

0, getMeasuredWidth(), child.getMeasuredHeight());

} else if (i == 2) {

//定位到左下角

child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(),

child.getMeasuredWidth(), getMeasuredHeight());

} else if (i == 3) {

//定位到右下角

child.layout(getMeasuredWidth() - child.getMeasuredWidth(),

getMeasuredHeight() - child.getMeasuredHeight(),

getMeasuredWidth(), getMeasuredHeight());

}

}

}

/**

* 測量尺寸

*

* @param widthMeasureSpec

* @param heightMeasureSpec

*/

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//先測量所有子組件的大小

this.measureChildren(widthMeasureSpec, heightMeasureSpec);

//再測量自己的大小

int width = this.measureWidth(widthMeasureSpec);

int height = this.measureHeight(heightMeasureSpec);

//應用尺寸

this.setMeasuredDimension(width, height);

}

/**

* 測量容器的寬度

*

* @param widthMeasureSpec

* @return

*/

private int measureWidth(int widthMeasureSpec) {

int mode = MeasureSpec.getMode(widthMeasureSpec);

int size = MeasureSpec.getSize(widthMeasureSpec);

int width = 0;

if (mode == MeasureSpec.EXACTLY) {

//match_parent 或具體值

width = size;

} else if (mode == MeasureSpec.AT_MOST) {

//wrap_content

int aWidth = 0;

int bWidth = 0;

int cWidth = 0;

int dWidth = 0;

for (int i = 0; i < this.getChildCount(); i++) {

if (i == 0)

aWidth = getChildAt(i).getMeasuredWidth();

else if (i == 1)

bWidth = getChildAt(i).getMeasuredWidth();

else if (i == 2)

cWidth = getChildAt(i).getMeasuredWidth();

else if (i == 3)

dWidth = getChildAt(i).getMeasuredWidth();

}

width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth);

}

return width;

}

/**

* 測量容器的高度

*

* @param heightMeasureSpec

* @return

*/

private int measureHeight(int heightMeasureSpec) {

int mode = MeasureSpec.getMode(heightMeasureSpec);

int size = MeasureSpec.getSize(heightMeasureSpec);

int height = 0;

if (mode == MeasureSpec.EXACTLY) {

//match_parent 或具體值

height = size;

} else if (mode == MeasureSpec.AT_MOST) {

//wrap_content

int aHeight = 0;

int bHeight = 0;

int cHeight = 0;

int dHeight = 0;

for (int i = 0; i < this.getChildCount(); i++) {

if (i == 0)

aHeight = getChildAt(i).getMeasuredHeight();

else if (i == 1)

bHeight = getChildAt(i).getMeasuredHeight();

else if (i == 2)

cHeight = getChildAt(i).getMeasuredHeight();

else if (i == 3)

dHeight = getChildAt(i).getMeasuredHeight();

}

height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight);

}

return height;

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

我猜測大家可能有一個疑問,在計算每個子組件的寬度和高度時,用了 4 個 if 語句進行判斷,如圖所示,這樣做的目的是為了在讀取子組件對象時防止因子組件數量不夠而出現下標越界,這個判斷是必須而且有用的。

接下來對該組件進行測試,看是否達到我們預期的要求。我們通過修改 layout_width 和

layout_height 兩個屬性值來測試不同的運行結果。第一個測試的運行結果如圖 所示,定義cornetlayout.xml 布局文件,內容如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:padding="10dp">

<bczm.com.day0617.CornerLayout

android:layout_width="wrap_content"

android:layout_height="wrap_content">

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_bright" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_dark" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_red_dark" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_green_light" />

</bczm.com.day0617.CornerLayout>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

如圖所示,將第一個子組件的寬度設置為 100dp,第四個子組件的高度設置為 100dp;

如圖所示的運行結果中,我們只定義了三個子組件,效果依舊完美。

最後一種情況,就是將 CornerLayout 的 layout_width 和 layout_height 屬性都設置為

match_parent,此時容器佔用了整個屏幕空間,4 個子組件向四周擴散,如圖 所示(橫屏顯示)。

內邊距 padding

我們為 CornerLayout 容器添加一個灰色的背景,因為沒有設置子組件與容器邊框的距離

(padding),所以子組件與容器邊框是重疊的,如圖所示。

如果考慮 padding 對容器帶來的影響,那事情就變得複雜些了。默認情況下,容器內的

padding 會自動留出來,但如果不改子組件位置會導致子組件不能完全顯示。另外,View 已經將padding 屬性定義好開發人員無須自行定義,並且定義了 4 個方法分別用於讀取 4 個方向的padding:

? public int getPaddingLeft() 離左邊的 padding

? public int getPaddingRight() 離右邊的 padding

? public int getPaddingTop() 離頂部的 padding

? public int getPaddingRight() 離底部的 padding

考慮 padding 屬性之後,將給容器的寬度和高度以及子組件的定位帶來影響,當 CornerLayout的 layout_width 為 wrap_content 時,其寬度 = A、C 的最大寬度 + B、D 的最大寬度 + 容器左邊的 padding + 容器右邊的 padding,當 layout_height 為 wrap_content 時,其高度 = A、B 的最大高度 + C、D 的最大高度 + 容器頂部的 padding + 容器底部的 padding。而在 onLayout()方法中定位子組件時,也同樣需要為 padding 留出空間。

總結一句話就是容器的寬度和高度都變大了。

在 measureWidth()方法中,計算容器寬度時,加上了左邊的 padding 和右邊的 padding:

private int measureWidth(int widthMeasureSpec){

……

width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth)

+ getPaddingLeft() + getPaddingRight();

……

return width;

}

1

2

3

4

5

6

7

在 measureHeight()方法中,計算容器高度時,加上了頂部的 padding 和底部的 padding:

private int measureHeight(int heightMeasureSpec){

……

height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight)

+ getPaddingTop() + getPaddingBottom();

……

}

1

2

3

4

5

6

onLayout()方法的改動則比較大,大家最好是根據自己的思路和理解自行實現,這樣才能真正消化。我們給大家提供的只是作為參考。

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int leftPadding = getPaddingLeft();

int rightPadding = getPaddingRight();

int topPadding = getPaddingTop();

int bottomPadding = getPaddingBottom();

for (int i = 0; i < getChildCount(); i++) {

View child = getChildAt(i);

if (i == 0) {

//定位到左上角

child.layout(leftPadding, topPadding,

child.getMeasuredWidth() + leftPadding,

child.getMeasuredHeight() + topPadding);

} else if (i == 1) {

//定位到右上角

child.layout(getMeasuredWidth() - child.getMeasuredWidth()

- rightPadding,

topPadding, getMeasuredWidth() - rightPadding,

child.getMeasuredHeight() + topPadding);

} else if (i == 2) {

//定位到左下角

child.layout(leftPadding,

getMeasuredHeight() - child.getMeasuredHeight()

- bottomPadding,

child.getMeasuredWidth() + leftPadding,

getMeasuredHeight() - bottomPadding);

} else if (i == 3) {

//定位到右下角

child.layout(getMeasuredWidth() - child.getMeasuredWidth()

- rightPadding,

getMeasuredHeight() - child.getMeasuredHeight()

- bottomPadding,

getMeasuredWidth() - rightPadding,

getMeasuredHeight() - bottomPadding);

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

在布局文件中,將 CornerLayout 的 paddingLeft、paddingTop、paddingRight 和 paddingBottom四個屬性分別設置為 10dp、20dp、30dp 和 40dp,運行結果如圖所示。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:padding="10dp">

<bczm.com.day0617.CornerLayout

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="#FFCCCCCC"

android:paddingBottom="40dp"

android:paddingLeft="10dp"

android:paddingRight="30dp"

android:paddingTop="20dp">

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_bright" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_dark" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_red_dark" />

<TextView

android:layout_width="150dp"

android:layout_height="150dp"

android:background="@android:color/holo_green_light" />

</bczm.com.day0617.CornerLayout>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

CornerLayout 並不具備實用價值,因為 FrameLayout 布局能輕易實現 CornerLayout 的功能,但是,對於理解布局容器的開發卻能提供一種非常清晰的方法和思路(這個才是最重要的,不是么?)。

外邊距 margin

熟悉 css 的朋友應該知道,當 div 定義了 maring:15px;的樣式時,div 各方向離相鄰元素的距離都將是 15 個像素(不考慮垂直合併的情況),其實,Android 組件的外邊距 margin 在理解的時候與 css 是基本相同的。我們修改 cornerlayout.xml 文件如下:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:padding="10dp">

<bczm.com.day0617.CornerLayout

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="#FFCCCCCC"

android:padding="10dp">

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:layout_margin="10dp"

android:background="@android:color/holo_blue_bright" />

</bczm.com.day0617.CornerLayout>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

xml 代碼中我們將第一個 TextView 的 layout_margin 屬性設置為 10dp(限於篇幅其他

TextView 省略了),我們希望第一個 TextView 離相鄰組件的距離為 10dp,但運行後卻發現沒有任何效果。原因是我們在 onLayout()方法中定位子組件時沒有考慮 margin 這個屬性。

如果要考慮 margin,則將影響以下幾個方面:

? 影響 onMeasure()方法測量的容器尺寸;

? 影響 onLayout()方法對子組件的定位;

? 必須為子組件提供默認的 MarginLayoutParams(或其子類)。

向容器添加子組件時,需要調用 addView()方法,該方法有幾個重載版本,如果調用 publicvoid addView(View child, LayoutParams params)方法,則必須手動指定 LayoutParams,LayoutParams中定義了兩個重要的屬性:width 和 height,對應了 xml 中的 layout_width 和 layout_height 屬性。

如果要讓組件支持 margin,則必須使用 MarginLayoutParams 類,該類是 LayoutParams 的子類,下面是 MarginLayoutParams 類的源碼片段:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {

public int leftMargin; //對應 layout_marginLeft 屬性

public int topMargin; //對應 layout_marginTop 屬性

public int rightMargin; //對應 layout_marginRight 屬性

public int bottomMargin; //對應 layout_marginBottom 屬性

}

1

2

3

4

5

6

然而,當我們調用 public void addView(View child)方法來添加子組件時,並不需要指定

LayoutParams,此時,ViewGroup 會調用其 generateDefaultLayoutParams()方法獲取默認的LayoutParams,對於支持子組件 margin 來說,這是必要的,addView()的源碼如下:

public void addView(View child, int index) {

LayoutParams params = child.getLayoutParams();

if (params == null) {

params = generateDefaultLayoutParams();

if (params == null) {

throw new llegalArgumentException("generateDefaultLayout-Params() cannot

return null");

}

}

addView(child, index, params);

}

1

2

3

4

5

6

7

8

9

10

11

自定義容器如果要支持 margin 特性,容器類必須重寫 generateDefault-LayoutParams()方法,返回 MarginLayoutParams 對象。另外,還需要重寫另外兩個方法:

? public LayoutParams generateLayoutParams(AttributeSet attrs)

創建 LayoutParams (或子類)對象,通過 attrs 可以讀取到布局文件中的自定義屬性值,該方法必須重寫;

? protected LayoutParams generateLayoutParams(LayoutParams p)

創建 LayoutParams(或子類)對象,可以重用參數 p,該方法建議重寫。

為了讓 CornerLayout 支持 margin 特徵,需要重寫 generateDefaultLayout-Params()和

generateLayoutParams()方法,代碼如下(為了不影響前面的案例,將類名改名為 CornerLayout2):

public class CornerLayout2 extends ViewGroup {

……

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

return new MarginLayoutParams(this.getContext(), attrs);

}

@Override

protected LayoutParams generateLayoutParams(LayoutParams p) {

return new MarginLayoutParams(p);

}

@Override

protected LayoutParams generateDefaultLayoutParams() {

return new MarginLayoutParams(LayoutParams.WRAP_CONTENT,

LayoutParams.WRAP_CONTENT);

}

……

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

測量容器大小時,在 layout_width 和 layout_height 皆為 wrap_content 的情況下(其他情況無需過多考慮),容器的寬度和高度分別為:

寬度 = A、C 的最大寬度 + B、D 的最大寬度 + 容器左邊的 padding + 容器右邊的 padding+ A、C 左、右的最大 margin + B、D 左、右的最大 margin;

高度 = A、B 的最大高度 + C、D 的最大高度 + 容器頂部的 padding + 容器底部的 padding +A、B 頂、底部的最大 margin + C、D 頂、底部的最大 margin。

修改後的 onMeasure()方法實現如下:

···

/**

* 測量尺寸

* @param widthMeasureSpec

* @param heightMeasureSpec

*/

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//先測量所有子組件的大小

this.measureChildren(widthMeasureSpec, heightMeasureSpec);

//再測量自己的大小

int width = this.measureWidth(widthMeasureSpec);

int height = this.measureHeight(heightMeasureSpec);

//應用

this.setMeasuredDimension(width, height);

}

/**

* 測量容器的寬度

* @param widthMeasureSpec

* @return

*/

private int measureWidth(int widthMeasureSpec){

int mode = MeasureSpec.getMode(widthMeasureSpec);

int size = MeasureSpec.getSize(widthMeasureSpec);

int width = 0;

if(mode == MeasureSpec.EXACTLY) {

//match_parent 或具體值

width = size;

}else if(mode == MeasureSpec.AT_MOST){

//wrap_content

int aWidth, bWidth, cWidth, dWidth;

aWidth = bWidth = cWidth = dWidth = 0;

int marginHa, marginHb, marginHc, marginHd;

marginHa = marginHb = marginHc = marginHd = 0;

for(int i = 0; i < this.getChildCount(); i ++){

MarginLayoutParams layoutParams = (MarginLayoutParams)

getChildAt(i).getLayoutParams();

if(i == 0) {

aWidth = getChildAt(i).getMeasuredWidth();

marginHa += layoutParams.leftMargin + layoutParams.rightMargin;

}else if(i == 1) {

bWidth = getChildAt(i).getMeasuredWidth();

marginHb += layoutParams.leftMargin + layoutParams.rightMargin;

}else if(i == 2) {

cWidth = getChildAt(i).getMeasuredWidth();

marginHc += layoutParams.leftMargin + layoutParams.rightMargin;

}else if(i == 3) {

dWidth = getChildAt(i).getMeasuredWidth();

marginHd += layoutParams.leftMargin + layoutParams.rightMargin;

}

}

width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth)

+ getPaddingLeft() + getPaddingRight()

+ Math.max(marginHa, marginHc)

+ Math.max(marginHb, marginHd);

}

return width;

}

/**

* 測量容器的高度

* @param heightMeasureSpec

* @return

*/

private int measureHeight(int heightMeasureSpec){

int mode = MeasureSpec.getMode(heightMeasureSpec);

int size = MeasureSpec.getSize(heightMeasureSpec);

int height = 0;

if(mode == MeasureSpec.EXACTLY) {

//match_parent 或具體值

height = size;

}else if(mode == MeasureSpec.AT_MOST){

//wrap_content

int aHeight, bHeight, cHeight, dHeight;

aHeight = bHeight = cHeight = dHeight = 0;

int marginVa, marginVb, marginVc, marginVd;

marginVa = marginVb = marginVc = marginVd = 0;

for(int i = 0; i < this.getChildCount(); i ++){

MarginLayoutParams layoutParams = (MarginLayoutParams)

getChildAt(i).getLayoutParams();

if(i == 0) {

aHeight = getChildAt(i).getMeasuredHeight();

marginVa += layoutParams.topMargin +

layoutParams.bottomMargin;

}else if(i == 1) {

bHeight = getChildAt(i).getMeasuredHeight();

marginVb += layoutParams.topMargin +

layoutParams.bottomMargin;

}else if(i == 2) {

cHeight = getChildAt(i).getMeasuredHeight();

marginVc += layoutParams.topMargin +

layoutParams.bottomMargin;k

}else if(i == 3) {

dHeight = getChildAt(i).getMeasuredHeight();

marginVd += layoutParams.topMargin +

layoutParams.bottomMargin;

}

}

height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight)

+ getPaddingTop() + getPaddingBottom()

+ Math.max(marginVa, marginVb) + Math.max(marginVc, marginVd);

}

return height;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

···

onLayout()方法定位子組件時也將更加複雜,考慮的因素包括子組件的尺寸、padding 和

margin,需要為 padding 和 margin 留出對應的空白空間。

/**

* 定位子組件

*/

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int leftPadding = getPaddingLeft();

int rightPadding = getPaddingRight();

int topPadding = getPaddingTop();

int bottomPadding = getPaddingBottom();

for (int i = 0; i < getChildCount(); i++) {

View child = getChildAt(i);

MarginLayoutParams layoutParams = (MarginLayoutParams)

child.getLayoutParams();

int leftMargin = layoutParams.leftMargin;

int rightMargin = layoutParams.rightMargin;

int topMargin = layoutParams.topMargin;

int bottomMargin = layoutParams.bottomMargin;

Log.i(TAG, "leftMargin:" + leftMargin + " rightMargin:"

+ rightMargin + " topMargin:" + topMargin

+ " bottomMargin:" + bottomMargin);

if (i == 0) {

//定位到左上角

child.layout(leftPadding + leftMargin,

topPadding + topMargin,

child.getMeasuredWidth() + leftPadding + leftMargin,

child.getMeasuredHeight() + topPadding + topMargin);

} else if (i == 1) {

//定位到右上角

child.layout(getMeasuredWidth() - child.getMeasuredWidth()

- rightPadding - rightMargin,

topPadding + rightMargin,

getMeasuredWidth() - rightPadding - rightMargin,

child.getMeasuredHeight() + topPadding + rightMargin);

} else if (i == 2) {

//定位到左下角

child.layout(leftPadding + leftMargin,

getMeasuredHeight() - child.getMeasuredHeight()

- bottomPadding - bottomMargin,

child.getMeasuredWidth() + leftPadding + leftMargin,

getMeasuredHeight() - bottomPadding - bottomMargin);

} else if (i == 3) {

//定位到右下角

child.layout(getMeasuredWidth() - child.getMeasuredWidth()

- rightPadding - rightMargin,

getMeasuredHeight() - child.getMeasuredHeight()

- bottomPadding - bottomMargin,

getMeasuredWidth() - rightPadding - rightMargin,

getMeasuredHeight() - bottomPadding bottomMargin);

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

最後,修改 cornerlayout.xml 布局文件,為第一個和第三個子組件指定 layout_margin 的值為10dp,運行效果如圖所示。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:padding="10dp"

android:orientation="vertical" android:layout_width="match_parent"

android:layout_height="match_parent">

<bczm.com.day0617.CornerLayout2

android:background="#FFCCCCCC"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

>

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:layout_margin="10dp"

android:background="@android:color/holo_blue_bright"/>

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_dark"/>

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:layout_margin="10dp"

android:background="@android:color/holo_red_dark"/>

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_green_light"/>

</bczm.com.day0617.CornerLayout2>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

如果在<bczm.com.day0617.CornerLayout2>標記上添加 android: padding=」10dp」屬性,結果如圖所示。

支持 margin 的 CornerLayout2 類的結構如圖所示。

自定義 LayoutParams

我們前面接觸過 LayoutParams 和 MarginLayoutParams 等布局參數類,這兩個類都是

ViewGroup 的靜態內部類。這也為我們自定義 LayoutParams 提供了參考依據,各位可以去閱讀這兩個類的源碼以便有更多的了解。

到目前為止,CornerLayout 還不支持顯示方位,這也是唯一尚未實現的需求。本節我們將一起來實現這個功能。方位包含 4 個方向:左上角、右上角、左下角、右下角,在 attrs.xml 文件中,定義一個名為layout_position 的屬性,類型為 enum,枚舉出這 4 個值。

<?xml version="1.0" encoding="utf-8"?>

<resources>

<declare-styleable name="CornerLayout3">

<attr name="layout_position" format="enum">

<enum name="left_top" value="0"/>

<enum name="right_top" value="1"/>

<enum name="left_bottom" value="2"/>

<enum name="right_bottom" value="3"/>

</attr>

</declare-styleable>

</resources>

1

2

3

4

5

6

7

8

9

10

11

12

如果要從容器中讀取子組件的自定義屬性,需要使用布局參數,比如有如下的配置:

<bczm.com.day0617.CornerLayout3

android:layout_width="wrap_content"

android:layout_height="wrap_content" >

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

trkj:layout_position="right_bottom"/>

</bczm.com.day0617.CornerLayout3>

1

2

3

4

5

6

7

8

如果我們想在 CornerLayout3 容器類中讀取 TextView 的 trkj:layout_position 屬性值,使用布局參數(LayoutParams)是一個很好的解決辦法。將 CornerLayout2 複製一個新類,取名為CornerLayout3,考慮到要支持 margin 特徵,在該類中定義一個繼承自 MarginLayoutParams的子類 PositionLayoutParams,按照慣例,PositionLayoutParams 類為 CornerLayout3 的靜態內部類。

public class CornerLayout3 {

……

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

return new PositionLayoutParams(this.getContext(), attrs);

}

@Override

protected LayoutParams generateLayoutParams(LayoutParams p) {

return new PositionLayoutParams(p);

}

@Override

protected LayoutParams generateDefaultLayoutParams() {

return new PositionLayoutParams(LayoutParams.WRAP_CONTENT,

LayoutParams.WRAP_CONTENT);

}

/**

* 自定義 LayoutParams

*/

public static class PositionLayoutParams extends ViewGroup.MarginLayoutParams{

public static final int LEFT_TOP = 0;

public static final int RIGHT_TOP = 1;

public static final int LEFT_BOTTOM = 2;

public static final int RIGHT_BOTTOM = 3;

public static final int NONE = -1;

public int position;

public PositionLayoutParams(Context c, AttributeSet attrs) {

super(c, attrs);

//讀取 layout_position 屬性

TypedArray a = c.obtainStyledAttributes(

attrs, R.styleable.CornerLayout3);

position = a.getInt(R.styleable.CornerLayout3_layout_position, NONE);

a.recycle();

}

public PositionLayoutParams(int width, int height) {

super(width, height);

}

public PositionLayoutParams(MarginLayoutParams source) {

super(source);

}

public PositionLayoutParams(LayoutParams source) {

super(source);

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

上述代碼中,根據父類的要求定義了 4 個構造方法,其中構造方法 publicPositionLayoutParams(Context c, AttributeSet attrs)讀取了 layout_position 屬性值,保存在 position成員變數中,如果未讀取到該屬性,則默認值為 NONE。其次定義了 4 個常量與 layout_position屬性的 4 個枚舉值相對應。

ViewGroup 類重寫的 generateLayoutParams()和 generateDefaultLayoutParams()方法返回的LayoutParams 為 PositionLayoutParams 對 象 。 其中public LayoutParams generateLayout-Params(AttributeSet attrs)方法將attrs傳入public PositionLayoutParams(Context c, AttributeSet attrs)構造方法,所以PositionLayoutParams 才能讀取到 layout_position 的屬性值。

在 onLayout()方法中,我們需要根據當前子組件的 PositionLayoutParams 的 position 屬性來確定方位,這裡有兩種情況:一種是沒有為組件定義方位時,依舊按照從左往右、從上往下的方式進行放置;另一種是如果組件定義了特定方位,如 right_bottom,則將該組件顯示在容器的右下角。

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int leftPadding = getPaddingLeft();

int rightPadding = getPaddingRight();

int topPadding = getPaddingTop();

int bottomPadding = getPaddingBottom();

for(int i = 0; i < getChildCount(); i ++){

View child = getChildAt(i);

PositionLayoutParams layoutParams = (PositionLayoutParams)

child.getLayoutParams();

int leftMargin = layoutParams.leftMargin;

int rightMargin = layoutParams.rightMargin;

int topMargin = layoutParams.topMargin;

int bottomMargin = layoutParams.bottomMargin;

int position = layoutParams.position;

if(i == 0 && position == PositionLayoutParams.NONE

|| position == PositionLayoutParams.LEFT_TOP){

//定位到左上角,代碼不變,略

}else if(i == 1 && position == PositionLayoutParams.NONE

|| layoutParams.position ==

PositionLayoutParams.RIGHT_TOP){

//定位到右上角,代碼不變,略

}else if(i == 2 && position == PositionLayoutParams.NONE

|| layoutParams.position ==

PositionLayoutParams.LEFT_BOTTOM){

//定位到左下角,代碼不變,略

}else if(i == 3 && position == PositionLayoutParams.NONE

|| layoutParams.position ==

PositionLayoutParams.RIGHT_BOTTOM){

//定位到右下角,代碼不變,略

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

為了更加清晰地看明白 CornerLayout3 容器內子組件的位置,我們為子組件 TextView 分別添加了 A、B、C、D 四個字元作為 text 屬性的值,在沒有為子組件指定方位的情況下,創建cornerlayout2.xml 布局文件,內容如下:

<?xml version="1.0" encoding="utf-8"?><!--

~ Copyright (c) 2016.

~ Android 自定義組件開發詳解 株洲新程 IT 教育 李贊紅

~ 版權所有 嚴禁用於商業用途

-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:trkj="http://schemas.android.com/apk/res-auto"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:padding="10dp">

<bczm.com.day0617.CornerLayout3

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="#FFCCCCCC"

android:padding="10dp">

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:layout_margin="10dp"

android:background="@android:color/holo_blue_bright"

android:gravity="center"

android:text="A"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_dark"

android:gravity="center"

android:text="B"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:layout_margin="10dp"

android:background="@android:color/holo_red_dark"

android:gravity="center"

android:text="C"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_green_light"

android:gravity="center"

android:text="D"

android:textColor="#FFFFFFFF" />

</bczm.com.day0617.CornerLayout3>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

運行效果如圖所示。

接下來,我們為每個子組件都指定一個不同的方位(方位相同會重疊),修改如下:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:trkj="http://schemas.android.com/apk/res-auto"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:padding="10dp">

<bczm.com.day0617.CornerLayout3

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="#FFCCCCCC"

android:padding="10dp">

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:layout_margin="10dp"

android:background="@android:color/holo_blue_bright"

android:gravity="center"

android:text="A"

android:textColor="#FFFFFFFF"

trkj:layout_position="right_bottom" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_dark"

android:gravity="center"

android:text="B"

android:textColor="#FFFFFFFF"

trkj:layout_position="left_bottom" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:layout_margin="10dp"

android:background="@android:color/holo_red_dark"

android:gravity="center"

android:text="C"

android:textColor="#FFFFFFFF"

trkj:layout_position="right_top" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_green_light"

android:gravity="center"

android:text="D"

android:textColor="#FFFFFFFF"

trkj:layout_position="left_top" />

</bczm.com.day0617.CornerLayout3>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

果然不出所料,組件的位置變了,效果如圖所示。CornerLayout3 中的

PositionLayoutParams 自定義布局參數類只有一個屬性,但足以說明問題,希望大家觸類旁通,深刻理解容器通過 LayoutParams 讀取子組件信息而影響容器外觀與行為的意義。總之一句話,容器類能讀取子組件的 LayoutParams。

因為 LayoutParams 類一般都定義為容器類的靜態內部類,所以在命名時往往都統一使用

「LayoutParams」作為布局參數類的名稱,不同的 LayoutParams 通過容器類來區分,比如:FrameLayout. LayoutParams、LinearLayout. LayoutParams 等等,嗯,這只是一個官方的做法而已,大家知道就好!

案例 :流式布局(FlowLayout)

在 Java Swing 中,有一種布局,叫流式布局(FlowLayout),這種布局的特點是子組件按照從左往右、從上往下的順序依次排序,如果一行放不下,自動顯示到下一行,和 HTML 中的float 效果類似,但在,Android 中沒有提供這樣的布局,本節,我們將一起來實現這種布局。

對於 FlowLayout 來說,難點有二:一是要事先預測組件的寬度和高度,這個和 CornerLayout有明顯的不同,FlowLayout 中的組件數是不固定的,而 CornerLayout 中最多只支持 4 個子組件,前者的難度係數更大,也需要更靈活的處理;二是對子組件進行定位時,也是個頭痛的問題,子組件的大小不一,數量多少不一,每一個組件放在哪一行、放在一行中的什麼位置都需要計算,最重要的是要找到規律,不可能一個一個去處理。

測量 FlowLayout 容器的寬度時,不允許子組件的寬度比容器的寬度還大,這是前提。當子組件個數很少總寬度比容器的 layout_width 為 match_parent 時的寬度小,那麼容器的

layout_width 為 wrap_content 時就是子組件的寬度之和。但是如果子組件個數很多,總寬度超出容器的最大寬度,則就算容器的 layout_width 為 wrap_content 最終測量寬度也要採用match_parent 值,並且需要另起一行繼續顯示上一行餘下的子組件。

//遍歷所有的子組件,計算出子組件的總寬度

for(int i = 0; i < n; i ++){

View child = getChildAt(i);

int childWidth = child.getMeasuredWidth();

//單個子組件的寬度不能超過容器寬度

if(childWidth > size){

throw new IllegalStateException("Sub view is too large.");

}

childrenWidth += childWidth;

}

if(childrenWidth > size){

//如果子組件的寬度之和大於容器的最大寬度,則使用容器寬度

width = size;

}else{

//如果子組件的寬度之和小於容器的最大寬度,

//容器寬度跟隨子組件寬度

width = childrenWidth;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

FlowLayout 容器高度是每一行最高的組件的高度之和。因為測量時並不需要顯示子組件,所以我們採用預測的方法判斷是否需要換行,換行後計算出當前行最高的組件高度並進行累加,最後算出所有行的最高高度之和。

for(int i = 0; i < n; i ++){

……

//預測是否需要換行,getChildAt(i + 1)是將下一個子組件事先考慮進來

//但不影響子組件的循環遍歷

if(i < n - 1 && maxLineWidth + getChildAt(i + 1).getMeasuredWidth()

> width - getPaddingLeft() - getPaddingRight()){

//當前行的子組件寬度如果超出容器的寬度,則要換行

height += maxViewHeight;

maxLineWidth = 0;

maxViewHeight = 0;

}else if(i == n - 1){

//已經遍歷到最後一個

height += maxViewHeight;

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

重寫 onLayout()方法定位子組件時,是一個邏輯性比較強的工作。從第 0 個子組件開始,一個個進行定位,如果當前行的已佔寬度加上當前子組件的寬度大於容器的寬度,則要換行,換行其實就是將當前子組件的寬度重新設置為 0、高度就是前面所有行的高度之和。每成功定位一個組件,都要算出當前行的最高高度並累計當前行的已佔寬度。

需要展開想像,並親身實踐才能感同身受,對於複雜的邏輯,講解總是蒼白無力。下面是 FlowLayout 類的源碼。

public class FlowLayout extends ViewGroup {

private static final String TAG = "FlowLayout";

public FlowLayout(Context context) {

super(context);

}

public FlowLayout(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int n = getChildCount();

int maxViewHeight = 0; //當前行的子組件的最大高度

int maxLineWidth = 0; //當前行的子組件的總寬度

int totalHeight = 0; //累計高度

int width = getMeasuredWidth(); //容器寬度

for (int i = 0; i < n; i++) {

View child = getChildAt(i);

//判斷是否要換行顯示(已佔寬度+當前子組件的寬度是否大於容器的寬度)

if (maxLineWidth + getChildAt(i).getMeasuredWidth()

> width - getPaddingLeft() - getPaddingRight()) {

//換行後累計已顯示的行的總高度

totalHeight += maxViewHeight;

Log.i(TAG, "totalHeight:" + totalHeight + " maxLineWidth:" +

maxLineWidth + " width:" + width);

//新起一行,新行的已佔寬度和高度重置為 0

maxLineWidth = 0;

maxViewHeight = 0;

}

layoutChild(child, maxLineWidth, totalHeight,

maxLineWidth + child.getMeasuredWidth(),

totalHeight + child.getMeasuredHeight());

//獲取當前行的最高高度

maxViewHeight = Math.max(maxViewHeight,

child.getMeasuredHeight());

//累加當前行的寬度

maxLineWidth += child.getMeasuredWidth();

}

}

/**

* 定位子組件,方法內考慮 padding

* Android 自定義組件開發詳解 株洲新程 IT 教育 李贊紅

* 郵箱:lifenote@21cn.com 歡迎捐助,支持我分享更多技術! - 250 -

*

* @param child

* @param l

* @param t

* @param r

* @param b

*/

private void layoutChild(View child, int l, int t, int r, int b) {

Log.i(TAG, child.getTag() + ":" + " Left:" + l + " Top:"

+ t + " Right:" + r + " Bottom:" + b);

//所有子組件要統一向右和向下平移指定的 padding

child.layout(l + getPaddingLeft(), t + getPaddingTop(),

r + getPaddingLeft(), b + getPaddingTop());

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

measureChildren(widthMeasureSpec, heightMeasureSpec);

int width = measureWidth(widthMeasureSpec);

int height = measureHeight(heightMeasureSpec);

setMeasuredDimension(width, height);

}

/**

* 測量容器寬度

*

* @param widthMeasureSpec

* @return

*/

private int measureWidth(int widthMeasureSpec) {

int mode = MeasureSpec.getMode(widthMeasureSpec);

int size = MeasureSpec.getSize(widthMeasureSpec);

int width = 0;

if (mode == MeasureSpec.EXACTLY) {

width = size;

} else if (mode == MeasureSpec.AT_MOST) {

//計算出所有子組件占的總寬度

int n = getChildCount();

int childrenWidth = 0;

for (int i = 0; i < n; i++) {

View child = getChildAt(i);

int childWidth = child.getMeasuredWidth();

//單個子組件的寬度不能超過容器寬度

if (childWidth > size) {

throw new IllegalStateException("Sub view is too large.");

}

childrenWidth += childWidth;

}

Log.i(TAG, "size:" + size + " viewsWidth:" + childrenWidth);

//在 wrap_content 的情況下,如果子組件占的總寬度>容器的

//最大寬度,則應該取容器最大寬度

if (childrenWidth > size) {

width = size;

} else {

width = childrenWidth;

}

//padding

width += this.getPaddingLeft() + getPaddingRight();

}

return width;

}

/**

* 測量容器高度

*

* @param heightMeasureSpec

* @return

*/

private int measureHeight(int heightMeasureSpec) {

int mode = MeasureSpec.getMode(heightMeasureSpec);

int size = MeasureSpec.getSize(heightMeasureSpec);

int height = 0;

if (mode == MeasureSpec.EXACTLY) {

height = size;

} else if (mode == MeasureSpec.AT_MOST) {

//wrap_content 容器高度跟隨內容

int width = getMeasuredWidth();

int n = getChildCount();

int maxViewHeight = 0;//當前行的子組件的最大高度

int maxLineWidth = 0;//當前行的子組件的總寬度

for (int i = 0; i < n; i++) {

View child = getChildAt(i);

maxLineWidth += child.getMeasuredWidth();

maxViewHeight =

Math.max(child.getMeasuredHeight(), maxViewHeight);

//預測是否需要換行

if (i < n - 1 && maxLineWidth

+ getChildAt(i + 1).getMeasuredWidth()

> width - getPaddingLeft() - getPaddingRight()) {

//當前行的子組件寬度如果超出容器的寬度,則要換行

height += maxViewHeight;

maxLineWidth = 0;

maxViewHeight = 0;

} else if (i == n - 1) {

//已經遍歷到最後一個

height += maxViewHeight;

}

}

}

//padding

height += getPaddingTop() + getPaddingBottom();

return height;

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

為了觀察 FlowLayout 的最終效果,定義 flowlayout.xml 布局文件,在該布局文件中定義了 9個大小不同的 TextView,每個子組件都定義了 tag 屬性,用於輸出 log 日誌。xml 如下:

<?xml version="1.0" encoding="utf-8"?>

<!--

~ Copyright (c) 2016.

~ Android 自定義組件開發詳解 株洲新程 IT 教育 李贊紅

~ 版權所有 嚴禁用於商業用途

-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:padding="10dp">

<bczm.com.day0617.FlowLayout

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="#99CCCCCC"

android:padding="10dp">

<TextView

android:layout_width="200dp"

android:layout_height="200dp"

android:background="@android:color/holo_blue_bright"

android:gravity="center"

android:tag="0"

android:text="0"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_dark"

android:gravity="center"

android:tag="1"

android:text="1"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="50dp"

android:background="@android:color/holo_green_light"

android:gravity="center"

android:tag="2"

android:text="2"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="50dp"

android:layout_height="50dp"

android:background="@android:color/background_dark"

android:gravity="center"

android:tag="3"

android:text="3"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/holo_blue_dark"

android:gravity="center"

android:tag="4"

android:text="4"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="50dp"

android:background="@android:color/holo_green_light"

android:gravity="center"

android:tag="5"

android:text="5"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="100dp"

android:background="@android:color/darker_gray"

android:gravity="center"

android:tag="6"

android:text="6"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="100dp"

android:layout_height="50dp"

android:background="@android:color/holo_green_light"

android:gravity="center"

android:tag="7"

android:text="7"

android:textColor="#FFFFFFFF" />

<TextView

android:layout_width="120dp"

android:layout_height="50dp"

android:background="@android:color/darker_gray"

android:gravity="center"

android:tag="8"

android:text="8"

android:textColor="#FFFFFFFF" />

</bczm.com.day0617.FlowLayout>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

自定義組件開發七 自定義容器

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

Eclipse編輯Spring配置文件xml時自動提示類class包名
Linux命令備忘錄:mount用於載入文件系統到指定的載入點

TAG:程序員小新人學習 |