一.View的常见回调方法
(1)onAttach
(2)onVisibilityChanged
(3)onDetach
Measure过程决定了View的宽/高,Measure完成之后,在几乎所有的情况下它都等于View的最终的宽/高,但是特殊情况除外??????(找出什么情况一种是getMeasureHeight,一种的getwidth,前者是OnMeasure,后者是在onLayout,如果在onLayout中队l,t,r,d进行修改就会导致不一样)
二.顶级View是DecorView的结构
所以会是setContentView
三.理解MeasureSpec
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
<1>DecorView的MeasureSpec由下面两个决定
(1)窗口的尺寸
(2)自身的LayoutParams
<2>普通的view的MeasureSpec由下面两个决定
(1)父容器的MeasureSpec
(2)自身的LayoutParams
对于普通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重写方法(下面会有)
四.View的工作流程
1.View的Measure过程
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);
}
}
2.ViewGroup的measure过程
对于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会把给定的值和自身测量的值对比,然后取小的
3.Layout过程
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显示不正常并且也没有实际意义,但是这证明了测量宽/高的确可以不等于最终宽/高
4.Draw过程
(1)View的绘制过程遵循如下几步:
<1>绘制背景backgroud.draw(canvas)
<2>绘制自己(onDraw)
<3>绘制children(dispatchDraw)(会遍历元素的draw方法)
<4>绘制装饰(onDrawScrollBars)
(2)自定义View的一些问题
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带有滑动嵌套情形时,需要处理好滑动冲突
(3)自定义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);