(1)onAttach
(2)onVisibilityChanged
(3)onDetach
Measure过程决定了View的宽/高,Measure完成之后,在几乎所有的情况下它都等于View的最终的宽/高,但是特殊情况除外??????(找出什么情况一种是getMeasureHeight,一种的getwidth,前者是OnMeasure,后者是在onLayout,如果在onLayout中队l,t,r,d进行修改就会导致不一样)
所以会是setContentView
MeasureSpec是由32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。Measure通过将SpecMode和Spec打包成一个int值来避免过多对象内存分配,为了方便操作,提供了打包和解包方法。
SpecMode有三类,每一类都表示特殊的含义,如下所示。
UnSpecified 父容器不对View有任何限制,要多大给多大(测量可以测出来但是显示不出来)
EXACTLY
父容器已经检测出VIew所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式
AT_MOST (父容器给一个可用的Specsize,子View不能大于这个,会和自身测得大小比较,取小的)
父容器制指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体体现。他对应于LayoutParams中的wrap_content
对于普通View来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,先看一下ViewGroup的measureChildwithMargins
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
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; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
parentSpecMode(右边) |
EXACTLY |
AT_MOST |
UNSPECIFIED |
childLayoutParams(下面) |
|||
Dp/px |
EXACTLY childSize |
EXACTLY childSize |
EXACTLY childSize |
Match_Parent |
EXACTLY parentSize |
AT_MOST parentSize |
UNSECIFIED 0 |
Wrap_content |
AT_MOST parentSize |
AT_MOST parentSize |
UNSECIFIED 0 |
补充那个下拉刷新的控件
注意:当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化,但是大小不能超过父容器的剩余空间(相当于给了个默认值parentSize(父容器中可使用的大小),但是一般都会针对Wrap_content重写方法(下面会有)
Measure是final修饰的,所以不能重写,但是在View的Measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可,View的onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
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; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
可以看出size是通过getSuggestedMinimumWidth得到的,
1.先判断是否有背景,
2.然后跟Android:minWidth(mMinWidth)比较取最大值,这个size只有UNSPECIFIED模式下会使用。
如果是AT_MOST或者EXACTLY,那么就会用getDefaultSize,他返回的大小是View测量后的大小(SpecSize),这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的。
问题1:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent,前面的表格已经体现出来了
也就是前面的measureChildWithMargins----> getChildMeasureSpec中可以看出如果View在布局中使用Wrap_content,那么他的specMode是AT_MOST,这个时候就默认是parentSize(父容器中目前可以使用的大小),也就是父容器当前剩余的控件大小,很显然,View的宽高就等于父容器当前剩余的空间大小(后面有提到用AT_MOST然后给一个最大的值,然后会自动比较自身的大小,不过这个,区别在于这个Specsize,这种情况下View的specSize是parentSize,那种情况是好像是自身的大小),这种效果和在布局中使用match_parent完全一致,如何解决这个问题?
直接写死就行(注意这个是View的)
@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(widthMeasureSpec); int heightSpecSize = MeasureSpec.getMode(widthMeasureSpec); if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,mHeight); }else if(widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,heightSpecSize); }else if(heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,mHeight); } }
对于Viewgroup来说,除了完成自己的measure过程之外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,他没有重写View的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); } } }
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); }
取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法进行测量,getChildMeasureSpec在前面已经讲了????
总结(查看源码??????)
对于一个控件,先判断是什么,即是ViewGroup还是View,如果是ViewGroup那么就在Measure中就调用MeasureChild,如果是View就调用onMeasure
ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayout,RelativeLayout.(具体可以看看书上188页)
问题2:如果在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽高???
View的Measure过程和Activity的生命周期方法不是同步执行的,所以无法保证Activity执行了onCreate,onStart,onResume对某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽/高就是0.下面给出四种方法来解决这个问题。
(1)Activtiy/View#onWindowFocusChanged
这个方法的含义是:View的初始化完毕了,宽/高已经准备好了,这个时候去获取宽高是没有问题的。但是注意当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说就是当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁调用
(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的可见性发现改变时,onGloballLayout方法将被回调,因此这个获取View的宽/高一个好的时机
需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次
@Override protected void onStart() { super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @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的宽/高。这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分。
Match_parent
直接放弃,因为无法知道parentSize,也就是父容器的剩余空间,所以理论上不可能测量出View的大小。
具体的数值(dp/px)
比如宽/高都是如下measure
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec);
Wrap_content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(1<<30)-1, View.MeasureSpec.AT_MOST); view.measure(widthMeasureSpec,heightMeasureSpec);
因为Measure的大小(size)是后面30位决定的,所以1<<30-1就是取最大的值,因为AT_MOST会把给定的值和自身测量的值对比,然后取小的
View的测量宽/高和最终宽/高有什么区别?
这个问题可以具体为:View的getMeasureWidth和getWidth这两个方法有什么区别。至于getMeasureHeight和getHeight的区别和前两者完全一样,为了回答这个问题,首先我们看一下getWidth和getHeight这两个方法的具体实现
public final int getWidth(){ return mRight - mLeft; } public final int getHeight{ return mBottom- mTop; }
在View的默认实现中,view的测量宽/高和最终宽/高是相等的,只不过测量宽高是在Measure过程,而最终宽/高形成于View的layout过程,即两者的赋值是不同的(这就解释了getMeasureHeight和getHeight在一定情况下有差别的原因)
如果想搞成不一样可以重写View的layout方法,代码如下:
Public void layout(int l ,int t , int r ,int b){
Super.layout(l,t,r+100,b+100);
}
这样回到任何情况下View的最终宽/高总是比测量宽/高大100px,虽然这样做会导致View显示不正常并且也没有实际意义,但是这证明了测量宽/高的确可以不等于最终宽/高
<1>绘制背景backgroud.draw(canvas)
<2>绘制自己(onDraw)
<3>绘制children(dispatchDraw)(会遍历元素的draw方法)
<4>绘制装饰(onDrawScrollBars)
1.让View支持wrap_content
这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理(前面已经说了,也就是MeasureSpec的mode是At_most的时候在onMeasure进行处理)
2.如果有必要,让你的View支持Padding
因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性无法起作用。另外:直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效
3.尽量不要在View中使用Handler,没必要
这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确的要使用handler来发送消息。
4.View中如果有线或者动画,需要及时停止,参考View#onDetachedFromWindow
这一条很好理解,如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好时机。当包含此View的Activity退出或者当前View被remove时,view的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachToWindow,当包含此View的Activity启动时。View的onAttachedToWindow会被调用。同时当View变得不可见时我们需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄露。
5.View带有滑动嵌套情形时,需要处理好滑动冲突
1.继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法。采用自己的方式需要自己wrap_content,并且padding也需要自己处理,下面通过一个具体的例子来演示如何实现这种自定义View.
2.注意wrap_content和padding的生效方法
3.自定义View的一些简单方法
MainActivity.java
/** * Created by Administrator on 2016/2/21 0021. */ public class MyCircleView extends View { private int mColor = Color.GREEN; private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); public MyCircleView(Context context) { super(context); System.out.println("xcqw 123"); init(); } public MyCircleView(Context context, AttributeSet attrs) { super(context, attrs, 0); System.out.println("xcqw 345"); //这种是可以的 // mColor = attrs.getAttributeIntValue("http://schemas.android.com/com.dx.text.mycircleview","circle_color",Color.BLUE); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView2); mColor = a.getColor(R.styleable.CircleView2_circle_color, Color.RED); a.recycle(); init(); } public MyCircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); System.out.println("xcqw woshidaxiong"); init(); } private void init() { mPaint.setColor(mColor); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //为了处理 wrap_content 否则会跟match_parent//然后写死150 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(150, 150); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(150, widthSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(150, heightSpecSize); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //图1 图2 // int width = getWidth(); // int height = getHeight(); // int radius = Math.min(width,height)/2; // canvas.drawCircle( width / 2, height / 2, radius, mPaint); //解决padding无效 final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; int radius = Math.min(width, height) / 2; canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint); } }
attr.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CircleView2"> <attr name = "circle_color" format="color"/> </declare-styleable> </resources>
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000" tools:context=".MainActivity"> <!-- 这是因为margin属性是由父容器控制的,因此不需要再CircleView做处理--> <!-- padding是默认无法生效的,需要特殊处理--> <!-- wrap_content如果不作处理就会跟match_parent--> <com.dx.text.mycircleview.MyCircleView android:id="@+id/circle_one" android:layout_width="wrap_content" android:layout_height="100dp" android:layout_margin="20dp" android:padding="20dp" app:circle_color="#00ff00" android:background="#fff"/> </RelativeLayout>
注意事项:
(1)
xmlns:app="http://schemas.android.com/apk/res-auto"(推荐用这种)
xmlns:app="http://schemas.android.com/apk/res/包名"
这两个声明app是自定义属性的前缀,当然可以换其他名字,但是CircleView中的自定义属性的前缀必须和这里一致,比如,app:circle_color="#00ff00"
这两个声明本质没有什么区别
(2)
activity.xml
xmlns:app="http://schemas.android.com/apk/res-auto"
app:circle_color="#00ff00"
attr.xml
<resources>
<declare-styleable name="CircleView2">
<attr name = "circle_color" format="color"/>
</declare-styleable>
</resources>
MyCircleView.java
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView2);
mColor = a.getColor(R.styleable.CircleView2_circle_color, Color.RED);
注意这三者的关系!!!
另外可以跟另外一种自定义View的方法作对比
mColor = attrs.getAttributeIntValue("http://schemas.android.com/com.dx.text.mycircleview","circle_color",Color.BLUE);