通过自定义View,我们可以画出一些简单的图形并进行应用。上一篇中我讲了自己对onDraw()方法的一些理解和运用,今天就来讲讲onMeasure()和onLayout()两个方法的理解和使用。在自定义View当中呢,我们对于onMeasure()和onLayout()方法基本上是不需要重写的,所以我这边新建了一个工程,自定义一个MyView2继承于ViewGroup,同样地它会自动提示添加一些构造方法,这些就不细说了,当然也包括其中什么Paint对象的实例化。
首先,讲一下onMeasure()方法,它呢,主要是用来确定子控件的规格的,何为规格?也就是大小和模式。下面是我重写的代码,根据代码来进行一下分析。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int viewGroupWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); int viewGroupHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom() - getPaddingTop(); int viewCount = getChildCount(); for (int i = 0; i < viewCount; i++) { View view = getChildAt(i); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); view.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST)); } setMeasuredDimension(viewGroupWidth, viewGroupHeight); }
首先是onMeasure方法里面的两个参数,widthMeasureSpec和heightMeasureSpec,它们是父容器传给子容器的宽高规格,通过这两个参数的getSize()方法可以得到父容器的宽高,而通过getMode()方法可以得到父容器的模式。这里就要介绍一下相关的模式了,主要是以下三种:
(1)AT_MOST,MeasureSpecSize代表的就是最大可获取的空间;
(2)EXACTLY,MeasureSpecSize代表的就是精确的尺寸;
(3)UNSPECIFIED,其实就相当于没有特别定义的,因为对相应的控件没有参考意义。
根据上面的代码实力,因为我们MyView2是继承于ViewGroup类,所以这个自定义的ViewGroup的宽就等于其父容器的宽度减去它与自身内边距的左边和右边的距离,高也是类似的。通过getChildCount()方法获得这个ViewGroup其中子View的个数,并且通过一个for循环的到这些子View,并且这些子View重写measure方法,这里用的是AT_MOST模式。最后通过setMeasuredDimension来设置这个ViewGroup的宽高。这是通过父容器得到ViewGroup的宽高规格并设置,再通过ViewGroup来得到其中子View的宽高规格并重写measure方法进行设置。其实自定义控件就是这样,一层一层地传递下来,从而确定每一个View的大小规格。
说完onMeasure()方法,说一下onLayout()的方法。onLayout()方法其实就是来确定控件位置的,本身是一个抽象方法。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) ;
而方法中的l,t,r,b就是英文中的left(左)、top(上)、right(右)、bottom(下),通过这四个参数我们就可以知道ViewGroup中子View的具体位置,当然这个位置是根据ViewGroup相对而言的,也就是它们的父容器。假设有个子View的位置为(10,10,10,10),也就是说他距离其父容器左上右下的距离是10,而不是只它相对屏幕的距离,这个地方也是很容易让人误解的地方。下面给出一个onLayout()方法的示例来进行分析:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); int contentWidth = getWidth() - paddingLeft - paddingRight; int contentHeight = getHeight() - paddingTop - paddingBottom; int centerX = contentWidth / 2; int centerY = contentHeight / 2; View view = getChildAt(0); int height = view.getMeasuredHeight(); view.layout(centerX - view.getWidth() / 2, centerY - view.getHeight() / 2, centerX + view.getWidth() / 2, centerY + view.getHeight() / 2); }
在上面的代码中最重要的是view.layout()方法,通过这个方法我们可以得到子View的位置,从上面的代码中可以看到,我们可以通过getPadding的四个方法得到ViewGroup的padding值,然后得到它的宽高,以及中心点,因为子View也是有宽高的,也就是view.getWidth()和view.getHeight()方法,所以我们在layout()方法中也考虑到它的大小,这里我们的这个view是ViewGroup中的第一个子View,处于ViewGroup的最中心,所以我们看到它与左上右下的距离对view的宽高的一半进行了加减法,如下图所示:
同样地,根据这个画法,我们可以确定更多的子view的具体位置。所以下面就是一个相对比较难,需要运用到数学算法的确定子View位置的一个Demo讲解,要实现的效果图大概如下。
如图所示,有三个圆圈,在这三个圆圈上面有规律地分布着一些图案。最外圆上是上面3个图案均分上面的那一段弧长,而下面那两个图案均分下面的弧长,中间五个图案均分中间的那个圆。
关于中间这个圆上的图案分布,我写了下面这个方法:
/** * 确定中间圈上的标签位置 * @param centerX 中心点横坐标 * @param centerY 中心点纵坐标 */ private void layoutMiddleImage(int centerX, int centerY) { int singleAngle = 360 / middleImageList.size(); for (int i = 0; i < middleImageList.size(); i++) { View childView = middleImageList.get(i); int width = childView.getMeasuredWidth(); int height = childView.getMeasuredHeight(); /** *第一个点无论如何都从中心点正上方的那一点开始,也就是i = 0的时候开始,, * cosA = sin(90 - A) = -cos(90 + A) * sinA = sin(180 - A) */ int x = (int) (centerX + Math.cos(singleAngle * i + 90) * middle_round_radius / 180); int y = (int) (centerY - Math.sin(singleAngle * i + 90) * middle_round_radius / 180); layout(x - width, y - height, x + width, y +height); } }middleImageList是中间那个圆上标签组成的list,所以我们先求出平均角——singleAngle,然后就开始进行for循环确定位置了。实例化View对象,并且获得它们的宽高,也就是图中标签的宽高。这里我是从最上方的那个标签开始的,我们知道最上方那个标签的中心点位置其实就是:
横坐标 x = 中心点位置的横坐标,纵坐标 y = 中心位置的纵坐标减去圆的半径,
然后我们再用x,y来加减标签的高宽的一半。然而我们知道这五个标签是有规律的,因此根据第一个标签的位置进行推到,又因为标签的角度依次增大,每次加上singleAngle,所以根据三角函数的关系得出了它的中心点位置。