其實自定義ViewGroup與自定義View很像。它們本質都是View,差別在于ViewGroup是用來組織顯示View的。自定義ViewGroup也有幾個關鍵的方法需要實作,而且onLayout方法是必須實作的。在自定義ViewGroup中我們常常需要重寫onMeasure、onLayout,而onDraw一般不需要重寫。
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
這個方法是用來測試量尺寸的,這兩個參數是父布局傳遞過來的。如果你自定義的ViewGroup已經是根布局,但是它一樣有父布局,因為android系統最後都會将其被添加到一個FrameLayout布局裡。這個方法不僅要測量ViewGroup的尺寸,那還必須測量ViewGroup内每個視圖。測試子視圖把這兩個參數傳遞過去,剩下的就是子視圖自己的onMeasure方法來測量出它自己的尺寸。這方法的參數各自都包含了測量模式和具體尺寸大小。我們根據不同的測量模式來決定其具體的尺寸。具體的測量模式兩三種:
- UNSPECIFIED:父布局沒有任何限制(在自定義View或ViewGroup中都不常用這個)
- EXACTLY:父布局已經确定了确切的尺寸
- AT_MOST:可以任意大小,直到指定确切的尺寸
我們隻關注後面兩種。這個模式的標明是根據xml布局檔案中android:layout_width和android:layout_height的取值情況來決定的:
-
EXACTLY模式:
(1)match_parent:占滿整個父布局的寬或高
(2)具體的數值:如300dp
-
AT_MOST模式:
(1)wrap_content:期望内容多大,寬和高相應多大,反正是能夠包裹住内容。這個模式給過來時,大小是不會像如期望一樣大小,它就是整個父布局的大小,因為父布局沒有辦法知道你的内容有多大,你必須在onMeasure方法親自确定其大小。
從兩個參數取出各自的測量模式與尺寸:
int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec);
對于EXACTLY模式,ViewGroup的尺寸直接用分離出來的尺寸。而對于AT_MOST模式的,分離出來的尺寸不能直接使用,因為它是傳過來的尺寸是父布局的尺寸,明顯不符合我們的要求。我們要根據需要,計算出ViewGroup中的子視圖尺寸并累加起來作為最終的尺寸,在這種模式下,還要考慮各個視圖之間的外邊距margin(在自定義View,則隻需要考慮内邊距padding)。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec);
int rowWidth = 0;// 臨時記錄行寬
int rowHeight = 0;// 臨時記錄行高
int maxWith = 0;
int maxHeight = 0;
measureChildren(widthMeasureSpec, heightMeasureSpec);// 測量children的大小
int count = getChildCount();
if (count != 0) {
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin;
int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
if (childWidth + rowWidth > measuredWidthSize - getPaddingLeft() - getPaddingRight()) {
// 換行
maxWith = Math.max(maxWith, rowWidth);
rowWidth = childWidth;
maxHeight += rowHeight;
rowHeight = childHeight;
} else {
// 不換行
rowWidth += childWidth;
rowHeight = Math.max(childHeight, rowHeight);
}
//最後一個控件
if (i == count - 1) {
maxWith = Math.max(maxWith, rowWidth);
maxHeight += rowHeight;
}
}
}
String widthModeStr = null;
switch (measuredWidthMode) {
case MeasureSpec.AT_MOST:
widthModeStr = "AT_MOST";
if (count == 0) {
mWidth = 0;
} else {
mWidth = maxWith + getPaddingLeft() + getPaddingRight();
}
break;
case MeasureSpec.EXACTLY:
widthModeStr = "EXACTLY";
mWidth = getPaddingLeft() + getPaddingRight() + measuredWidthSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredWidthMode);
case MeasureSpec.UNSPECIFIED:
break;
}
String heightModeStr = null;
switch (measuredHeightMode) {
case MeasureSpec.AT_MOST:
heightModeStr = "AT_MOST";
if (count == 0) {
mHeight = 0;
} else {
mHeight = maxHeight + getPaddingTop() + getPaddingBottom();
}
break;
case MeasureSpec.EXACTLY:
heightModeStr = "EXACTLY";
mHeight = getPaddingTop() + getPaddingBottom() + measuredHeightSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredHeightMode);
case MeasureSpec.UNSPECIFIED:
break;
}
setMeasuredDimension(mWidth, mHeight);
String str = "@widthMode#" + widthModeStr + ":" + measuredWidthSize + "@heightMode#" + heightModeStr + ":" + measuredHeightSize;
Log.d("Layout尺寸", str);
}
因為我們在處理AT_MOST模式時,必須獲得每個子視圖的外邊距資訊:
MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
是以必須加入以下代碼,否則會報錯:
// 自定義ViewGroup必須要有以下這個方法,否則拿不到child的margin的資訊
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
onLayout(boolean changed, int l, int t, int r, int b)
在自定義View中一般不需要重寫這個方法,但是在自定義ViewGroup中必須重寫這個方法,因為ViewGroup是View的集合,必須處理它們的位置關系。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
Log.i("Child大小W", child.getMeasuredWidth() + "#H:" + child.getMeasuredHeight());
ChildPosition pos = mChildPos.get(i);
//設定View的左邊、上邊、右邊底邊位置
child.layout(pos.left, pos.top, pos.right, pos.bottom);
}
}
}
小結
是以自定義ViewGroup還是相當簡單的。因為我們隻需要量好ViewGroup的視圖,在測量過程中,比較值得注意的就是當模式是AT_MOST時,需要計算各個子View的尺寸再累加起來,得到就是ViewGroup的尺寸。onMeasure的參數都是父布局傳給子控件的,這就是為什麼在自定義ViewGroup中,測量子View時的這兩個參數是來自定義的ViewGroup。最後就是就是在onLayout中布局子View的位置。
流式布局
我們繼承ViewGroup定義一個流式布局CustomFlowLayout。在流式布局中,我額外增加了滾動的功能,因為當我們的内容超過我們自定義ViewGroup的可視範圍後,就需要用到滾動功能。滾動功能的實作思路:
- 首先重寫onTouchEvent方法,消費滑動事件,根據我們的手指滑動的方向,來移動我們的視圖。
- 添加上邊界的檢查,一旦到達上邊界就不允許再滾動
- 添加下邊界的檢查,一旦到達下邊界就不允許再滾動
- android的事件傳遞方面,Activity->View group ->View,這可以了事件傳遞的順序。
- android的事件傳遞機制,要了解好dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent這個方法的工作過程,明白誰消費了事件,誰傳遞了事件。
- 了解好scrollTo與scrollBy的差別,前者是絕對位置移動,後者是相對位置移動。
- 要了解getY與getRawY與getScrollY它們的坐标系的特點。
- 最後一點,要知道android螢幕的坐标系是無限大,布局視圖并沒有邊界,我們的螢幕隻是這個坐标系的一部分,顯示出來的視圖隻是剛好在這一部分而已。、
- 我們滾動視圖,實質是改變了視圖整體的位置而已。
package com.wong.customtextview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
import java.util.ArrayList;
import java.util.List;
public class CustomFlowLayout extends ViewGroup {
private int mWidth = 0;
private int mHeight = 0;
private int realHeight;
private boolean scrollable = false;
private boolean isInterceptedTouch;
/**
* 判定為拖動的最小移動像素數
*/
private int mTouchSlop;
private int topBorder; // 上邊界
private int bottomBorder;// 下邊界
//記錄每個View的位置
private List<ChildPosition> mChildPos = new ArrayList<ChildPosition>();
private static class ChildPosition {
int left, top, right, bottom;
public ChildPosition(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
}
public CustomFlowLayout(Context context) {
this(context, null);
}
public CustomFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ViewConfiguration configuration = ViewConfiguration.get(context);
// 擷取TouchSlop值,用于判斷目前使用者的操作是否是拖動
mTouchSlop = configuration.getScaledPagingTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec);
int rowWidth = 0;// 臨時記錄行寬
int rowHeight = 0;// 臨時記錄行高
int maxWith = 0;
int maxHeight = 0;
measureChildren(widthMeasureSpec, heightMeasureSpec);// 測量children的大小
int count = getChildCount();
if (count != 0) {
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin;
int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
if (childWidth + rowWidth > measuredWidthSize - getPaddingLeft() - getPaddingRight()) {
// 換行
maxWith = Math.max(maxWith, rowWidth);
rowWidth = childWidth;
maxHeight += rowHeight;
rowHeight = childHeight;
} else {
rowWidth += childWidth;
rowHeight = Math.max(childHeight, rowHeight);
}
//最後一個控件
if (i == count - 1) {
maxWith = Math.max(maxWith, rowWidth);
maxHeight += rowHeight;
}
}
}
String widthModeStr = null;
switch (measuredWidthMode) {
case MeasureSpec.AT_MOST:
widthModeStr = "AT_MOST";
if (count == 0) {
mWidth = 0;
} else {
mWidth = maxWith + getPaddingLeft() + getPaddingRight();
}
break;
case MeasureSpec.EXACTLY:
widthModeStr = "EXACTLY";
mWidth = getPaddingLeft() + getPaddingRight() + measuredWidthSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredWidthMode);
case MeasureSpec.UNSPECIFIED:
break;
}
String heightModeStr = null;
switch (measuredHeightMode) {
case MeasureSpec.AT_MOST:
heightModeStr = "AT_MOST";
if (count == 0) {
mHeight = 0;
} else {
mHeight = maxHeight + getPaddingTop() + getPaddingBottom();
}
break;
case MeasureSpec.EXACTLY:
heightModeStr = "EXACTLY";
mHeight = getPaddingTop() + getPaddingBottom() + measuredHeightSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredHeightMode);
case MeasureSpec.UNSPECIFIED:
break;
}
//真實高度
realHeight = maxHeight + getPaddingTop() + getPaddingBottom();
//測量高度
if (measuredHeightMode == MeasureSpec.EXACTLY) {
scrollable = realHeight > mHeight;
} else {
scrollable = realHeight > measuredHeightSize;
}
if (scrollable) {
// 初始化上下邊界值
MarginLayoutParams lp1 = (MarginLayoutParams) getChildAt(0).getLayoutParams();
topBorder = getChildAt(0).getTop() - lp1.topMargin;
if (measuredHeightMode == MeasureSpec.EXACTLY) {
bottomBorder = realHeight - mHeight + getPaddingBottom();
} else {
bottomBorder = realHeight - measuredHeightSize + getPaddingBottom();
}
}
setMeasuredDimension(mWidth, mHeight);
String str = "@widthMode#" + widthModeStr + ":" + measuredWidthSize + "@heightMode#" + heightModeStr + ":" + measuredHeightSize;
Log.d("Layout尺寸", str);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mChildPos.clear();
int rowWidth = 0;// 臨時記錄行寬
int rowHeight = 0;// 臨時記錄行高
int maxWith = 0;
int maxHeight = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin;
int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
if (childWidth + rowWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
// 換行
maxWith = Math.max(maxWith, rowWidth);
rowWidth = childWidth;
maxHeight += rowHeight;
rowHeight = childHeight;
mChildPos.add(new ChildPosition(
getPaddingLeft() + mlp.leftMargin,
getPaddingTop() + maxHeight + mlp.topMargin,
getPaddingLeft() + childWidth - mlp.rightMargin,
getPaddingTop() + maxHeight + childHeight - mlp.bottomMargin
));
} else {
// 不換行
mChildPos.add(new ChildPosition(
getPaddingLeft() + rowWidth + mlp.leftMargin,
getPaddingTop() + maxHeight + mlp.topMargin,
getPaddingLeft() + rowWidth + childWidth - mlp.rightMargin,
getPaddingTop() + maxHeight + childHeight - mlp.bottomMargin
));
rowWidth += childWidth;
rowHeight = Math.max(childHeight, rowHeight);
}
}
// 布局每一個child
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
Log.i("Child大小W", child.getMeasuredWidth() + "#H:" + child.getMeasuredHeight());
ChildPosition pos = mChildPos.get(i);
//設定View的左邊、上邊、右邊底邊位置
child.layout(pos.left, pos.top, pos.right, pos.bottom);
}
}
// 自定義ViewGroup必須要有以下這個方法,否則拿不到child的margin的資訊
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
private float mLastYMove;
private float currentY;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (scrollable) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
this.mLastYMove = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
this.currentY = event.getRawY();
int scrolledY = getScrollY();
float diff = Math.abs(this.mLastYMove - this.currentY);
// 當手指拖動值大于TouchSlop值時,認為應該進行滾動,攔截子控件的事件
if (diff > mTouchSlop) {
int dy = (int) (this.mLastYMove - this.currentY);
if (scrolledY + dy < topBorder) {
dy = 0;
scrollTo(0, topBorder);
return true;
//最頂端,超過0時,不再下拉,要是不設定這個,getScrollY一直是負數
} else if (scrolledY + dy > bottomBorder) {
dy = 0;
scrollTo(0, bottomBorder);
return true;
}
scrollBy(0, dy);
this.mLastYMove = event.getRawY();
}
break;
}
}
return true;
}
}