大家都知道,Canvas是画布,开发者通过onDraw(Canvas canvas)方法,将View的具体content 画到 Canvas中,最后显示在屏幕上,但是有这么几个问题,一直困扰我好久: 1 什么是View的视图区域,其位置和大小是怎么决定的? 2 为什么View的Content可以无限大,是什么样的机制,保证将正确的内容显示屏幕上? 3 Canvas 是怎么来保存 View的具体内容的? 4 Canvas 的translate,rolate等变化是如何实现的? 接下来,将对上述几个问题,进行一一解答。
1) 什么是View的视图区域,其位置和大小是怎么决定的?
同一个窗口的视图层次树中的View节点,其onDraw中的Canvas参数都是同一个,是由该窗口的ViewRootImpl通过surface.lockCanvas获得,并把该Canvas传给根视图, 对于应用窗口来说,其根视图的Canvas大小一般为整个屏幕;
对于视图层次树中的View,通过 Measure 和 layout 两个过程之后,便可确定其大小和位置,视图在绘制的过程中,根据其mLeft, mRight, mTop, mButtom, mScrollX和mScrollY等参数,将父节点传过来的Canvas进行剪切操作,得到新的Canvas,该剪切区便是父节点分配给子节点的视图区域,其源代码如下:
if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN &&
!useDisplayListProperties && cache == null) {
if (offsetForScroll) {
canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop));
} else {
if (!scalingRequired || cache == null) {
canvas.clipRect(0, 0, mRight - mLeft, mBottom - mTop);
} else {
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
}
Canvas之所以要这么设计的主要原因是,为了View的绘制,通过这种方式,使得View每次绘制是,都可以该视图的原点坐标作为绘制的原点坐标,而独立于其他View,使得绘制逻辑大大简化。
2) View的Content可以无限大,是什么样的机制,保证将正确位置的内容显示屏幕上?
在许多资料和博客上,都有提到,View的Content可以无限大,但是通过什么 样的机制,将正确位置的内容显示到屏幕中?
从问题1中,咱们可知,每个View都在屏幕存在一个由父节点分配的可视区域,当View绘制的内容,超过了可视区域的内容,就不会显示,此时咱们可以通过scrollTo或者scrollBy方法,将不显示的内容,滚动到可视区域,下面将详细的解释下其原理:
首先来看图中,那黑色和红色的坐标系,其中黑色的为ViewGroup的坐标轴,即父节点的坐标轴;红色的为子节点的坐标轴,通过父节点对子节点的measure和layou的过程之后,并可以确定子节点坐标轴的原点坐标为
当绘制的内容超过其可视区域是,便通过scrollTo或者scrollBy方法,将超过可视区域的内容,滚动到可视区域(mScrollX > 0时 向左滚动,mScrollY > 0时 向上滚动) ,其具体实现过程是:
1) 记录具体的滚动位置(mScrollX mScrollY),并调用invalidate()方法,scrollTo和scrollBy作用存在差别,但其本质都是调用scrollTo;
2) 由invalidate方法向上传递,并最终又触发了该View的这个方法
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)
在这个方法中,作了这么几个特殊处理:
1 将该坐标系移动到
if (offsetForScroll) {
canvas.translate(mLeft - sx, mTop - sy);
} else {
if (!useDisplayListProperties) {
canvas.translate(mLeft, mTop);
}
if (scalingRequired) {
if (useDisplayListProperties) {
// TODO: Might not need this if we put everything inside the DL
restoreTo = canvas.save();
}
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
从代码中可知,是通过canvas.translate的方式,来移动坐标系,translate方法这个将在第4个问题来详细描述;
2 在对父节点传过来的Canvas进行可视区域进行截取时,作了一些处理,其源代码如下:
if (offsetForScroll) {
canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop));
} else {
if (!scalingRequired || cache == null) {
canvas.clipRect(0, 0, mRight - mLeft, mBottom - mTop);
} else {
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
从代码中可知,即使有滚动,但是其View的可视区域的位置和大小都没有更改,如图中,那个灰色的布局窗口;
通过处理之后,最终又会调用View.onDraw方法,此时绘制相对的坐标系,并且更新后的坐标系,即图中的橙色坐标系,通过这种方式,将显示在可视区域的内容的坐标滚动了
3 Canvas 是怎么来保存 View的具体内容的:
Canvas本身只是一个工具类,它可以调用底层绘制驱动来绘制相应的图形,但真正保存图像的是Canvas中的Bitmap对象,Bitmap中有一个byte数组,用来存放[0~255]的像素值;
4 Canvas 的translate,rolate等变化是如何实现的?
Canvas中存在四种变换,及translate rolate Scale和Skew,本文将以rolate(旋转来进行讲解)。许多博客中,都把canvas rolate当作是 Canvas本身的旋转,其实这样的理解是有偏差的,真正旋转的并不是画布本身,而是将其坐标系(通过Matrix矩阵的变化)来进行旋转,下面将以图形结合代码来进行说明:
当调用Canvas.rolate(-45)时,是把绘制坐标逆时针旋转45°,但View的可视区域并不会更改。此时绘制的时候,会根据新的坐标系来进行绘制,所以最后显示在屏幕上的,就是两者的交集区域,下面是运行的真实效果,与预期一致:
其余几个变换也都是通过Matrix矩阵,来进行坐标系的调整,但是Canvas本身并没有更改,而且这种调整是不可逆的,但能通过canvas.save和canvas.restore来进行保存和恢复;