自定义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. 【redis专题(8)】命令语法介绍之通用KEY

    select num 数据库选择 默认有16[0到15]个数据库,默认自动选择0号数据库 move key num 移动key到num服务器 del key [key ...] 删除给定的一个或多个 ...

  2. 前端代码组织优化--小demo(进阶你的思路)

    事出必有因 最近在看老项目的代码,一个富客户端的js代码,几千行的代码,全是function(){} var...的垂直布局,真的是要感动的哭了. 一开始都是这样,想实现什么功能,不管三七二十一,fu ...

  3. CSharpGL(41)改进获取字形贴图的方法

    CSharpGL(41)改进获取字形贴图的方法 在(http://www.cnblogs.com/bitzhuwei/p/CSharpGL-28-simplest-way-to-creating-fo ...

  4. swust oj(0088)表达式的转换

    表达式的转换(0088) Time limit(ms): 5000 Memory limit(kb): 65535 Submission: 435 Accepted: 93 Accepted 16级卓 ...

  5. 转账示例(一):Dao层面实现(本例采用QueryRunner来执行sql语句,数据源为C3P0)

    缺点:Dao层面把Service层面的操作完成了,不利于后期的代码修改和重构 1.自行创建C3P0Util account数据库 2.jar包 3.Dao层面 接口: package com.lear ...

  6. JS取消浏览器文本选中的方法

    一 .问题的出现 今天在使用Easy-UI 的messager.alert()方法时候出现浏览器文本被选中,不知道其中是什么原因,如下图所示. 二 .解决思路 我最后的思路时在弹出消息框的同时,取消浏 ...

  7. 关于mpu6050的几个很好的帖子

    最近在研究6050,真是很磨人啊,这个小东西还挺复杂,一个读取程序竟然需要600多行. 这几天连查资料找到了几个很好的帖子,要是以后有人看到这篇帖子,可以避免误入歧途,也可以省去很多时间. 1.阿西莫 ...

  8. webmagic源码学习(一)

    最近工作主要是一些爬虫相关的东西,由于公司需要构建自己的爬虫框架,在调研过程中参考了许多优秀的开源作品,包括webmagic,webcollector,Spiderman等,通过学习这些优秀的源码获益 ...

  9. SQLite数据库_实现简单的增删改查

    1.SQLite是一款轻量型的数据库是遵守ACID(原子性.一致性.隔离性.持久性)的关联式数据库管理系统,多用于嵌入式开发中. 2.Android平台中嵌入了一个关系型数据库SQLite,和其他数据 ...

  10. python基本数据类型——tuple

    一.元组的创建与转换: ages = (11, 22, 33, 44, 55) ages = tuple((11, 22, 33, 44, 55))ages = tuple([]) # 字符串.列表. ...