这周敲完了《第一行代码》的最后一个项目酷欧天气,然后开始,第二本书《Android群英传》的敲代码之旅。虽然说项目驱动才是最好的学习方式,但照书敲然后思考,也不失为一种好办法。算是迈出Android进阶的第一步吧。
Android控件架构
Android里的控件,大致被分为两类,ViewGroup 和 View
从字面上来看,ViewGroup就是一组View的集合,可以通过它来管理控制其包含的View。通过它,界面上的所有控件也就能够形成一个树形结构,也就是常说的控件树,上层控件负责下层子控件的测量与绘制,并且传递交互事件。我们在Activity里经常使用的findViewById()方法,就是在控件树中通过DFS来查找的(数据结构还是很有用的)。而对于每一颗控件树,都有一个ViewParent对象,这就是整颗树的控制核心,所有的交互管理事件都由它来进行统一调度和分配。大概就是下面这张图的样子:
还记得写Activity的时候都要用setContentView()方法来加载布局,之后才会显示其中的内容,那这个方法究竟干了什么?首先要看Android的界面架构图:
每个Activity对象都包含一个Window对象,一般由PhoneWindow来实现。PhonwWindow将DecorView设置为整个应用窗口的根View。DecorView封装了一些窗口操作的通用方法,DecorView将显示的具体内容呈现在了PhoneWindow上,其中所有View的监听事件都是通过WindowManagerService来接收,并且通过Activity对象来回调相应的onClickListener。
—— 《Android群英传》
其实这里最最简单的例子就是Button的点击监听,屏幕将点击事件传递给WindowManagerService,然后调用了我们在Button上写的setOnClickListener()方法来实现点击的逻辑。
这里书中提到了两个需要注意的点:
- requestWindowFeature()方法一定要在setContentView()方法之前调用,否则会无效(我就踩过这个坑),原因是 一般的Window布局由TitleView和ContentView构成,如果要设置一些特性(比如NO_TITLE),应该要在加载ContentView之前进行设置,否则,界面已经加载了TitleView,再调用方法也就会无效
- 程序在onCreate()方法中调用了setContentView()后,ActivityManagerService会回调onResume()方法,此时系统才会把整个DecorView添加到PhoneWindow中,并且让它显示出来。
View的绘制
在自定义View之前,我们需要知道View是怎么绘制出来的。
主要操作的有三个方法onMeasure()
、onDraw()
、onLayout()
其实也很好理解,就和一个人要画画一样,他首先要知道画的东西的大小(onMeasure()负责测量大小),然后是画在画布的哪里(onLayout()指导他画在那里),再就是怎么画了(onDraw()方法来告诉它怎么画)。
onMeasure()方法
Android系统为我们的测量提供了一个类MeasureSpec
,通过它来帮助我们测量View。MeasureSpec是一个32位的int,其中高2位代表测量的模式,利用位运算可以提高效率。
测量的模式分为3种:
- EXACTLY 精确值模式,当使用固定数值或者指定为match_parent时使用
- AT_MOST 最大值模式,控件指定为warp_content时,随着控件大小变化而变化,不超过父控件允许的最大尺寸即可
UNSPECIFIED 未明确模式,主要用在自定View的绘制中
View类默认的onMeasure()方法只支持EXACTLY模式,这就意味着如果要使用其他模式就必须要重写该方法。如果要让自定义View支持warp_content属性,就要在onMeasure()方法中来指定warp_content时的大小。
另外如果仔细研究onMeasure()方法,会发现系统最终调用setMeasuredDimension()方法来吧测量后的宽和高交给view。
总而言之,我们通过MeasureSpec这一个类能够获取View的测量模式和绘制的大小,从而控制其显示时候的大小。
onLayout()方法
控件的onLayout()方法一般都是通过实现其父控件(一般也就是一个ViewGroup)的onLayout()来实现的。
1 |
|
在自定义View中,onLayout()配合onMeasure()方法一起使用,可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法,动态获取子View和子View的测量大小,然后进行layout布局。
onDraw()方法
画画首先需要的材料就是画布和画笔。而在Android之中正好就有两个系统2D绘图API对象Canvas和Paint。
Canvas就是我们的画布,不过我们在创建的时候,需要传入一个Bitmap对象
1 | Canvas canvas = new Canvas(bitmap); |
传进去的Bitmap对象和Canvas画布是紧密相连的,这个过程我们称之为装载画布。Canvas.drawXXX方法都发生在这个bitmap上,bitmap储存所有绘制在Canvas上的像素信息。
bitmap承载Canvas的一系列绘图操作,图形的改变是通过Bitmap的改变,然后让View重新绘制来实现的
Paint类就是一只画笔,接下来我们来结合一个自定义View的实例来看Paint和相关方法的使用。
自定义View实例 流光TextView
实现自定义View大致有三种方法:
- 对现有控件进行拓展
- 通过组合来实现新的控件
- 重写View来实现全新的控件
这里就拿书本上的一个简单的例子,使用第一种方法拓展TextView,实现一个
流光字的TextView
既然是拓展,第一反应就是继承TextView,然后重写相关的方法。没错,就是这么简单。
1 |
|
在这个类里我们定义了一些成员变量,暂时先不管,然后写了一个构造方法,很简单,这就是一个简单的继承嘛。如果我们不去重写相关的方法,那么这个类和TextView其实是没有任何区别的。
要实现流光字,利用Paint对象的Shader渲染器。通过设置不断变化的LinearGradient(线性渐变),然后用带有该属性的Paint来绘制文字(就好比在画笔上沾了一些特殊的颜料,画出来自然是带特效的)。我们先要在onSizeChanged()方法中做一些初始化:
1 |
|
然后,我们在onDraw()方法中不断用矩阵平移渐变效果,就可以实现流光字了。(和我以前在ps里面制作流光字的讨论差不多)
1 |
|
效果还是很不错哒,如下所示:
初探旅程就到这,还有两种方法,等待探索。