自定义View
代码
public class MyView extends View {
private final static int REQUEST_DRAW = 0;
private final static int REQUEST_LAYOUT = 1;
private Paint mPaint;
private int mCount = 0;
private Rect mBounds;
//不能这样定义,因为此时Thread.sleep(1000);休眠的是主线程,所以很可能导致ANR,就算没有导致ANR,
//比如此时主界面有一个button,那么该button永远也接受不到点击事件,因为点击事件的处理也是在main线程,但是main线程一直在这里阻塞了
/*private Handler anoHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
while (true) {
try {
mCount++;
if (mCount == 100) {
break;
}
Thread.sleep(1000);
invalidate();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};*/
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case REQUEST_DRAW:
invalidate();//只会调用onDraw
break;
case REQUEST_LAYOUT:
requestLayout();//会重新调用整个流程,onMeasure,onLayout,onDraw
break;
default:
break;
}
}
};
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(30);
mBounds = new Rect();
new Thread() {
@Override
public void run() {
while (true) {
try {
mCount++;
if (mCount == 100) {
break;
}
Thread.sleep(1000);
if (mCount == 10) {
handler.sendEmptyMessage(REQUEST_LAYOUT);//此时变成两位数,所以需要重新测量布局
} else {
//注意这里不能直接调用invalidate,控件在main线程,同时控件是线程不安全的,所以不能在子线程中刷新view,所以只能通过handler发消息到main线程
//或者可以直接使用postInvalidate();实际上postInvalidate()内部实现使用了handler,原理还是发消息到main线程,让main线程去刷新view
handler.sendEmptyMessage(REQUEST_DRAW);//否则只是重绘view,不需要重新测量布局
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
@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);
String text = String.valueOf(mCount);
mPaint.getTextBounds(text, 0, text.length(), mBounds);
//文字的宽度,为了支持padding,必须加上-左右padding
//为什么不用mBounds.width()测量文字宽,而是用measureText方法,通过实际发现,mBounds.width()测量宽度的结果并不准确
int textWidth = (int) mPaint.measureText(text) + getPaddingLeft() + getPaddingRight();
//文字的高度,为了支持padding,必须加上-上下padding
int textHeigth = mBounds.height() + getPaddingTop() + getPaddingBottom();
//如果设置了背景图片或者view中设置了android:minWidth,android:minHeight属性,那么defWidth等于getSuggestedMinimumWidth/getSuggestedMinimumHeight
//否则就是文字的宽度/高度,注意为了支持padding,宽度必须加上上下左右padding
int defWidth = getSuggestedMinimumWidth() == 0 ? textWidth : getSuggestedMinimumWidth();
int defHeight = getSuggestedMinimumHeight() == 0 ? textHeigth : getSuggestedMinimumHeight();
//如果是EXACTLY,设置了具体数值/match_parent,就使用父view传递下来的widthSize/heightSize,否则如果是wrap_content,AT_MOST就使用defWidth/defHeight
int width = (widthMode == MeasureSpec.EXACTLY) ? widthSize : defWidth;
int heigth = (heightMode == MeasureSpec.EXACTLY) ? heightSize : defHeight;
setMeasuredDimension(width, heigth);
//实际上以上的实现,基本跟这句等效,只是如果view设置的是wrap_content,模式为AT_MOST,那么最后view的大小得到的将是父view传递下来的大小
//而上面的代码中对wrap_content,模式为AT_MOST进行了自己的实现
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas); 因为父类view的onDraw没有任何实现,如果继承自button等,就必须调用super.onDraw(canvas)
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
//画一个长方新蓝色背景
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
String text = String.valueOf(mCount);
mPaint.setColor(Color.YELLOW);
mPaint.getTextBounds(text, 0, text.length(), mBounds);
int textWidth = (int) mPaint.measureText(text);
int textHeigth = mBounds.height();
//在view的中间画数字
canvas.drawText(text, getMeasuredWidth() / 2 - textWidth / 2, getMeasuredHeight() / 2 + textHeigth / 2, mPaint);
}
}
效果
<lbb.mytest.demo.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minHeight="50dp"
android:minWidth="50dp"/>
实际显示:中间的数字一直在更新。
此时width设置为wrap_content,但是设置了minWidth。所以该view最后的大小就是50dp
<lbb.mytest.demo.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
实际显示:中间的数字一直在更新
此时width设置为wrap_content,但是没有设置背景图片也没有设置minWidth,同时没有设置padding,所以最后宽度就是文字的实际宽度,在数字累加到10的时候,会重新测量一次。
invalidate: 刷新view,调用OnDraw,不可以在子线程中调用
postInvalidate:刷新view,调用OnDraw,可以在子线程中直接刷新,内部使用handler发消息到main线程
requestLayout: 重新测量布局绘制view,调用过程onMeasure,onLayout,onDraw
自定义ViewGroup
代码
对《Android 手把手教您自定义ViewGroup》 中的例子做了优化,让它支持margin,同时增加了自身的绘制过程。
package lbb.mytest.demo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by liaobinbin on 2015/10/19.
*/
public class MyViewGroup extends ViewGroup {
private Paint paint;
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
//setPadding(0, 0, 0, 0);//忽略padding的影响
}
//为了支持margin,默认情况下ViewGroup的generateLayoutParams方法返回的是LayoutParams对象,只能获取layout_width和layout_height。
//但是MarginLayoutParams(context,attrs)会额外获取margin参数,padding默认是支持的。
//generateLayoutParams在inflate过程中被调用,未每个子view生成layoutparam。setContentView内部其实也是调用inflate函数的
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@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);
//必须先手动测量一次viewgroup内所有子view,否则子view的getMeasuredWidth/getMeasuredHeight都是0
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;//如果设置了具体数值或者使用了match_parent,那么使用父控件传下来的size
} else { //如果是wrap_content(AT_MOST),那么就自己计算width
int tWidth = 0, bWidth = 0;
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
if (i == 0 || i == 1) {
tWidth += view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
} else {
bWidth += view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
}
width += getPaddingLeft() + getPaddingRight() + Math.max(tWidth, bWidth);//上下排最大值+padding作为width
if (width > widthSize) {//如果计算出来的值比父控件给的size还要大,那就说明已经超过了最大值。
width = widthSize;
}
}
int height = 0;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
int lHeight = 0, rHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
if (i == 0 || i == 2) {
lHeight += view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
} else {
rHeight += view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
}
height += getPaddingTop() + getPaddingBottom() + Math.max(lHeight, rHeight);
if (height > heightSize) {
height = heightSize;
}
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int startX = 0, startY = 0;
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();//取得子view的layoutparam参数
switch (i) {
case 0:
startX = getPaddingLeft() + lp.leftMargin;
startY = getPaddingTop() + lp.topMargin;
break;
case 1:
startX = getMeasuredWidth() - getPaddingRight() - lp.rightMargin - view.getMeasuredWidth();
startY = getPaddingTop() + lp.topMargin;
break;
case 2:
startX = getPaddingLeft() + lp.leftMargin;
startY = getMeasuredHeight() - getPaddingBottom() - lp.bottomMargin - view.getMeasuredHeight();
break;
case 3:
startX = getMeasuredWidth() - getPaddingRight() - lp.rightMargin - view.getMeasuredWidth();
startY = getMeasuredHeight() - getPaddingBottom() - lp.bottomMargin - view.getMeasuredHeight();
break;
default:
break;
}
view.layout(startX, startY, startX + view.getMeasuredWidth(), startY + view.getMeasuredHeight());
}
}
//测量,布局完毕之后,就到了绘制过程
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
paint.setColor(getResources().getColor(android.R.color.darker_gray));
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);//在viewgroup的最外层绘制一个长方形作为背景
paint.setColor(getResources().getColor(R.color.background));//#FF00FFE1
paint.setStyle(Paint.Style.FILL);
int left = getPaddingLeft();
int right = getMeasuredWidth() - getPaddingRight();
int top = getPaddingTop();
int buttom = getMeasuredHeight() - getPaddingBottom();
canvas.drawRect(left, top, right, buttom, paint);//viewgroup去掉padding绘制一个长方形作为背景
}
}
主界面
<?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="5dp">
<lbb.mytest.demo.MyViewGroup
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginTop="10dp"
android:background="#00000000"
android:padding="30dp">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="#fff944"
android:text="11"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00ff00"
android:text="22"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0000"
android:text="11"/>
<lbb.mytest.demo.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"/>
</lbb.mytest.demo.MyViewGroup>
</LinearLayout>
实际显示,MyView支持padding,MyViewGroup完美支持margin
MyViewGroup中measureChildren(widthMeasureSpec, heightMeasureSpec);会去测量每个子view,所以当测量myview的时候,自然会去执行myView的onMeasure,这个时候测量出来的结果自然是文字的宽高加上padding了。
分析
1、ViewGroup的职责是啥?
ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。
2、View的职责是啥?
View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。
3、ViewGroup和LayoutParams之间的关系?
大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。
首先可以去参考《Android View绘制流程》《Android LayoutInflater原理分析》
1. 为什么要去重写generateLayoutParams方法才能支持margin?
因为不管viewgroup通过inflate加载还是setcontentview(内部其实还是通过inflate)加载,都会去解析xml中的资源,然后会为每个view设置它的layoutparam,这个layoutparam是通过它的父viewgroup调用generateLayoutParams方法得到的,默认情况下这个方法返回的是LayoutParams对象,只能获取layout_width,layout_height。如果要支持margin,那么generateLayoutParams方法就必须返回MarginLayoutParams对象。
ViewGroup.LayoutParams params = null;
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
//inflate的是根View,也是generateLayoutParams这个带attrs参数的重载方法。
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
在inflate视图的时候,递归解析子视图时,调用了viewgroup的generateLayoutParams(attrs)方法,并把该layoutparam绑定给了该view
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,R.styleable.ViewGroup_Layout_layout_width,R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
默认情况下LayoutParams对象只会获取,layout_width和layout_height这两个参数。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
setBaseAttributes(a,
R.styleable.ViewGroup_MarginLayout_layout_width,
R.styleable.ViewGroup_MarginLayout_layout_height);
int margin = a.getDimensionPixelSize(
com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
}
.............
}
所以如果要能获取margin参数,那么必须重写
generateLayoutParams方法
另一方面为什么LinearLayout等支持,因为他们实现了自己的测量过程,已经在测量过程中计算了margin。可以看到LinearLayout的内部类LayoutParams
public static class LayoutParams extends ViewGroup.MarginLayoutParams。继承了MarginLayoutParams所以可以获取margin
2. 为什么说默认是支持padding的?
viewgroup测量循环测量子view大小,measureChildren(widthMeasureSpec, heightMeasureSpec);可以看到里面把padding考虑进去了
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);
}
}
}
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
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中使用了padding,其实也是用了layoutparam,但是layoutparam只取了了layout_width和layout_height,当然不支持margin了。
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://如果viewgroup是EXACTLY
if (childDimension >= 0) { //如果子view设置的是具体数值
resultSize = childDimension; //具体数值
resultMode = MeasureSpec.EXACTLY; //EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子view设置的是MATCH_PARENT
// Child wants to be our size. So be it.
resultSize = size; //父view的大小
resultMode = MeasureSpec.EXACTLY; //EXACTLY
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子view设置的是WRAP_CONTENT
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //父view的大小
resultMode = MeasureSpec.AT_MOST; //AT_MOST
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST: //如果viewgroup是AT_MOST
if (childDimension >= 0) { //如果子view设置的是具体数值
// Child wants a specific size... so be it
resultSize = childDimension; //具体数值
resultMode = MeasureSpec.EXACTLY; //EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子view设置的是MATCH_PARENT
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size; //父view的大小
resultMode = MeasureSpec.AT_MOST; //AT_MOST
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子view设置的是WRAP_CONTENT
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //父view的大小
resultMode = MeasureSpec.AT_MOST; //AT_MOST
}
break;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//现在测量子view了,其实viewgroup自身的大小也是这里的。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());//背景图片大小
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size; //如果是UNSPECIFIED,那么使用的getSuggestedMinimumWidth,很可能就是背景图片大小了。
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize; //说明如果是AT_MOST,EXACTLY那么最终的结果是从measureSpec中拿到的。
break;
}
return result;
}
可以看到如果是UNSPECIFIED,那么View最终的大小将是背景图片宽高和android:minWidth/android:minHeight之间的最大值
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
case R.styleable.View_minHeight:
mMinHeight = a.getDimensionPixelSize(attr, 0);