一步一步走进 自定义View

View简介

1.View原理及其子类介绍

View是Android UI组件的基类,ViewGroup是容纳UI组件的容器,ViewGroup本身也是从View派生出来的。Android视图,是类似于Dom树的架构。父视图负责测量定位绘制等操作。我们经常在用的findViewById 方法代价昂贵的原因,就是因为他负责至上而下遍历整棵控件树,来寻找View实例,在重复操作中尽量少用。现在在用的很多控件都是直接或者间接继承自View的,如下图:


View与GroupView的子类详尽继承关系图分析见:Android View与GroupView原理以及其子类描述
2.Android UI界面架构
每个Activity包含一个PhoneWindow对象,PhoneWindow类继承了Window类,PhoneWindow类有两个重要的成员变量mDecor和mContentParent,它们的类型分别DecorView和ViewGroup。其中,成员变量mDecor是用描述自己的窗口视图,而成员变量mContentParent用来描述视图内容的父窗口。PhoneWindow设置DecorView为应用窗口的根视图,再里面就是熟悉的TitleView和ContentView。TitleView就是在很多界面顶部显示的那部分内容,可以在代码中控制让它是否显示没错,而ContentView就是一个FrameLayout,这个布局的id叫作content,平时使用的setContentView()就是设置的ContentView,调用setContentView()时所传入的布局其实就是放到这个FrameLayout中的。

DecorView类所描述的应用程序窗口视图是否需要重新绘制是由另外一个类ViewRoot来控制的,ViewRoot类继承于Handler类。系统在启动一个Activity组件的过程中,会为这个Activity组件创建一个ViewRoot对象,同时还会将前面为这个Activity组件所创建的一个PhoneWindow对象的成员变量mDecor所描述的一个视图(DecorView)保存在这个ViewRoot对象的成员变量mView中。这样,这个ViewRoot对象就可以通过调用它的成员变量mView的所描述的一个DecorView的成员函数draw来绘制一个Acitivity组件的UI了。ViewRoot类的作用是非常大的,它除了用来控制一个Acitivity组件的UI绘制之外,还负责接收Acitivity组件的IO输入事件,例如,键盘事件。


View的绘制过程

由以上介绍可以知道,Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View的,如TextView、Button、ImageView、ListView等。这些控件虽然是Android系统本身就提供好的,我们只需要拿过来使用就可以了,那它们又是怎样被绘制到屏幕上的呢?任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw(),三个阶段的详尽分析: Android视图绘制流程完全解析 View(二)
附带LayoutInflater的分析: Android LayoutInflater原理分析 View(一)

自定义View

1.为什么要自定义View?
  • 现有的View满足不了你的需求,也没有办法从已有控件派生一个出来;界面元素需要自己绘制;
  • 现有View可以满足要求,把它做成自定义View只是为了抽象:为这个自定义View提供若干方法,方便调用着操纵View。通常做法是派生一个已有View,或者结合xml文件直接inflate;
  • 在多个应用并行开发的团队,将公用的交互效果提取成自定义控件,方便复用,减少不必要的重复劳动。

常见的Android自定义View主要有两种类型:

  • 组合控件:通过Android的基础控件(TextView、CheckBox、Button、ProgressBar等)组合而成,比如试题控件(TextView+VideoGroup)、下拉刷新、瀑布流控件、带左/右滑功能的控件、视频控件等,这种自定义View的难点在于程序的逻辑处理;
  • 完全自定义控件:继承自View、TextureView或SurfaceView,然后重写核心的回调方法,以View为例,按需复写其构造函数、onMeasure、onLayout、onTouchEvent、onDraw、onAttachedToWindow、onDetachedFromWindow等方法,这种自定义View的难点在于程序的设计、效率优化和排版,比如输入法中的手写控件、图文混排控件(现在很多都是通过webview加载网页实现了)、词典取词控件、图表控件、个性化进度条、弹幕显示控件、Markdown控件、IDE代码编辑控件等。
2.基础知识点储备

(1)了解下核心知识点View、SurfaceView、TextureView的区别:

  • View:普通View,与宿主窗口共享同一个绘图表面,UI在主线程中绘制,在有无硬件加速的情况下都能工作(没有硬件加速时,canvas的有些方法会失效);
  • SurfaceView:继承自View,绘制和显示效率高,因为拥有独立的绘图表面,UI在一个独立的线程中进行绘制,不会占用主线程的资源。-
    SurfaceView的使用和普通的View不一样,需要结合SurfaceHodler一起使用。因为和宿主窗口不是共享同一个绘图表面的原因,笔者在实际使用SurfaceView的过程中发现对其做动画操作会达不到想要的效果(一坨黑);
  • TextureView:继承自View,与SurfaceView相比,TextureView不会创建一个单独的绘图表面,这使得它可以像一般的View一样执行一些变换操作,比如移动、动画等等,但TextureView必须在硬件加速开启的窗口中才能正常工作。

(2)自义定属性;
对于自定义View的一些属性设置,除了可以在自定义View中提供公开接口外,还可以通过自定义属性,在对自定义View布局时就指定,这样可以简化用户使用控件的复杂度,实现自定义属性的步骤如下:

  • 在res/values文件夹下新建一个attrs.xml的文件,在里面定义自定义属性的ID、属性和属性对应的类型
<declare-styleable name="TipView">
    <attr name="singleLine" format="boolean"/>
    <attr name="styleMode">
        <flag name="white" value="1" />
        <flag name="orange" value="2" />
        <flag name="front" value="3" />
    </attr>
    <attr name="showMore" format="boolean" />
</declare-styleable>  
  • 在自定义View带attrs参数的构造方法中解析自定义属性值
int styleMode;
boolean singleLine = false;
boolean showMore = false;
if (null != attrs) {
    TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.TipView);
    styleMode = tArray.getInt(R.styleable.TipView_styleMode, STYLE_MODE_WITHE);
    singleLine = tArray.getBoolean(R.styleable.TipView_singleLine, false);
    showMore = tArray.getBoolean(R.styleable.TipView_showMore, false);
    tArray.recycle();
}

对自定义属性的解析需要注意两点:
a. TypedArray使用后一定要调用其recycle方法,否则会有内存泄露的问题;
b. 如果自定义View在一个单独的module中(不属于主工程),对attr的获取不能使用switch-case语句,要用if...else,具体原因之前有介绍过,详见:在Android library中不能使用switch-case语句访问资源ID的原因分析及解决方案

(3)SpannableString
可以通过它将同一串字符中的不同文字做不同的处理,比如某些文字的颜色、字体、背景色、大小等有变化,都可以通过它来设置,熟练掌握SpannableString对于灵活自定义View会有很大地帮助。

3.自定义View的步骤又是什么?

先看张自定义View的函数调用流程图:


其中需注意的是(具体分析见:安卓自定义View进阶 - 分类和流程):

  • 几种重载的构造函数,在什么情况下调用哪种构造函数
  • 如何自定义属性和使用attrs
  • 几个重要的函数onMeasure、onLayout、onDraw等

继续深一步的学习:

布局性能优化

在定义布局时,难免会有一些不必要的嵌套和View节点,那怎样优化减少不必要的infalte呢?
使用抽象布局标签(include, viewstub, merge)可进行相应的优化,还可以使用一些布局调优相关工具(hierarchy viewer和lint)等。具体分析见:性能优化之布局优化布局技巧:合并布局

  • <include/>标签:常用于将布局中的公共部分提取出来供其他layout共用,以实现布局模块化,这在布局编写方便提供了大大的便利。
    include标签唯一需要的属性是layout属性,指定需要包含的布局文件。可以定义android:id和android:layout_*属性来覆盖被引入布局根节点的对应属性值。
  • <viewstub/>标签:viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,即既不会占用显示也不会占用位置,从而在解析layout时节省cpu和内存。 viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。
  • <merge/>标签:使用了include后可能导致布局嵌套过多,多余不必要的layout节点,从而导致解析变慢,不必要的节点和嵌套可通过hierarchy viewer或设置->开发者选项->显示布局边界查看。merge标签在UI的结构优化中起着非常重要的作用,它可以删减多余的层级,优化UI。 merge标签可用于两种典型情况:
    a. 布局顶结点是FrameLayout且不需要设置background或padding等属性,可以用merge代替,因为Activity内容视图的parent view就是个FrameLayout,所以可以用merge消除只剩一个。
    b. 某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,832评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,733评论 22 665
  • 自从选择工作转型后,遇到过大大小小的困难,都咬着牙克服过来。从心底里觉得那些困难都不算什么事儿,因为我始终相信自己...
    岛屿书阅读 393评论 0 2
  • 昨天把学习的随记分享后,居然有人隐名打赏支持,有一位的金额还不少,像突发事件让我慌了,自己只是为了学习,摘抄了一敏...
    青可路香阅读 139评论 0 1
  • 你知道18岁和19岁的过渡间隙是什么样的吗?你知道18岁的你对19岁有什么想象吗?你知道听见梦里花开的声...
    仄平阅读 168评论 0 1