4_View的工作原理

View的工作原理

1、初识ViewRoot和DecorView

为了更好的自定义View,还需要掌握View的底层工作原理,比如View的测量流程、布局流程和绘制流程,掌握这几个基本流程之后,我们就对View的底层更加了解,这样我们就可以做出一个比较完善的自定义View。

自定义View的实现看起来很复杂,实际上说简单也简单。

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成。

在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个流程才能最终将一个View绘制出来,其中:

①、measure用来测量View的宽和高

②、layout用来确定View在父容器中的放置位置

③、draw用于将View绘制到屏幕上

measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasureHeight方法获取到VIew测量后的宽高,几乎所有情况下它都等同于View最终的宽高;

layout过程决定了View的四个顶点的坐标和实际的View的宽高,完成以后,可以通过getTop、getBottom、getLeft、getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法来拿到VIew的最终宽高;

draw过程这决定了View的显示,只有draw方法完成后View的内容才呈现在屏幕上。

DecorView作为顶级View,一般情况下它内部会包含一个竖直方法的LinearLayout,在这个LinearLayout里面有上下两个部分,上面是标题栏titlebar,下面是内容栏

通过源码我们知道,DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后才传递给我们的View。

2、理解View的MeasureSpec

MeasureSpec:测量规格,测量说明书。不管怎么翻译,它看起来都好像是或多或少地决定了View的测量过程。

MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。

在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成为对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高。

1)、MeasureSpec

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小

SpecMode有三类,每一类都表示特殊的含义:

UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。

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

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

②、MeasureSpec和LayoutParams的对应关系

在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。

需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。

对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。

①、当View采用固定宽高是,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式,并且大小是LayoutParams中的大小;

②、当View的宽高是match_parent时,如果父容器的模式是精确模式,那么View也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,并且大小不会超过父容器的剩余空间;

③、当View的宽高是wrap_content时,不管父容器的模式是精确模式还是最大模式,View的模式总是最大模式,并且大小不超过父容器的剩余空间。

④、我们的分析漏掉了UNSPECIFIED模式,那是因为这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。

3、View的工作流程

View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽高,layout确定view的最终宽高和四个顶点的位置,而draw则将view绘制到屏幕上。

1)、measure过程

measure过程要分情况来看,如果是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

①、View的measure过程:

结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,直接在布局中使用wrap_content就相当于使用match_parent。

②、ViewGroup的measure过程:

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法。

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasureWidth和getMeasureHeight方法就可以正确地获取到View的测量宽高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高,在这种情况下,在这种情形下,在onMeasure方法中拿到的测量宽、高很可能是不准确的。一个较好的习惯是在onLayout方法中获取View的测量宽高或最终宽高。

获取View宽高的方法:

①、Activity/View---->onWindowFocusChanged

onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这时候获取宽高是没问题的

  @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus){
            int width = iv_splash_bg.getMeasuredWidth();
            int height = iv_splash_bg.getMeasuredHeight();
        }
    }
②、View.post(runnable)

通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。

 iv_splash_bg.post(new Runnable() {
            @Override
            public void run() {
                int width = iv_splash_bg.getMeasuredWidth();
                int height = iv_splash_bg.getMeasuredHeight();
            }
        });
③、ViewTreeObserver

使用ViewTreeObserver的众多回调可以完成这个功能,例如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的时机,需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。

  ViewTreeObserver treeObserver =iv_splash_bg.getViewTreeObserver();
        treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                iv_splash_bg.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int width = iv_splash_bg.getMeasuredWidth();
                int height = iv_splash_bg.getMeasuredHeight();
            }
        });

2)layout过程

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,onLayout方法则会确定所有子元素的位置。

3)draw过程

Draw过程叫比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:

①、绘制背景background.draw(canvas)

②、绘制自己(onDraw)

③、绘制children(dispatchDraw)

④、绘制装饰(onDrawScrollBars)

4、自定义View

自定义View是一个综合的技术体系,它涉及View的层次结构,事件分发机制和View的工作原理等技术细节。而这些技术细节每一项又都是初学者难以掌握的。

1)自定义View的分类

①、继承View重写onDraw方法

这种方式主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则图形。这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content、并且padding也需要自己处理。

②、继承ViewGroup派生特殊的Layout

这种方式主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局以外,我们需要重新定义一种新布局,当某种效果看起来很像几个View组合在一起时,可以采用这种方式来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

③继承特定的View(如TextView)

这种方式比较常见,一般用于扩展某种已有的View的功能,比如:TextView、这种方式比较容易实现,这种方式不需要自己支持wrap_content和padding等。

④、继承特定的ViewGroup(比如LinearLayout)

这种方式比较常见,当效果看起来很像几种View组合在一起时,可以采用这种方式实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。一般来说方法2实现的效果方法4也都能实现,两者的区别是方法2更加接近View底层。

2)、自定义View须知

这些问题处理不好,有些会影响View的正常使用,而有些则会导致内存泄露

①、让View支持wrap_content

这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。

②、如果有必要,让View支持padding

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

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

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

④View中如果有线程或者动画,需要及时停止,参看View---onDetachedFromWindow

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

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

如果有滑动冲突的话,那么要合适地处理滑动冲突,否则将会严重影响View的效果。

3)、自定义View示例

①、继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法,采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。margin属性有父容器控制。对于直接继承自View的控件,如果不对wrap_content做特殊处理,那么使用wrap_content就相当于match_parent,其次,针对padding的问题,也很简单,只要在绘制时考虑一下padding即可。

最后,为了让我们的View更加容易使用,很多情况下我们还需要提供自定义属性,像android:layout_width和android:layout_padding这种以android开头的属性是系统自带的属性。

①第一步、在values目录下面创建自定义属性的xml。比如attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>
②、第二步,在View的构造方法中解析自定义属性的值并作响应处理

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
        typedArray.recycle();
        init();
    }
③、在布局文件中使用自定义属性

@创建者 :yqlee
 * @时间 :2016/3/25  14:30
 * @描述 :圆形
 */
public class CircleView extends View {

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
        typedArray.recycle();
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取padding
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        //处理padding
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(width / 2, height / 2, radius, mPaint);
    }
}

②、继承ViewGroup派生特殊的Layout

这种方法主要用于实现自定义的布局,采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程。

4、自定义View的思想

自定义View是一个综合的技术体系,很多情况下需要灵活地分析从而找出最高效的方法。核心思想:首先要掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等,这些东西都是自定义View所必须的。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值