View的绘制流程是从ViewRoot的PerformTraversals方法开始的。它经过measure,layout,draw三个过程将view绘制出来。mesure用来测量view的宽高,layout用来确定位置,draw绘制。流程图如下
performTraversals会依次调用performMeasure, performLayout, performDraw三个方法,这三个方法分别完成顶层View的measure,layout,draw方法,onMeasure又会调用所有子元素的measure过程,直到完成整个View树的遍历。同理,performLayout, performDraw的传递流程与performMeasure相似。唯一不同在于,performDraw的传递过程在draw方法中通过dispatchDraw实现,但没有本质区别。
Measure过程后可以调用getMeasureWidth和getMeasureHeight方法获取View测量后的宽高,与getWidth和getHeight的区别是:getMeasuredHeight()返回的是原始测量高度,与屏幕无关,getHeight()返回的是在屏幕上显示的高度。几乎在所有情况下两者相等
Layout过程确定View四个顶点的位置和实际的宽高。
Draw过程确定View的显示,只有draw方法完成后View的内容才会出现在屏幕上。
MeasureSpec代表一个32位int值,高2位表示SpecMode,低30位表示SpecSize。SpecMode表示测量模式,SpecSize表示在对应模式下的大小。
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* 没有限制。要多大有多大
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* 精确模式,对应LayoutParams中的match_parent和具体数值这两种模式
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* 最大模式,大小不定,但是不能超过窗口的大小。对应LayoutParams中的wrap_content
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
普通View的MeasureSpec的创建规则
(1)measure过程 要分情况,如果只是一个view,那么measure就完了。如果是ViewGroup,除了自己测量外,还会去遍历调用所有子元素的measure方法。 (1.1)view的measure过程 过程我就不详述了,我就说明一下注意点 直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。 如何解决呢
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//当mode=AT_MOST时,设置默认的宽高
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
(2)ViewGroup的measure过程 ViewGroup是一个抽象类,并没有重写onMeasure方法,而是提供了一个measureChildren方法,会对每一个子元素进行measure。 原文以LinearLayout为例,可以看看
注意:现在你应该明白为什么onCreate,onStart,onResume里面获取view的宽高为0了吧,因为只有measure完成才能获取宽高,而onCreate,onStart,onResume的时候view还没有生成,自然取不到。当然,还是有方法获取的
(1)Activity/view:onWindowFocusChanged
这个函数的含义是:view已经初始化完毕,宽高已经准备好,获取自然没问题。这个函数会被平频繁调用.在Activity窗口得到或者失去焦点时时候被调用,即onResume和onPause都会调用这个函数
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
(3)ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener接口,当View树的状态发生改变或者View树内部的View的可见性发现改变时,onGlobalLayout方法将被调用,因此是获得View宽高的好时机。需要注意的是,伴随View树的状态改变等,onGlobalLayout会被多次调用。典型代码:
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)
手动调用View的measure方法,通过手动并正确调用View的measure过程后,就可以通过View.getMeasureWidth()方法得到测量后的宽高。这种方法比较复杂,需要根据View的LayoutParams来分:
1.match_parent
直接放弃,无法measure出具体的宽高。原因在于根据View的Measure过程,需要构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小。
2.wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
3.具体的数值(比如宽/高都是100px)
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
(2)layout过程 大致流程如下:首先通过setFrame确定四个顶点的位置,接着调用onLayout方法,确定子元素位置
(3)draw过程 遵循如下四步:
绘制背景background.draw(canvas)
绘制自己(onDraw)
绘制children(dispatchDraw)
绘制修饰(onDrawScrollBars)
view有个特殊的方法setWillNotDraw
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
可以看出中间有个标记位,通过设置这个标记为再调用invalidate()可以自动调用onDraw函数
分类 1.继承View重写onDraw方法 采用这种方法需要自己支持wrap_content,并且padding也需要自己处理 2.继承ViewGroup派生特殊的Layout 处理ViewGroup的测量、布局两个过程,子元素的测量和布局 3.继承特定的View(比如TextView) 比较简单,不需要自己支持wrap_content和padding 4.继承特定的ViewGroup(比如LinearLayout) 不需要自己处理ViewGroup的测量、布局两个过程
注意点 1.让View支持wrap_content 继承自view的控件,如果不对wrap_content处理,相当于设置了match_parent 2.如果有必要,让View支持padding 继承view的控件,如果不处理padding,那么padding属性不起作用 3.尽量不要在View中使用Handler,没必要 view有post系列方法 4.View中如果有线程或动画,需要及时停止,参考View#onDetachedFromWindow 5.View带有滑动嵌套情形时,需要处理好滑动冲突
1.继承view重写onDraw 第一步,创建自定义属性的xml(attrs.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
第二步:在构造函数中解析这个xml文件
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs,
R.styleable.CircleView);
color = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
array.recycle();
mPaint.setColor(color);
}
第三步:使用自定义属性
<com.example.androidtest.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="#FFD700" />
注意,有wrap_content和padding 最后贴出源码:
public class CircleView extends View {
public CircleView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CircleView(Context context) {
super(context);
init(context, null);
}
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int color;
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs,
R.styleable.CircleView);
color = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
array.recycle();
mPaint.setColor(color);
}
/**
* 处理wrap_content,设置默认的200宽度
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
/**
* 在onDraw中处理padding
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int getPaddingLeft = getPaddingLeft();
int getPaddingRight = getPaddingRight();
int getPaddingTop = getPaddingTop();
int getPaddingBottom = getPaddingBottom();
int width = getWidth() - getPaddingLeft - getPaddingRight;
int height = getHeight() - getPaddingTop - getPaddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(getPaddingLeft + width / 2, getPaddingTop + height
/ 2, radius, mPaint);
}
}
2.继承viewgroup派生特殊的layout 我这里贴源码吧,可以看注释去理解
public class MyHorizontalScrollView extends ViewGroup {
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 上次滑动的坐标
private int mLastX, mLastY;
// 上次滑动的坐标 onInterceptTouchEvent
private int mLastXIntercept, mLastYIntercept;
private Scroller scroller;
private VelocityTracker mVelocityTracker;
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
if (scroller == null) {
scroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("ACTION_DOWN", "ACTION_DOWN");
intercepted = false;
if (!scroller.isFinished()) {
scroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
Log.e("ACTION_MOVE", "ACTION_MOVE");
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
//模板有介绍,这里写父容器需要滑动事件的条件
//如果x方向上的距离大于y,则认为是滑动,要拦截。之后就调用onTouchEvent函数
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
Log.e("ACTION_UP", "ACTION_UP");
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
Log.e("intercepted", "" + intercepted);
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("ACTION_DOWN", "ACTION_DOWN");
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
Log.i("ACTION_MOVE", "ACTION_MOVE");
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
Log.i("ACTION_UP", "ACTION_UP");
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
Log.e("dx", "" + dx);
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastX = x;
mLastY = y;
return true;
}
private void smoothScrollBy(int dx, int dy) {
scroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.i("onMeasure", "onMeasure");
Log.i("width", "" + getWidth());
Log.i("MeasuredWidth", "" + getMeasuredWidth());
int measureWidth = 0;
int measureHeight = 0;
int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(measureWidth, measureHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(getChildAt(0).getMeasuredWidth() * childCount,
heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, getChildAt(0)
.getMeasuredHeight());
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i("onLayout", "onLayout");
Log.e("width", "" + getWidth());
int childLeft = 0;
int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}