自定义容器
容器类一般要继承ViewGroup类,ViewGroup类同时也是View的子类。ViewGroup又是一个抽象类,定义了onLayout()等抽象方法。当然,根据需要,我们也可以让容器类继承自FrameLayout等ViewGroup的子类。比如ListView继承自ViewGroup,而ScrollView继承自FrameLayout。
ViewGroup类
ViewGroup常用方法
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
private View[] mChildren;
private int mChildrenCount;
public int getChildCount(){}
public View getChildAt(int index){}
public void addView(View child){}
public void addView(View child,int index){}
public void addView(View child,int width,int height){}
public void addView(View child,LayoutParams params){}
public void addView(View child,int index,LayoutParams params){}
}
在ViewGroup中,定义了一个View[]类型的数组mChildren,该数组保存了容器中所有的子组件,负责维护组件的添加、移除、管理组件顺序等功能,另一个成员变量mChildrenCount则保存了容器中子组件的数量。在layout布局文件中,容器中的子元素会根据顺序自动添加到mChildren数组中。
ViewGroup具备了容器类的基本特征和运作流程,也定义了相关的方法用于访问容器内的组件,主要的方法有:
//获取容器内的子组件的个数;
public int getChildCount()
//容器内的所有子组件都存储在名为mChildren的View[]数组中,该方法通过索引index找到指定位置的子组件;
public View getChildAt(int index)
//向容器中添加新的子组件,child表示子组件(也可以是子容器),index表示索引,指定组件所在的位置,params参数为组件指定布局参数。该方法还有两个简化的版本;
public void addView(View child,int index,LayoutParams params)
//添加child子组件,并为该子组件指定布局参数;
public void addView(View child,LayoutParams params)
//布局参数使用默认的ViewGroup.LayoutParams,其中layout_width和layout_height均为wrap_content;
public void addView(View child,int index)
//布局参数同上,但index为-1,表示将child组件添加到mChildren数组的最后。向容器中添加新的子组件时,子组件不能有父容器,否则会报错;
public void addView(View child)
//移除index位置的子组件;
public void removeViewAt(int index)
//移除子组件View;
pubic void removeView(View view)
//移除从start开始连续的count个子组件;
public void removeViews(int start,int count)
//测量子组件的尺寸;
protected void measureChild(View child,int parentWidthMeasureSpec,int parentHeightMeasureSpec)
//测量所有子组件的尺寸;
protected void measureChildren(int parentWidthMeasureSpec,int parentHeightMeasureSpec)
//该方法从View类中继承,用于测量组件或容器自己的尺寸,参数 widthMeasureSpec 和 heightMeasureSpec 为 0 时表示按实际大小进行测量;
public final void measure(int widthMeasureSpec,int heightMeasureSpec)
ViewGroup运行的基本流程大致为:
1 . 测量容器尺寸:
重写onMeasure()方法测量容器的大小,和自定义组件有所区别的是,在测量容器大小之前,必须先调用measureChildren()方法测量所有子组件的大小,不然结果永远为0。
2 . 确定每个子组件的位置:
重写onLayout()方法确定每个子组件的位置,在onLayout()方法中,调用View的layout()方法确定子组件的位置。
3 . 绘制容器
重写onDraw()方法,其实ViewGroup类并没有重写onDraw()方法,除非有特别的要求,自定义容器很少去重写。比如LinearLayout重写了该方法用于绘制水平或垂直分割线,而FrameLayout则是重写了draw()方法,作用其实是一样的。
ViewGroup的工作原理
onMeasure()
重写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);
}
}
}
measureChildren()方法中,循环遍历每一个子组件,如果当前子组件的可见性不为GONE,也就是没有隐藏则继续调用measureChild()方法测量当前子组件的大小。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);
}
measureChild()方法结合父容器的MeasureSpec、子组件的padding和LayoutParams三个因素利用getChildMeasureSpec()方法计算出子组件的尺寸模式和尺寸大小,并调用子组件的measure()方法进行尺寸测量。measure()方法的实现如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//---;
onMeasure(widthMeasureSpec,heightMeasureSpec);
//---;
}
measure()方法调用了onMeasure()方法,该方法正是我们重写的用来测量组件尺寸的方法。
我们可以发现,从根元素出发,一步步向下递归驱动测量,每个组件又负责计算自身大小。
onLayout()
接下来调用onLayout()方法定位子组件,以确定子组件的位置和大小,在onLayout()方法中,将会调用子组件的layout(),这里要一分为二,如果子组件是一个View,定位流程就到此结束,如果子组件又是一个容器,又会继续调用该容器的onLayout()方法对组件进行定位。所以,onLayout()方法也是一个递归的过程。
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
onLayout(changed, l, t, r, b);
}
onDraw()
ViewGroup类并没有重写该方法,但是每一个组件在绘制的时候会调用View的draw()方法,draw()方法如下:
public void draw(Canvas canvas) {
//---;
dispatchDraw(canvas);
//---;
}
draw()方法中执行了语句dispatchDraw()方法,但是在View类中的dispatchDraw()方法是空方法,在ViewGroup类中,该方法是实现了的。如下:
protected void dispatchDraw(Canvas canvas) {
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
dispatchDraw()方法的作用是将绘制请求分发到各子组件,并调用drawChild()方法来完成子组件的绘制,drawChild()方法的源码如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild()方法再次调用了子组件的draw()方法,该方法如下:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
//---;
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
//---;
}
最后也调用了draw()方法,如果子组件不再是一个容器,将调用onDraw()语句完成组件的绘制,同样的,onDraw()方法,正是我们需要重写的方法。组件的绘制同样是一个不断递归的过程。
重写onLayout()方法
onLayout()方法的原型为:onLayout(boolean changed,int l,int t,int r,int b),其中参数changed判断是否有新的大小和位置变化,l表示left,t表示top,r表示right,b表示bottom,后面的四个参数表示容器自己相对父容器的位置以及自身的大小。通常情况下,r - l的值等同于方法getMeasuredWidth()方法的返回值,b - t的值等同于getMeasuredHeight()方法的返回值。
在onLayout()方法中,需要调用View的layout()方法用于定义子组件和子容器的位置,layout()方法的原理如下:
public void layout(int l,int t,int r,int b)
参数l、t、r、b四个参数的作用与上面相同,通过这四个参数,基本可以确定子组件的位置与大小了。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
--------------------last line for now--------------------