Android ViewGroup详解及自定义
理解Android ViewGroup及自定义ViewGroup
什么是父控件和子控件
父控件就是容纳子控件的控件(也就是我们常说的布局)也称作容器,常见的父控件有LinearLayout,RelativeLayout,FrameLayout,TableLayout,GridLayout;
子控件是被父控件(也就是我们常说的布局)包裹住的控件,常见的子控件有TextView,Button,Edit Text,ImageView
例:下面的代码中LinearLayout为父控件,里面容纳的TextView为子控件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="这是一段文字"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
</LinearLayout>
什么是ViewGroup
ViewGroup是上面提到的所有的父控件的父类;但ViewGroup是一个抽象类,它里面有一个抽象方法onLayout,这个方法的作用就是摆放它所有的子控件(安排位置),因为是抽象类,不能直接new对象,所以我们在布局文件中不能直接使用 ViewGroup
例:直接使用ViewGroup为父控件运行后会报错
<?xml version="1.0" encoding="utf-8"?>
<ViewGroup
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="这是一段文字"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
</ViewGroup>
不能实例化抽象类android.view.ViewGroup
ViewGroup的工作原理
创建一个View(执行构造方法)的时候不需要测量控件的大小,只有将这个view放入容器(父控件)中的时候才需要测量;当子控件的父控件要放置该子控件的时候,父控件会调用子控件的onMeaonsure()方法,然后传入两个参数widthMeasureSpec和heightMeasureSpec,这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件,子控件取得这些条件就能正确的测量自身的宽高了
如果还有不明白请点击这里onMeaonsure()方法详解
自定义ViewGroup步骤
- 覆盖构造方法(必须)
- 重写onMeaonsure()测量控件尺寸(可选)
- 重写onLayout()方法摆放控件(必须)
下面是个简单的自定义ViewGroup的例子,顺序放置所有控件,如果一行不够放置则换行
public class MyLayout extends ViewGroup {
private Context mContext;
public MyLayout(Context context) {
super(context, null);
}
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
this.mContext=context;
}
public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);// 计算出所有的childView的宽和高
//测量并保存layout的宽高(使用getDefaultSize时,wrap_content和match_perent都是填充屏幕)
//稍后会重新写这个方法,能达到wrap_content的效果
setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount(); //子控件个数
int childWidth = 0; //子控件宽度
int childHeight = 0; //子控件高度
int myLayoutWidth = 0; //MyLayout当前宽度
int myLayoutHeight = 0; //MyLayout当前高度
int maxHeight = 0; //一行中子控件最高的高度
for(int i = 0; i<count; i++){
View child = getChildAt(i);
//注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
childWidth = child.getMeasuredWidth(); //子控件的测量宽度
childHeight = child.getMeasuredHeight(); //子控件的测量高度
if( getWindowWidthPixels ()-myLayoutWidth > childWidth ){ //屏幕剩余宽度大于子控件宽度
left = myLayoutWidth;
right = left+childWidth;
top = myLayoutHeight;
bottom = top+childHeight;
} else{ //屏幕剩余宽度小于子控件宽度就换行
myLayoutWidth = 0; //myLayoutWidth重置为0
myLayoutHeight += maxHeight; //当前myLayoutHeight=myLayoutHeight+上一行子控件的最大高
maxHeight = 0; //maxHeight重置为0
left = myLayoutWidth;
right = left+childWidth;
top = myLayoutHeight;
bottom = top+childHeight;
}
myLayoutWidth += childWidth; //宽度累加
if(childHeight > maxHeight){ //如果子控件的高度大于行最大高度
maxHeight = childHeight; //行最大高度设置为子控件高度
}
child.layout(left, top, right, bottom);//摆放子控件(左上右下)点的坐标值
}
}
//获取屏幕宽度
private int getWindowWidthPixels () {
Resources resources = mContext.getResources();
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
int width = displayMetrics.widthPixels;
return width;
}
}
<com.example.andy.mytest.MyLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView1"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView2"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView3"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView4"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView5"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView6"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView7"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView8"
android:textSize="25dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
</com.example.andy.mytest.MyLayout>
复杂的自定义ViewGroup
例:编写一个自定义ViewGroup扩展它的布局属性,子控件按扩展的布局属性放置显示
- 自定义ViewGroup的布局属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyTextView">
<!--声明MyTextView需要使用系统定义过的text属性,注意前面需要加上android命名-->
<attr name="android:text" />
<attr name="android:layout_width" />
<attr name="android:layout_height" />
<attr name="android:background" />
<attr name="mTextColor" format="color" />
<attr name="mTextSize" format="dimension" />
</declare-styleable>
<declare-styleable name ="CustomLayout">
<attr name ="layout_position">
<enum name ="center" value="0" />
<enum name = "Left" value="1"/>
<enum name = "Right" value="2"/>
<enum name = "top" value="3"/>
<enum name = "Bottom" value="4"/>
<enum name ="LeftTop" value="5" />
<enum name ="RightTop" value="6" />
<enum name ="LeftBottom" value="7" />
<enum name ="RightBottom" value="8" />
</attr >
</declare-styleable>
</resources>
- 自定义LayoutParams类
自定义LayoutParams的作用是扩展ViewGroup的布局属性
可以继承ViewGroup.LayoutParams,也可以继承ViewGroup.MarginLayoutParams;
继承ViewGroup.LayoutParams,自定义布局只是简单的支持layout_width和layout_height属性;
继承ViewGroup.MarginLayoutParams,就能使用layout_marginxxx属性
public class MyLayoutParams extends ViewGroup.MarginLayoutParams {
public static final int POSITION_CENTER = 0; // 中间
public static final int POSITION_LEFT = 1; // 左侧
public static final int POSITION_RIGHT = 2; // 右侧
public static final int POSITION_TOP = 3; // 上侧
public static final int POSITION_BOTTOM = 4; // 下侧
public static final int POSITION_LEFT_TOP = 5; // 左上方
public static final int POSITION_RIGHT_TOP = 6; // 右上方
public static final int POSITION_LEFT_BOTTOM = 7; // 左下方
public static final int POSITION_RIGHT_BOTTOM = 8; // 右下方
public int POSITION = POSITION_CENTER; // 默认位置为中间
//在这个构造方法中初始化参数值,会使布局文件被映射为对象的时候被调用
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray typedArray = c.obtainStyledAttributes(attrs,R.styleable.CustomLayout );
//获取设置在子控件上的位置属性
POSITION = typedArray.getInt(R.styleable.CustomLayout_layout_position ,POSITION );
typedArray.recycle();
}
public MyLayoutParams(int width, int height) {
super(width, height);
}
public MyLayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public MyLayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
- 重写generateLayoutParams()
<com.example.andy.mytest.MyLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ying="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_red_light">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文字1"
android:textSize="25dp"
android:padding="10dp"
android:layout_marginLeft="10dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"
ying:layout_position="LeftTop"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文字2"
android:textSize="25dp"
android:padding="10dp"
android:layout_marginLeft="10dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"
ying:layout_position="LeftBottom"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文字3"
android:textSize="25dp"
android:padding="10dp"
android:layout_marginRight="10dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"
ying:layout_position="RightTop"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文字4"
android:textSize="25dp"
android:padding="10dp"
android:layout_marginRight="10dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"
ying:layout_position="RightBottom"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文字5"
android:textSize="25dp"
android:padding="10dp"
android:layout_marginTop="50dp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"
ying:layout_position="center"/>
</com.example.andy.mytest.MyLayout>
public class MyLayout extends ViewGroup {
private Context mContext;
public MyLayout(Context context) {
super(context, null);
}
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
this.mContext=context;
}
public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//父控件ViewGroupMode和Size
int widthMode = MeasureSpec. getMode(widthMeasureSpec);
int heightMode = MeasureSpec. getMode(heightMeasureSpec);
int widthSize = MeasureSpec. getSize(widthMeasureSpec);
int heightSize = MeasureSpec. getSize(heightMeasureSpec);
int layoutWidth = 0;
int layoutHeight = 0;
int measureWidth = 0;
int measureHeight = 0;
int count = getChildCount();
//遍历所有子控件并保存了宽和高
for( int i = 0; i < count; i++){
View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
MyLayoutParams params = null;
if(widthMode == MeasureSpec. EXACTLY){ //父控件ViewGroup宽度设置(size/match_parent)
layoutWidth = widthSize; //layoutWidth=父控件剩余的宽度
} else{ //父控件ViewGroup宽度设置(UNSPECIFIED/wrap_content)
for ( int i = 0; i < count; i++) { //layoutWidth=遍历子控件后,所有子控件宽度最大的
View child = getChildAt(i);
measureWidth = child.getMeasuredWidth(); //子控件宽度值(若设置了padding则包含左右padding值)
params = (MyLayoutParams) child.getLayoutParams();
//获取子控件宽度和左右边距之和,作为这个控件需要占据的宽度
int marginWidth = measureWidth + params.leftMargin + params.rightMargin ;
layoutWidth = marginWidth > layoutWidth ? marginWidth : layoutWidth;
}
}
if(heightMode == MeasureSpec. EXACTLY){ //父控件ViewGroup高度设置(size/match_parent)
layoutHeight = heightSize; //layoutHeight=父控件剩余的高度
} else{ //父控件ViewGroup高度设置(UNSPECIFIED/wrap_content)
for ( int i = 0; i < count; i++) {
View child = getChildAt(i);
measureHeight = child.getMeasuredHeight(); //子控件高度值((若设置了padding则包含上下padding值)
params = (MyLayoutParams) child.getLayoutParams();
//获取子控件高度和上下边距之和,作为这个控件需要占据的高度
int marginHeight = measureHeight + params.topMargin + params.bottomMargin ;
layoutHeight = marginHeight > layoutHeight ? marginHeight : layoutHeight;
}
}
// 测量并保存父控件ViewGroup的宽高
setMeasuredDimension(layoutWidth, layoutHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
int childMeasureWidth = 0;
int childMeasureHeight = 0;
MyLayoutParams params = null;
for ( int i = 0; i < count; i++) {
View child = getChildAt(i);
// 注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
childMeasureWidth = child.getMeasuredWidth();
childMeasureHeight = child.getMeasuredHeight();
params = (MyLayoutParams) child.getLayoutParams();
switch (params. POSITION) {
case MyLayoutParams. POSITION_CENTER: // 中间
//getWidth()为父布局宽度
left = (getWidth()-childMeasureWidth)/2 - params.rightMargin + params.leftMargin ;
top = (getHeight()-childMeasureHeight)/2 + params.topMargin - params.bottomMargin ;
break;
case MyLayoutParams. POSITION_LEFT_TOP: // 左上方
left = 0 + params. leftMargin;
top = 0 + params. topMargin;
break;
case MyLayoutParams. POSITION_RIGHT_TOP: // 右上方
left = getWidth() - params.rightMargin - childMeasureWidth;
top = 0 + params. topMargin;
break;
case MyLayoutParams. POSITION_LEFT_BOTTOM: // 左下角
left = 0 + params. leftMargin;
top = getHeight() - params.bottomMargin - childMeasureHeight;
break;
case MyLayoutParams. POSITION_RIGHT_BOTTOM:// 右下角
left = getWidth() - params.rightMargin - childMeasureWidth;
top = getHeight() - params.bottomMargin -childMeasureHeight;
break;
default:
break;
}
// 确定子控件的位置,四个参数分别代表(左上右下)点的坐标值
child.layout(left, top, left+childMeasureWidth, top+childMeasureHeight);
}
}
//在布局文件被填充为对象的时候调用
//如果不重写布局文件中设置的布局参数无法取得
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MyLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MyLayoutParams(LayoutParams.MATCH_PARENT , LayoutParams.MATCH_PARENT);
}
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return p instanceof MyLayoutParams ;
}
}
本文地址:https://blog.csdn.net/weixin_45558166/article/details/112647461