4_View的工作原理

View的底层工作原理,比如View的测量,View的绘制,View的布局。除了View的三大流程,View的常见的回调方法也是需要熟练的,比如构造方法,onAttach、onVisibilityChanged、onDetach等。

4.1 初识ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的,在ActivtityThread中,当ActivityThread中,当Activity对象被创建完毕后。会将DecorView添加Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

View的绘制流程是从ViewRoot的performTraversals方法开始的。

4.2 理解MeasureSpec,MeasureSpec在很大程度上决定了一个Veiw的尺寸规格,之所以说很大程度上是因为这个过程还要受到父容器的影响,在测量过程中系统会将View的LayoutParams根据父容器的规则转换成对应的MesureSpec,然后再根据这个measureSpec来测量出View的宽/高。

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

SpecMode有三类:

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

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

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

对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,针对不同的父容器和View本身不同的LayoutParams,Veiw可以有多种MeasureSpec。当View采用固定宽高的时候,不管父容器的MeaureSpec是什么,View的MesureSpec都是精确模式并且其大小遵循Layoutparams中的大小。当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也是精确模式并且大小是父容器的剩余空间;如果父容器的模式是最大模式,那么View也是最大模式并且其大小是父容器的剩余空间。当View的宽/高是wrap_content时,不管父容器的模式是精准模式还是最大模式,view都是最大模式并且大小不能超过父容器的剩余空间。

总结:只要提供父容器的Mesurespec和view的Layoutparams就可以确定出view的MesureSpec了,有了MesureSpec就可以确定出view的测量后的大小了。

4.3.1 measure过程

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

这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的。但是几乎所有情况下的View的测量大小和最终大小是相等的。 至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size.

Veiw的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredWidth/Height方法可以正确获取到View的测量宽/高。但是在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高,在onMeasure方法中拿到的测量宽/高很可能是不准确的。在onLayout中获取View的宽高是最终宽高。

在onCreate、onStart、onResume中均无法正确得到某个View的宽/高,因为View的onMeasure过程和activity的生命周期不是同步执行的,因此无法保证activity执行onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕那么View的获得的宽高就是0。

解决方案:

(1)Activity/View#onWindowFoucusChanged

这个方法的含义就是View已经初始化完毕了,这个时候去获取宽/高是没问题的。需要注意:onWindowFoucusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次,具体来说,当activity继续执行和暂停执行时,onWindowFoucusChanged均会被调用,如果频繁的进行onResume和onPause,那么onWindowFoucusChanged也会被频繁的调用

(2)view.post(runnable)

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

view.post(new Runnable(){
    @Override
    public void run(){
        int width = view.getMeasureWidth();
        int height = view.getMeasureWidth();
    }
})

(3)ViewTreeObserver

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

(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)

通过手动对View进行measure来得到View的宽高需要根据View的LayoutParams来说分。

4.3.2 layout过程

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

父元素在layout方法中完成自己的定位以后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层地传递下去就完成了整个View树的layout过程。

View的测量宽/高和最终宽/高有什么区别?也可以具体为:View的getMeasureWidth 和getWidth这两个方法有什么区别?

在View的默认实现中,View的测量宽/高和最终测量宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程,即两者的赋值时机不同。但是存在某些特殊情况将会导致两者不一致。

重写View的layout方法:

public void layout (int i,int t, int r,int b){
    super.layout(i,t,r,b);
}

上述代码会导致在任何情况下View的最终宽高总是比测量宽高大100px。还有一些情况是View需要多次measure才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高可能和最终宽高不一致。

4.3.3 draw过程

draw过程比较简单,它的作用是将View绘制到屏幕上面,View的绘制过程遵循如下:

(1)绘制背景 background.draw(canvas)

(2)绘制自己(onDraw)

(3)绘制children (dispatchDraw)

(4)绘制装饰(onDrawScroolBars)

View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。View有一个特殊的方法

setWillNotDraw:如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化,默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开放的意义是:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行相应的优化,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,需要显示关闭WILL_NOT_DRAW。

4.4.1 自定义View的分类

1 继承View重写onDraw方法

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

2 继承ViewGroup派生特殊的layout

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

3 继承特定的View(比如TextView)

这中方法比较常见,一般用于扩展某种已有的View的功能,比如TextView。

4 继承特定的ViewGroup(比如LinearLayout)

这种方法比较常见,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法。

4.4.2 自定义View的须知:

1 让View支持wrap_content

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

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

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

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

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

4 View中如果有线程或者动画,需要及时停止

如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机,当包含此View的activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow。

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

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

第一步: 在values目录下面创建自定义属性的XML,比如attrs.xml也可以选择类似于attrs_circle_view.xml等这种以attrs开头的文件名,这个文件名并没有什么限制,可以随便取名字。如下:

<resources>
  <declare-styleable name="CircleView">
  <attr name="circle_color" format="color"/>
  </declare-styleable>
</resources>

第二步: 在View的构造方法中解析自定义属性并做相应的处理.

TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
a.getColor(R.styleable.CircleView_circle_color,Corlor.RED);
a.recycle();
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值