View绘制流程(3)----view的绘制流程及自定义View的相关问题

一.View的常见回调方法

(1)onAttach

(2)onVisibilityChanged

(3)onDetach

Measure过程决定了View的宽/高,Measure完成之后,在几乎所有的情况下它都等于View的最终的宽/高,但是特殊情况除外??????(找出什么情况一种是getMeasureHeight,一种的getwidth,前者是OnMeasure,后者是在onLayout,如果在onLayout中队l,t,r,d进行修改就会导致不一样)


二.顶级ViewDecorView的结构

View绘制流程(3)----view的绘制流程及自定义View的相关问题_第1张图片

所以会是setContentView

三.理解MeasureSpec

MeasureSpec是由32int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。Measure通过将SpecModeSpec打包成一个int值来避免过多对象内存分配,为了方便操作,提供了打包和解包方法。

SpecMode有三类,每一类都表示特殊的含义,如下所示。

UnSpecified  父容器不对View有任何限制,要多大给多大(测量可以测出来但是显示不出来

EXACTLY  

父容器已经检测出VIew所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式

 

AT_MOST (父容器给一个可用的Specsize,子View不能大于这个,会和自身测得大小比较,取小的)

父容器制指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体体现。他对应于LayoutParams中的wrap_content

<1>DecorViewMeasureSpec由下面两个决定

(1)窗口的尺寸

(2)自身的LayoutParams

 

<2>普通的viewMeasureSpec由下面两个决定

(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.ViewMeasure过程

Measurefinal修饰的,所以不能重写,但是在ViewMeasure方法中会去调用ViewonMeasure方法,因此只需要看onMeasure的实现即可,ViewonMeasure

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,那么他的specModeAT_MOST,这个时候就默认是parentSize(父容器中目前可以使用的大小),也就是父容器当前剩余的控件大小,很显然,View的宽高就等于父容器当前剩余的空间大小(后面有提到用AT_MOST然后给一个最大的值,然后会自动比较自身的大小,不过这个,区别在于这个Specsize,这种情况下ViewspecSizeparentSize,那种情况是好像是自身的大小),这种效果和在布局中使用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.ViewGroupmeasure过程

对于Viewgroup来说,除了完成自己的measure过程之外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。View不同的是,ViewGroup是一个抽象类,他没有重写ViewonMeasure方法,但是它提供了一个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直接传递给Viewmeasure方法进行测量,getChildMeasureSpec在前面已经讲了????

 

 

 

 

 

总结(查看源码??????

对于一个控件,先判断是什么,即是ViewGroup还是View,如果是ViewGroup那么就在Measure中就调用MeasureChild,如果是View就调用onMeasure

 

 

 

ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayout,RelativeLayout.(具体可以看看书188)

 

 

 

问题2:如果在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽高???

 

ViewMeasure过程和Activity的生命周期方法不是同步执行的,所以无法保证Activity执行了onCreate,onStart,onResume对某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽/高就是0.下面给出四种方法来解决这个问题。

 

(1)Activtiy/View#onWindowFocusChanged

这个方法的含义是:View的初始化完毕了,宽/高已经准备好了,这个时候去获取宽高是没有问题的。但是注意当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说就是当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResumeonPause,那么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();
        }
    });
}


4view.measure(int widthMeasureSpec , int heightMeasureSpec)


通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,这里要分情况处理,根据ViewLayoutParams来分。

 

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的测量宽/高和最终宽/高有什么区别?

这个问题可以具体为:ViewgetMeasureWidthgetWidth这两个方法有什么区别。至于getMeasureHeightgetHeight的区别和前两者完全一样,为了回答这个问题,首先我们看一下getWidthgetHeight这两个方法的具体实现

public final int getWidth(){
    return mRight - mLeft;
}

public final int getHeight{
    return mBottom- mTop;
}


View的默认实现中,view的测量宽/高和最终宽/高是相等的,只不过测量宽高是在Measure过程,而最终宽/高形成于Viewlayout过程,即两者的赋值是不同的(这就解释了getMeasureHeight和getHeight在一定情况下有差别的原因)

如果想搞成不一样可以重写Viewlayout方法,代码如下:

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做特殊处理(前面已经说了,也就是MeasureSpecmodeAt_most的时候在onMeasure进行处理

 

2.如果有必要,让你的View支持Padding

因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性无法起作用。另外:直接继承自ViewGroup的控件需要在onMeasureonLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效

 

3.尽量不要在View中使用Handler,没必要

这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确的要使用handler来发送消息。

 

4.View中如果有线或者动画,需要及时停止,参考View#onDetachedFromWindow

这一条很好理解,如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好时机。当包含此ViewActivity退出或者当前Viewremove时,viewonDetachedFromWindow方法会被调用,和此方法对应的是onAttachToWindow,当包含此ViewActivity启动时。ViewonAttachedToWindow会被调用。同时当View变得不可见时我们需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄露。

 

5.View带有滑动嵌套情形时,需要处理好滑动冲突

 

 

 

 

(3)自定义View示例

1.继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法。采用自己的方式需要自己wrap_content,并且padding也需要自己处理,下面通过一个具体的例子来演示如何实现这种自定义View.

2.注意wrap_contentpadding的生效方法

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);











 














你可能感兴趣的:(View绘制流程(3)----view的绘制流程及自定义View的相关问题)