本文由全栈大佬Pony出品,小弟我只负责打打杂。对本文有任何意见或建议,非常欢迎留言交流。
View,几乎是所有界面系统中的基类,在iOS里面是UIView,在Android里是View。 那么,到底View是什么东西,他做了些什么,他是怎么做到的,在这篇文章中,希望能带给大家一些启发。
抽象
View实际上是一个抽象类,他负责对渲染、布局以及触摸事件进行抽象。
渲染抽象
我们知道,不管是 iOS 还是 Android,他们的渲染引擎都是 OpenGL,OpenGL是面向C语言的(当然,在Objective-C和Java中都有封装)。而作为前端开发者,要直接使用OpenGL编写界面,真是不(Tai)现(Nan)实(Le)。于是,我们有了界面库,这种界面库,在iOS上,我们称之为UIKit,在Android上,我们使用android.view.*包。不管是iOS还是Android,界面库要做的事情,目标都是一致的,那就是将界面渲染从具体变成抽象。
布局抽象
布局,是View最重要的特性,诸如层级控制、矩形大小、Matrix变换都属于布局抽象的范畴,布局是渲染、触摸两者的基础,缺少布局,渲染和触摸便无法继续。
触摸事件抽象
除了渲染、布局以外,View还需要承担另外一个职责————触控。所谓触控,有触才有控,一方面View要负责接收触摸事件,另一方面View要负责反馈接收到的触摸事件,至于具体的触控实现,下文会详细描述。
渲染
一般来说View不会直接面向OpenGL进行封装,而是通过中间层,在iOS上,使用的是CALayer(CoreGraphics),而在Android上,使用的是 Canvas(Skia)。
iOS
在iOS上,每个UIView都会有一个相对应的CALayer,我们称之为Layer-Back,也就是说,所有的UIView属性,最终,都会设置到CALayer身上。
为什么要使用CALayer这个中间层呢?很重要的一点是,CoreGraphcis框架,这个框架,在NEXT系统创建之初就存在了,并且是整个macOS系统的核心框架。这么6的框架,为毛不让他移植到iOS上呢?于是,CALayer就顺理成章地成为了UIView背后的贤内助。UIView会将以下属性proxy到CALayer上
-
alpha
-
frame
-
backgroundColor
-
clipsToBounds
-
hidden
为毛这么少属性?嗯,因为其它属性需要你自己去设置到CALayer上。什么?你要问CALayer是怎么渲染到屏幕上的?你自己查吧,据说,专门有一本书是写这个的。总的来说,UIView在渲染上,并没有做什么神奇的事情,CALayer才是一直默默耕耘的那个。
Android
Android实际上是个草根系统,出生的时候,并没有一个有钱的爸爸…所以呢? Android View的渲染层,其实是照抄H5中的Canvas。
比如,我要在 View 上画一个黑色的 backgroundColor,实际上会在 void onDraw(Canvas canvas)
方法中执行以下代码。
Paint paint = new Paint();
paint.color = Color.BLACK;
canvas.drawRect(x, y, width, height);
又例如,我想要让View有圆角裁剪的效果,怎么办呢?实际上,会这么做。
canvas.save();
canvas.clipPath(...); // 画一个圆角的路径,然后 clip。
// draw contents...
canvas.restore();
酱紫,在这个View中所绘制的所有图案(包括子View)都会被某个路径裁剪掉。
那么,View干了啥? View实际上会定义好x, y, width, height只有知道这些参数,你才能画出一个背景色,不然……你画个卵?View还管理着 alpha/backgroundColor等属性,这些属性,你都能在Canvas、Paint类中找到相关的参数。
iOS VS Android
这个,没什么好对比的,无非就是渲染层的抽象不一样而已。就渲染性能而言,iOS是更胜一筹的,自Android 4.x引入Skia以后,特别是Skia在 Google黄油计划以后,Android的渲染性能也差不了去哪里了。如果,你要死抠对比的话,我只能说一个是CALayer,一个是Canvas,CALayer更抽象而已了。
布局
我说过View最重要的事情,就是布局。布局,对于开发者来说,最简单的理解就是x, y, width, height。再复杂一点的话,就是层级、变换。
x, y, width, height
一例胜千言
let fooView = UIView()
fooView.frame = CGRect(x: 44.0, y: 44.0, width: 44.0, height: 44.0)
fooView.backgroundColor = UIColor.black self.view.addSubview(fooView)
就这样,我们在view中,就能在(44, 44, 44, 44) 这个区域中,渲染一片黑色。 噢,这已经说明了布局的用途了,确定位置,确定大小。
层级
我们要在上面的代码上,加点改进,在黑色区域的右下象限,添加一片红色。
let fooView = UIView()
fooView.frame = CGRect(x: 44.0, y: 44.0, width: 44.0, height: 44.0)
fooView.backgroundColor = UIColor.black let redView = UIView()
redView.frame = CGRect(x: 22.0, y: 22.0, width: 22.0, height: 22.0)
fooView.addSubview(redView) self.view.addSubview(fooView)
就是这么简单,因为,有了层级,我们可以很轻松地完成这件事情。我们可以不关心最终的界面是如何渲染出来的,我们只需要关心当前的一小区域即可。这就是层级的魔力 ———— 分而治之。
变换
如果想要我渲染出来的东西,旋转一下,那你最好使用 Matrix 变换。变换,在 View 里面,也属于布局的范畴。具体,不在这里展开讨论。
布局系统
上述的例子,是使用iOS作示例的,在Android上同样可以使用FrameLayout做到这件事情。
Q: 老师!我有问题!为什么你直接写x, y, width, height,我使用RelativeLayout/LinerLayout/AutoLayout不是更好吗?
A: 同学,你说得对,那是更高级的布局系统,是更高级的抽象!到最终,还是会变成x,y,width,height的,不信?你自己去探究一下。
触摸事件处理
如果你以为View只是渲染一下这么简单,那真是图森破图样了。一个常规的View类,必须做的事情,那就是触摸事件处理。常见的触摸事件处理,主要有两个过程,冒泡、向上递归。
冒泡
冒泡的主要作用是为了找出触摸点所在的 View,我们有个术语描述这个冒泡的过程————hitTest。hitTest一般是由最顶层的View开始进行的,在iOS里面是 UIWinodw,在Android里面是Window,因为他们是最先接收到这个触摸事件的响应者。接着,View使用 hitTest
询问自己能否成为响应者,成为响应者有几个条件,alpha > 0
、hidden == false
、userInteractionEnabled == true
以及 x, y 是否在 x, y, width, height矩形内。如果可以,则继续向自己的Subviews询问hitTest
,直至找到最终的响应者为止。
从我们看到的界面来说,响应者就是你所点中的那个View,响应链就是你所点中的那个View向上的superview->superview->superview…的这个路径。
任何一个触摸事件响应系统中,响应者和响应链都是必须的,一旦确定好响应者和响应链,触摸的过程就开始了。一般来说,hitTest只需要在TouchStart的时候进行。
你可以在iOS UIView中重写hitTest
方法,加以验证。在Android中,重写public boolean dispatchTouchEvent(MotionEvent event)
验证。
向上递归
冒泡过程完成后,我们会得到响应者A,紧接着touchstart、touchmove、touchend、touchcancel事件就会分发到这个响应者身上。响应者要做的事情,就是要识别这个触摸是不是他想要的,并且往superview继续传递这个事件。传递这个操作,十分重要,这意味着,当最深的 View 无法处理这个事件时,上一级的View可以收到这个事件并处理。你可以在iOS UIView中重写touchesBegan
、touchesMoved
touchesEnded
、touchesCancelled
方法,加以验证。在Android中,重写public boolean onTouchEvent(MotionEvent event)
验证。
iOS VS Android
在触摸事件的处理上,iOS与Android差异较大。iOS除了hitTest和向上递归外,还封装了不少GestureRecognizer,使得开发者几乎可以忽略原理就可以使用起来。而Android开发者,并没有那么幸运,遇到难题时,还是需要从触摸事件 原理入手去解决问题。
结论
这篇文章整篇都是结论的样子,如果有什么地方写错了,还希望大家能够指出。