Android View基础

5,255 阅读5分钟

1 View的基础属性

int mLeft: View左边距,即View的左边 距离 父View左边 的距离
int mTop: View上边距,即View的上边 距离 父View上边 的距离
int mRight: View右边距,即View的右边 距离 父View右边 的距离
int mBottom: View下边距,即View的下边 距离 父View下边 的距离
上面四个属性,全部都是针对View原始位置的,也就是说:如果View滑动了,这四个属性也不变

int translationX: View相对于原始位置的 水平偏移量,默认是0
int translationY: View相对于原始位置的 垂直偏移量,默认是0

int x: View左边 距离 父View左边 的距离
int y: View上边 距离 父View上边 的距离
这两个属性是针对View当前位置的,也就是说:如果View滑动了,这四个属性也会随着改变 换算关系为:

int x = mLeft + translationX;
int y = mTop + translationY;

其实x和y这两个属性是没有的,代码中是通过以下函数实现的

public float getX() {
    return mLeft + getTranslationX();
}

public void setX(float x) {
    setTranslationX(x - mLeft);
}

int mScrollX: View左边缘 距离 View内容 左边缘 的距离: View左边缘 - View内容左边缘, 所以当View左边缘在View内容右边时,也就是向左滑动时,mScrollX为正,反之为负,因为左边是X轴的负方向,所以可以简记为: 向负为正

int mScrollY: View上边缘 距离 View内容 上边缘 的距离: View上边缘 - View内容上边缘,所以当View上边缘在View内容下边时,也就是向上滑,mScroll为正,反之为负,因为上边是Y轴的负方向,所以可以简记为: 向负为正

上面两条可以简单记为:向负为正 这里有个知识点需要理解: View.scroll(20,20),其实View本身没动,只是将View的内容(不包括背景)滑动了(-20,-20)而已

来个图意思一下:

View属性

图中黑色矩形是初始位置,橙色矩形是滑动后的位置,(x,y)表示矩形左上角的点

int mMeasureWidth: View测量的宽度,在onMesasure()回调之后可以获取

int mMeasureHeight: View测量的高度,在onMeasure()回调之后可以获取

getWidth(): 即 mRight - mLeft: 获取View实际宽度,在onLayout()之后可以获取,因为mLeft, mTop, mRight, mBototm 都是在onLayout()之后确定的

getHeight(): 即 mBottom - mTop: 获取View实际高度,在onLayout()之后可以获取,原因同上

点击属性

class MotionEvent {
    getX()/getY(): 获取相对父容器左上角的坐标 
    getRawX()/getRawY(): 获取相对屏幕左上角的坐标
}

2 View的基础方法原理

View树 可以把View体系理解为一个View树,每个View都有个mParent属性,来指向自己的父View,最顶层的mParent是ViewRootImpl(也就是decorView的mParent),每个ViewGroup又有mChildren属性,表示自己的子View集合,这样每个View可以向上访问自己的父View,也可以向下访问自己的子View集合,可以将整个View体系理解为一个多叉树,顶层节点就是ViewRootImpl。

Measure过程

  • 1 View是承载在Window上的,可以理解为一个画框,View是画纸
  • 2 View的测量是从Window传递下来的,Activity中View的测量是在super.onResume()里面的
  • 3 测量过程从最顶层的DecorView开始依次下发,直到每一个View为止
  • 4 如果某一个View更新了自己的大小,则会调用父View的requestLayout(),父View又会调用自己的父View的requestLayout(),直到DecorView为止,然后在向下measure()到这个View为止
  • 5 measure()过之后,就可以通过getMeasureWidth()/getMeasureHeight()获取自己的宽高
  • 6 自定义View直接继承自View的,measure()需要注意计算padding属性,以及处理wrap_content

Layout过程

  • 1 View的layout()和Measure()基本类似,也是一个自顶向下的过程
  • 2 layout()过之后,可通过getLeft()/getTop()/getRight()/getBottom()获取自己的边界
  • 3 自定义View,直接继承自View的,需要计算margin属性

Draw过程

  • 1 draw过程需要了解两个坐标系: View的坐标系: 以View左上角为原点的坐标系
    View内容的坐标系: 以View内容左上角为原点的坐标系

  • 2 View的绘制是先在View的坐标系绘制View背景,然后变换到View内容的坐标系去绘制View的内容,变换过程是用canvas.translation(x,y)实现的

  • 3 View的绘制也是自顶向下的,父View绘制完毕,就会将坐标系转换为View的坐标系,然后交给View去绘制,View绘制完后,将坐标系还原(canvas.restore()),在返回到父View,父View继续交给下一个View去绘制,直到绘制完毕为止

综上 每次View有更新,都会触发一个requestLayout(),view会沿着View树向上触发requestLayout(),并且沿路添加标记,直到ViewRootImpl为止,然后ViewRootImpl再自顶向下依次去measure(),layout(),draw(),在measure()/layout()/draw()中,每个view都会检测自己是否被标记,如果没被标记,就直接跳过,如果被标记,则会执行自己的测量/布局/绘制,然后清除标记,说白了就是:
向上的过程: 添加标记
向下的过程: 检测标记 更新自己 清除标记
这样的好处是,避免没经过的View,也就是没变动的View,也白白的执行更新操作。举个例子:

View的更新

如图: D修改了自己的大小或者更改了位置,那么它就调用自己的requestLayout()给自己添加标记,然后找自己的父View C,C也给自己添加标记,依次向上,直到A,也就是1,2,3;

然后A开始向下分发去测量/布局/绘制,并沿途清除标记,也就是4,5,6,当然E和F也会被测量/布局/绘制,但是因为他们没被标记,所以什么都不做,可以看到,这样的方式,可以省去很多不必要的操作,提高View的更新效率。