自定义View,也可以称为自定义控件,通过自定义View可以使得控件实现各种定制的效果。

  实现自定义View,需要掌握View的底层工作原理,比如View的测量过程、布局流程以及绘制流程,除此之外,还需要掌握View常见的回调方法。而对于那些具有滑动效果的自定义View,我们还需要处理View的滑动,如果遇到滑动冲突则需要处理相应的滑动冲突。

  下面是View的常见回调方法:

  • 构造方法
  • onAttach
  • onVisibilityChanged
  • onDetach
  • onFinishInflate
  • onSizeChanged
  • onMeasure
  • onLayout
  • onTouchEvent

  自定义控件的实现手段可简要分为四种类:

  • 继承View重写onDraw方法,这种方法主要是用于实现一些不规则的效果,采用这种方式需要自己支持wrap_content,并且处理padding。
  • 继承ViewGroup派生特殊的Layout,这种方法主要是用于实现自定义的布局,当某种效果看起来像是几个View组合在一起时,可以采用这种方法来实现。采用这种方法是需要合理处理ViewGroup的测量和布局这两个过程,并同时处理子元素的测量和布局过程。
  • 继承特定的View,用于拓展已有的View的功能。
  • 继承特定的ViewGroup(如LinearLayout、RelativeLayout),其适用情形和方法2 类似。

  在自定义View中需要的注意点:

  应当遵守Android标准控件的规范(如命名、可配置、事件处理、状态保存及恢复等)

  • 命名表意明确
  • 控件属性可以在XML中配置
  • 让View支持wrap_content和padding(下文会具体讲到)
  • 在View中尽量不使用Handler,因为View中自带post系列的方法。
  • 自定义View的内存泄漏问题(如果有线程或者动画,需要及时停止)
  • View的滑动冲突(在View带有滑动嵌套的情形,需要处理好滑动冲突)
  • 具有一定的交互性,如按下、点击等
  • 自定义View内部实现状态保存和恢复的机制
  • 兼容性

  下面主要从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 void onMeasure(int widthMeasureSpec, 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)

  • match_parent 不能
  • 具体值和wrap_content可以。

4.Layout过程

Layout过程用于ViewGroup确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素,并调用其layout方法,在layout方法中onLayout方法又会被调用。

layout方法首先通过setFrame方法俩设置view的四个顶点的位置,接着调用onLayout方法,确定子元素的位置。

由于onLayout的实现同样与布局有关,因此View和ViewGroup均没有实现onLayout方法。

5.draw过程

  • 将View绘制到屏幕上,大概的几个步骤
    1.绘制背景background.draw(canvas)
    2.绘制自己(onDraw)
    3.绘制children(dispatchDraw)
    4.绘制装饰(onDrawScrollBars)
  • View的绘制过程是通过dispatchDraw来实现的,它会遍历所有子元素的draw方法。
  • 如果一个View不需要绘制任何内容,那么设置setWillNotDraw为true后,系统会进行相应的优化;ViewGroup默认为true,如果我们的自定义ViewGroup需要通过onDraw来绘制内容的时候,需要显示的关闭它。

Android学习笔记View的工作原理的更多相关文章

  1. [Android学习笔记]理解焦点处理原理的相关记录

    焦点处理相关记录 以下所涉及的焦点部分,只是按键移动部分,不明确包含Touch Focus部分 需解决问题 控件的下一个焦点是哪? 分析思路 当用户通过按键(遥控器等)触发焦点切换时,事件指令会通过底 ...

  2. [Android学习笔记]View的measure过程学习

    View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...

  3. [Android学习笔记]View的draw过程学习

    View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...

  4. [Android学习笔记]view的layout过程学习

    View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...

  5. 《Android开发艺术探索》读书笔记 (4) 第4章 View的工作原理

    本节和<Android群英传>中的第3章Android控件架构与自定义控件详解有关系,建议先阅读该章的总结 第4章 View的工作原理 4.1 初始ViewRoot和DecorView ( ...

  6. Android艺术开发探索第四章——View的工作原理(下)

    Android艺术开发探索第四章--View的工作原理(下) 我们上篇BB了这么多,这篇就多多少少要来点实战了,上篇主席叫我多点自己的理解,那我就多点真诚,少点套路了,老司机,开车吧! 我们这一篇就扯 ...

  7. Android艺术开发探索第四章——View的工作原理(上)

    这章就比较好玩了,主要介绍一下View的工作原理,还有自定义View的实现方法,在Android中,View是一个很重要的角色,简单来说,View是Android中视觉的呈现,在界面上Android提 ...

  8. View的工作原理(一) 总览View的工作流程

    View的工作原理(一) 总览View的工作流程 学习自 <Android开发艺术探索> 简书博主-丶蓝天白云梦 Overview 从本章开始,开始学习View的工作原理,包括View的 ...

  9. 【转】 Pro Android学习笔记(七四):HTTP服务(8):使用后台线程AsyncTask

    目录(?)[-] 5秒超时异常 AsyncTask 实现AsyncTask抽象类 对AsyncTask的调用 在哪里运行 其他重要method 文章转载只能用于非商业性质,且不能带有虚拟货币.积分.注 ...

随机推荐

  1. 关于DCL的使用

    DCL1 创建用户语法:CREATE USER 用户名@地址 IDENTIFIED BY '密码';CREATE USER user1@localhost IDENTIFIED BY '123'; C ...

  2. Virtualbox 复制 CentOS 虚拟机无法联网

    Centos刚装好后无法联网 复制虚拟机后,出现 No such device eth0 我们要处理的三个问题: 在Virtualbox上安装好Centos后如何联网 如何在Virtualbox上复制 ...

  3. Latex 公式在线可视化编辑器

    寻觅 最近的一个demo需要用到Latex公式在线编辑器,从搜索引擎一般会得到类似http://latex.codecogs.com/eqneditor/editor.php的结果,这个编辑器的问题在 ...

  4. NODEJS环境搭建 第一篇 安装和部署NODEJS

    一.下载安装文件 根据自己当前系统环境,下载相对应的安装文件 https://nodejs.org/en/download/ 二.双击安装 都傻瓜式的安装步骤,一步一步安装就好了. 三.检查安装结果 ...

  5. 【uwp】浅谈China Daily中数据同步到One Drive的实现

    新版China Daily与旧版相比新增了数据同步的功能,那这个功能具体是如何实现的呢,现在让我们来一起看看. 1.注册应用 开发者中心的应用注册就不用多说了(https://developer.mi ...

  6. node-ejs-mongodb结合的项目案例-----引用mongoose和未引用mongoose模块

    本项目个人尝试了2种方法,一个是直接用mongod,一个是引用mongod里的mongoose. nodejs-ejs-mogondb- nodej+ejs模板,通过mogondb数据查询数据实现简单 ...

  7. python——网络编程

    Socket socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. sock ...

  8. sed命令详解-应用篇

    本篇从实用的角度讲解sed,关于sed的详细帮助文档,请参考前篇 http://www.cnblogs.com/the-capricornus/p/5279979.html 本篇用到的选项请参考前篇. ...

  9. linux的大小端、网络字节序问题 .

    1.80X86使用小端法,网络字节序使用大端法. 2.二进制的网络编程中,传送数据,最好以unsigned char, unsigned short, unsigned int来处理, unsigne ...

  10. google ip地址

    http://203.208.46.146 http://203.208.46.177 http://203.208.46.178 http://209.116.186.251 http://203. ...