前言
自定义控件按照使用方式不同可分为自定义View和自定义ViewGroup,自定义View一般用在没有子控件的控件上;自定义Viewgroup用在需要容纳控件的容器
1.自定义View
自定义View一般需要完成两个任务
1.计算出自身宽高并告知系统
2.绘制自身显示图形
由此需要进行的两个步骤
1.继承View,复写onMeasure方法,在onMeasure方法里测量自身宽高并通过setMeasuredDimension方法设置宽高
2.复写onDraw方法,绘制图形
注意:很多情况下我们需要让自定义的控件支持自定义属性,这时我们需要在这两个步骤之前自定义属性,关于自定义属性可以参考这篇文章 自定义属性基础
1.1 onMeasure步骤解读
由于在使用该自定义控件时设置的宽高可以是具体的值,也可以是match_parent和wrap_content,而在通过setMeasuredDimension设置宽高必须是具体的值,所以我们需要在设置之前需要根据自己的需求进行转化和处理,那么怎么转化?
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
在onMeasure的参数中widthMeasureSpec和heightMeasureSpec里面都包含了约束信息,包括3种测量模式和宽高值等,google把此整数32位的前两位用来存放测量模式的信息,后30位用来存放宽高信息,想要从widthMeasureSpec或者heightMeasureSpec取出这些信息也很简单只需要调用下面两个方法就可以了
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
测量模式有3种,分别是UNSPECIFIED和AT_MOST以及EXACTLY
UNSPECIFIED:父控件没有限制子控件的大小,子控件可以取任何值,adapterView的item的heightmode和ScrollView的子控件的heightmode就是取的这个模式(此模式在开发中很少用到)
AT_MOST:约束值为子控件能取的最大值
EXACTLY:表示子控件应该直接取约束值
一般来说,子View的测量模式由自身宽高设置值和父控件的测量模式共同决定,具体可看ViewGroup的源码。不过通常我们可将3种测量模式和宽高取值match_parent、wrap_content、具体数值这样对应:
具体数值---EXACTLY
match_parent---EXACTLY
wrap_content---AT_MOST
一般来说EXACTLY模式不需要我们做太多处理,直接将宽高设置成约束值即可,我们需要处理的是测量模式为AT_MOST的情况,根据此模式的含义我们也可以猜到,在不超过约束值的情况下,应该刚好能包裹内容就可以了,需要注意的是应该考虑padding对宽高的影响。
1.2 onDraw方法解读
在这个方法里通过canvas.drawXXX等方法画出各种图形,需要注意的是画图的边界问题,要自己处理不能超过padding区域。关于canvas的文章可以看看这篇:
Canvas类的最全面详解
1.3 一个简单的例子
/**
* Created by apeku on 2019/1/31.
* 该自定义View主要是根据xml设置的正方形的边长画一个正方形和一个内接正方形的圆
*/
public class MSelfDefineView extends View {
private int mSquareSideLength;
public MSelfDefineView(Context context) {
this(context,null);
}
public MSelfDefineView(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public MSelfDefineView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获得自定义属性,正方形的边长
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MSelfDefineView);
mSquareSideLength = typedArray.getDimensionPixelSize(R.styleable.MSelfDefineView_lengthofside, 40);
typedArray.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int width=0;
int height=0;
switch (widthMode){
case MeasureSpec.EXACTLY:
width=widthSize;
break;
case MeasureSpec.AT_MOST:
//将padding考虑在内
int paddingLeft=getPaddingLeft();
int paddingRight=getPaddingRight();
width=Math.min(widthSize,paddingLeft+paddingRight+mSquareSideLength);
break;
default:
break;
}
switch (heightMode){
case MeasureSpec.EXACTLY:
height=heightSize;
break;
case MeasureSpec.AT_MOST:
height=Math.min(heightSize,getPaddingTop()+getPaddingBottom()+mSquareSideLength);
break;
default:
break;
}
setMeasuredDimension(width,height);
}
@Override
protected void onDraw(Canvas canvas) {
Paint mPaint=new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(3);
mPaint.setColor(Color.parseColor("#000000"));
int width=getWidth();
int height=getHeight();
//考虑padding对正方形中心点的影响
float centerX=getPaddingLeft()+((float)(width-getPaddingLeft()-getPaddingRight()))/2;
float centerY=getPaddingTop()+((float)(height-getPaddingBottom()-getPaddingTop()))/2;
//手动控制画的正方形不能超过padding的区域,在此的逻辑是如果超过除开padding后剩余区域的大小,则把正方形大小设置为剩余区域宽高较小值
mSquareSideLength=Math.min(mSquareSideLength,Math.min(width-getPaddingLeft()-getPaddingRight(),height-getPaddingBottom()-getPaddingTop()));
canvas.drawRect(centerX-mSquareSideLength/2,centerY-mSquareSideLength/2,centerX+mSquareSideLength/2,centerY+mSquareSideLength/2,mPaint);
canvas.drawCircle(centerX,centerY,mSquareSideLength/2,mPaint);
}
}
xml布局文件如下:
<com.apeku.defindviewapp1.MSelfDefineView
android:layout_width="80dp"
android:layout_height="80dp"
android:paddingLeft="10dp"
android:paddingTop="20dp"
android:paddingRight="30dp"
android:paddingBottom="20dp"
app:lengthofside="50dp"
android:layout_centerInParent="true"
android:layout_marginLeft="40dp"
android:background="@color/colorPrimaryDark"/>
运行结果:
2.自定义ViewGroup
自定义ViewGroup需完成如下任务
- 测量子View的大小
- 测量自己并告知系统自身大小
- 排列子View
自定义ViewGroup的步骤
- 继承ViewGroup,复写onMeasure方法,测量子View的大小。由于子View完成了测量自身的方法,我们只需告诉子View去测量自己即可
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
参数childMeasureSpec需要我们自己传入,不过ViewGroup已经有了常规化的处理,即前面提到的子View的测量模式由父View的测量模式和自身宽高的设置值有关,相关代码如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
当然Viewgroup也提供了更为方便的测量子View的方法,例如测量单个子View的方法
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed)
以及测量所有子View的方法
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)
我们可根据情况选用即可。
- 测量自身大小,这一步和自定义View差不多,需要注意的点是当测量模式为AT_MOST时,自定义的ViewGroup需要包裹所有的子View,这里面包括了子View的大小,子View的margin值,以及自己的padding值。
- 排列子View。这一步需要我们自行计算各个子View的相对于父空间的位置,然后调用子View的layout方法确定子View的位置。
child.layout(left,top,right,bottom)
请注意,这里left,right,top,bottom的数值全都是相对于父控件的;同样的,我们也需要考虑父view的padding和子view自身的margin值对left,top,right,bottom的影响
特别注意:
- 有时我们需要自己复写generateLayoutParams函数,来为自定义的ViewGroup的子View确定LayoutParams的类型,LayoutParams将决定子View支持哪些layout属性,比如我们想在ViewGroup的子View支持margin属性我们应该这么做
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
- 当我们需要在ViewGroup额外绘制一些图形时,我们一般需要复写dispatchDraw方法而不是onDraw方法,这是因为当ViewGroup无背景时,调用是dispatchDraw方法而不会调用onDraw方法
一个流式布局的例子
public class FlowLayout extends ViewGroup {
private List<Integer> lineHeights=new ArrayList<Integer>();
private List<List<View>> lineViews=new ArrayList<List<View>>();
public FlowLayout(Context context) {
this(context,null);
}
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 onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int wrapWidth=0;
int wrapHeight=0;
int childCount=getChildCount();
int lineWidth=0;
int lineHeight=0;
lineHeights.clear();
lineViews.clear();
List<View> lineView=new ArrayList<View>();
for(int i=0;i<childCount;i++){
View child=getChildAt(i);
measureChild(child,widthMeasureSpec,heightMeasureSpec);
MarginLayoutParams lp= (MarginLayoutParams) child.getLayoutParams();
if(child.getVisibility()==View.GONE){
if(i==childCount-1){
wrapHeight+=lineHeight;
wrapWidth=Math.max(wrapWidth,lineWidth);
lineHeights.add(lineHeight);
lineViews.add(lineView);
}
continue;
}
if(lineWidth+child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin>widthSize-getPaddingLeft()-getPaddingRight()){
lineHeights.add(lineHeight);
wrapHeight+=lineHeight;
lineViews.add(lineView);
wrapWidth=Math.max(wrapWidth,lineWidth);
lineWidth=0;
lineHeight=0;
lineView=new ArrayList<View>();
}
//宽度和高度考虑了子View的margin值
lineWidth+=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
lineHeight=Math.max(lineHeight,child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin);
lineView.add(child);
if(i==childCount-1){
wrapHeight+=lineHeight;
wrapWidth=Math.max(wrapWidth,lineWidth);
lineHeights.add(lineHeight);
lineViews.add(lineView);
}
}
//wrap_content时宽高加上padding值
setMeasuredDimension(widthMode==MeasureSpec.EXACTLY?widthSize:wrapWidth+getPaddingLeft()+getPaddingRight(),
heightMode==MeasureSpec.EXACTLY?heightSize:wrapHeight+getPaddingTop()+getPaddingBottom());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int startTop=getPaddingTop();
int startLeft=getPaddingLeft();
int childLeft=0;
int childTop=0;
int childRight=0;
int childBottom=0;
for(int i=0;i<lineViews.size();i++){
List<View> lineView=lineViews.get(i);
for(int j=0;j<lineView.size();j++){
View child=lineView.get(j);
MarginLayoutParams lp= (MarginLayoutParams) child.getLayoutParams();
//排列子View时考虑了margin值
childTop=startTop+lp.topMargin;
childLeft=startLeft+lp.leftMargin;
childRight=childLeft+child.getMeasuredWidth();
childBottom=childTop+child.getMeasuredHeight();
//调用child.layout方法确定子View的位置
child.layout(childLeft,childTop,childRight,childBottom);
startLeft+=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
}
startLeft=getPaddingLeft();
startTop+=lineHeights.get(i);
}
}
//支持margin
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
}
运行结果如下