自定义 View,也可以称为自定义控件,通过自定义 View 可以使得控件实现各种定制的效果。
实现自定义 View,需要掌握 View 的底层工作原理,比如 View 的测量过程、布局流程以及绘制流程,除此之外,还需要掌握 View 常见的回调方法。而对于那些具有滑动效果的自定义 View,我们还需要处理 View 的滑动,如果遇到滑动冲突则需要处理相应的滑动冲突。
下面是 View 的常见回调方法:
自定义控件的实现手段可简要分为四种类:
在自定义 View 中需要的注意点:
应当遵守 Android 标准控件的规范(如命名、可配置、事件处理、状态保存及恢复等)
下面主要从 View 的基本知识、View 的绘制过程讲一下 View 的工作原理。
1. 从 Activity 中的 View 结构讲起
每个 Activity 都含有一个 Window 对象,而这个 Window 对象一般都是 PhoneWindow。PhoneWindow 将以 DecorView 设置为整个应用窗口的根 View。DecorView 作为窗口界面的顶层视图,封装了一些窗口操作的通用方法。可以这么说,DecorView 将要显示的具体内容呈现在了 PhoneWindow 中,这里面的所有的 View 的监听事件都是通过 WindowManagerService 来接收的,并通过 Activity 对象来回调相应的 onClickListenr。
在显示上,将屏幕分成两部分,一个是 TitleView,另一个是 ContentView,这个 ContentView 想必大家都很熟悉,它是一个 ID 为 content 的 FrameLayout,activity_main 就是设置在这样一个 FrameLayout 中。
如下图 1 和图 2 所示:
图 1
图 2
View 的绘制流程:
图 3
如上图所示,performTraversals 会依次调用 performMeasure、performLayout、performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout、draw 这三大流程。
其中在 performMeasure 中又会调用 measure,接着在 measure 中调用 onMeasure 方法,在 onMeasure 中会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,即完成依次 measure 操作,接着子元素进行同样的 measure 过程,如此方法直至完成整个 View 树的遍历。同理,performLayout 和 performDraw 的传递流程和 performmeasure 是类似的(performDraw 的传递过程是在 draw 方法中的 dispatchDraw 完成的,并无实质区别)。
measure 过程决定了 View 的宽高, measure 完成后,可以通过 getMeasuredWidth 和 getMeasuredHeight 方法来获取到 View 测量后的宽高,在几乎所有的情况下它都等同于 View 的最终高度,但特殊情况除外。Layout 过程确定了 View 的四个顶点的坐标和实际的 View 的宽高,完成以后,可以通过 getTop、getBottom、getLeft、getRight 来得到四个顶点的位置,并可以通过 getWidth 和 getHeight 来得到 View 的最终宽高。Draw 过程决定了 View 的显示,只有 draw 方法完成后,View 的内容才会显示在屏幕上。
2. 如何完成测量过程呢?
Android 系统提供了一个 MeasureSpec 类,通过它可以帮助我们测量 View。MeasureSpec 是一个 32 位的 int 值,其中高 2 位为测量的模式,低 30 位为测量的大小。
EXACTLY:精确值模式, 当我们将空间的 layout_width 或者 layout_height 属性指定为具体值时,或者指定为 match_parent 属性时,系统使用的是 EXACTLY。
AT_MOST:最大值模式,当空间的 layout_width 属性或者 layout_height 属性为 wrap_content 时,控件大小一般随着空间的子控件或者内容的变化而变化,此时,控件的尺寸只要不超过父控件允许的最大尺寸即可。
UNSPECIFIED:不指定其测量大小,通常情况下在绘制自定义 View 时才会使用它。
在 view 的测量过程中,系统会将 LayoutParams 在父容器的约束下转换为对应的 MeasureSpec, 然后根据这个 MeasureSpec 来确定 View 测量后的宽高。MeasureSpec 由父容器和 LayoutParams 共同决定。
对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 决定
对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 决定
当 View 的 LayoutParams 采用精确值时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 模式都是 EXACTLY,并且大小遵循 LayoutParms 的大小。
当 View 的宽高是 match_parent 模式,view 的 MeasureSpec 模式遵循父容器的 MeasureSpec 模式。
当 View 的宽高是 wrap_content,不管父容器的模式是 EXACTLY 还是 AT_MOST,View 的模式都是 AT_MOST 并且大小不超过父容器的剩余空间。
下面分别简要讲一下 View 的 measure 过程和 ViewGroup 的 measure 过程。
1)View 的 measure 过程:
参考源码:
- protected voidonMeasure(intwidthMeasureSpec,int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
图 4
由上述源码可知,在调用 onMeasure 方法时会调用 setMeasuredDimension 方法,在这个方法中会传入其宽高。由此可知,在自定义 View 中,需要重新定义 view 的宽和高。
View 类默认的 onMeasure 方法只支持 EXACTLY 模式,如果在自定义控件的时候不重写 onMeasure 方法,就只能使用 EXACTLY 模式。控件可以相应你指定的具体宽高值或者 match_parent 属性,如果要让自定义 View 支持 wrap_content 属性,则必须要重写 onMeasure 方法,否则在布局中使用 wrap_content 就相当于使用 match_parent)
2)ViewGroup 的 measure 过程:
对于 ViewGroup 而言,除了完成自己的 measure 过程,还要遍历去调用所有子元素的 measure 方法,各个子元素再递归执行这个过程。
ViewGroup 是一个抽象类,它没有重写 View 的 onMeasure 方法,但它提供了一个 measureChildren 的方法,在 measureChildren 方法中它会遍历 ViewGroup 中的子元素,并调用 measureChild 方法,对子元素进行 measure。measureChild 的思想就是取出子元素的 LayoutParams,然后通过 getChildMeasureSpec 来创建子元素的 measureSpec,最后将子元素的 measureSpec 传递给 measure 方法就能完成测量,如下图所示:
图 5
正如前面提到的 ViewGroup 是一个抽象类,它没有重写 onMeasure 方法,其测量过程中的 onMeasure 需要其子类去具体实现。如 LinearLayout、RelativeLayout。不同的 ViewGroup 子类的布局特性不同,这也导致其测量细节不同。
下面简要了解一下 LinearLyaout 和 RelativeLayout 的 onMeasure 实现
1)LinearLayout 的 Measure 实现:
LinearLayout 的布局方向有两种,所以 LinearLayout 会根据 mOrientation 来分别调用 measureVertical 或者是 measureHorizontal。以水平布局为例,
遍历所有的 view,跳过为 null 或者属性为 View.GONE 的,加上分割线宽度 mDividerWidth 和左右 margin,计算所有 View 的 childWidth 之和 mTotalLength, 统计所有 View 的 weight 和 totalWeight,并且对子 view 进行测量。
2)RelativeLayout 的 Measure 实现:
当第一次执行 onMeasure 或者 requestLayout 后,需要调用 sortChildren 方法,根据添加顺序对所有的子 view 进行排序,横着一次,竖着一次,然后对两个序列进行检查,通过依赖图静态类中的 getSortedViews 方法根据依赖关系进行排序。
之后在 onMeasure 中,对子 view 进行遍历,即对两个序列进行分别遍历。
首先是横向遍历,调用 mSortedHorizontalChildren,获取 RelativeLayout.layoutParams, 并依次调用方法,计算控件的横向位置及 mLeft 和 mRight,然后横向测量子 View,接下去根据前面的结果很想摆放子 View,如果此时父 RelativeLayout 的宽度是 WRAP_CONTENT,会在此时对宽高进行修正。
横向完毕后进行垂直排列的 View 序列进行上述操在,步骤大致相同,在此处会对子 view 进行 measure 时就会正确的测量,之后的操作就是对父 RelativeLayout 的宽高等属性进行再次修正。
从上面的分析中,一个最明显的不同就是 RelativeLayout 在进行 measure 过程中需要进行两次遍历,而 LinearLayout 则只需要一次遍历过程。
此外,需要注意的是,在某些极端情况下,系统可能需要调用多次 measure 才能确定最终的测量宽高,在这种情况下,在 onMeasure 方法中拿到的测量高很可能是不准确的。所以最好在 onLayout 方法中获取 View 的高宽。
3. 如何获取 View 的宽和高
(1)调用 onWindowFocusChanged 方法(焦点变化),这个时候 View 已经初始化完毕,这个时候去获取 View 的宽高是没有问题的。然而当频繁进行 onResume 和 onPause,onWindowFocusChanged 方法也会被频繁调用。
(2)调用 view.post(runnable)
通过 post 将一个 Runnable 投递到消息队列的尾部,然后等待 Looper 调用此 Runnable,view 也已经初始化好了。
(3)ViewTreeObserver
使用 ViewTreeObserver 的众多回调可以使用这个功能,如 OnGlobalLayoutListener,当 View 树的状态发生改变或者 View 树的 View 的可见性发生改变时,OnGlobalLayoutListener 会被回调,需要注意的是,伴随着 View 树状态的改变,onGlobalLayoutListener 会被回调多次。
(4)View.measure(int widthMeasureSpec,int heightMeasureSpec)
4.Layout 过程
Layout 过程用于 ViewGroup 确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素,并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。
layout 方法首先通过 setFrame 方法俩设置 view 的四个顶点的位置,接着调用 onLayout 方法,确定子元素的位置。
由于 onLayout 的实现同样与布局有关,因此 View 和 ViewGroup 均没有实现 onLayout 方法。
5.draw 过程
来源: http://www.cnblogs.com/hustzhb/p/6926010.html