许多ViewGroups比如LinearLayout和RelativeLayout都是常规容器。这意味着它们为了计算出如何布局它们的子view,必须重复做测量和布局的工作。view越多层次越深,越复杂并且布局的变化时间开销就越大。如果你知道一个view是如何布置在容器中的,那么你就能通过自己测量和布局自己的view来提高性能。
在 第一部分中,我们讨论了如何才能创建自己的view以避免多个view的嵌套,减小复杂度,从而避免了布局过程达到提高性能的效果。在这篇文章中,我将阐明ViewGroup 中的onMeasure 和onLayout 方法。这两个方法分别用于测量和布局容器中的所有子view。
如何设置一个ViewGroup中子view的大小
在这里,我想让ViewGroup 的子view去测量自己并把它们横向布局。
第一步: 创建一个自定义的ViewGroup
创建一个继承自ViewGroup的新类。然后重写构造方法。
public class HorizontalBarViewGroup extends ViewGroup { public HorizontalBarViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } public HorizontalBarViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public HorizontalBarViewGroup(Context context) { super(context); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public HorizontalBarViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override protected void onLayout( boolean changed, int l, int t, int r, int b) { } }
我们还需要提供onLayout 方法的实现。这个方法设置它每个子view的位置和大小。下面我提供了一个非常基础的实现,遍历所有子view然后逐个横向依次布局。
@Override protected void onLayout( boolean changed, int l, int t, int r, int b) { int count = getChildCount(); int prevChildRight = 0; int prevChildBottom = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); child.layout(prevChildRight, prevChildBottom, prevChildRight + child.getMeasuredWidth(), prevChildBottom + child.getMeasuredHeight()); prevChildRight += child.getMeasuredWidth(); } }
代码很简单,我们循环遍历每个子view并把前一个view 的结束位置作为它的起始位置。参数为:
Left — view的x轴起始位置
Top — view的y轴起始位置
Right — view的x轴结束位置
Bottom — view的y轴结束位置
然后创建一个activity 并在布局中使用这个ViewGroup :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <com.example.alimuzaffar.perfmatters.HorizontalBarViewGroup android:background="#80FF0000" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:background="#00FF00" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World"/> <TextView android:background="#0080FF" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World"/> </com.example.alimuzaffar.perfmatters.HorizontalBarViewGroup> </LinearLayout>
运行此代码,你会发现自定义的ViewGroup 充满了整个屏幕,但是子view是看不见的。这是因为我们还没有告诉子view它们该如何测量(measure )自己,因此它们不会被渲染。我们的ViewGroup 当然知道如何测量自己,但是它并没用使用这个信息做任何事情,因此它直接填充满了所有可用空间。
第二步:测量子view
我们将在onMeasure()方法中告诉子view去测量自己。最简单的实现方法就是遍历子view,并对它们使用measureChild()方法。
@Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); } }
注:measureChild方法是ViewGroup的一个方法,定义如下:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
如果你现在运行代码,你会看见子view可以被渲染了。ViewGroup 仍然占据了整个屏幕,但是子view的渲染和预期一致。你还可以使用measureChildren() 方法来简化上面的代码,这个方法将自动遍历所有子view并让它们测量自己。这个方法还可以忽略那些visibility 设置为gone的子view,因此它支持visibility gone标志。
注:前面是measureChild()方法,而这里是measureChildren() 方法,不要混淆了。
目前,margins还不能工作。如果我们想支持margins,可以在我们容器的onLayout 里面添加它们而不是在测量一个子view的时候去考虑margins。
第三步:测量容器
目前为止,我们还没有告诉容器去测量自己,假设我们想在容器中使用wrap_content ?为此我们需要知道所有子view的总宽度与最高子view的高度。如果我们能够计算出这两个值,我们就能使用setMeasuredDimension()来设置容器的宽度和高度。用下面的代码来替换onMeasure中的内容。
注:现在你不应该再调用 super.onMeasure()了。
int totalWidth = 0; int totalHeight = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); totalWidth += child.getMeasuredWidth(); if (child.getMeasuredHeight() > totalHeight) { //height of the container, will be the largest height. totalHeight = child.getMeasuredHeight(); } } setMeasuredDimension(totalWidth, totalHeight);
这样你就能看到代码运行的效果了,在其中一个子view上添加padding ,然后运行代码,你可以看到如下效果:
红色空白区域暗示了ViewGroup 的宽度正好是子view宽度的和而高度是最高子view的高度。
第三步: 让子view为特定的大小
如果我们想节省时间,我们可以直接告诉子view它们的大小该是多少。在这种情况下,你应该明确知道你ViewGroup里面的元素以及每个元素应该有的实际大小以及它应该摆放的位置。为了告诉子view如何布置它们自己,你可以建立自己的MeasureSpec 并把它传递给子view。
如果你把下面的代码放在onMeasure中,每个子view的大小会调整为精确的300px:
int totalWidth = 0; int totalHeight = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); int width = MeasureSpec.makeMeasureSpec(300, MeasureSpec.EXACTLY); int height = MeasureSpec.makeMeasureSpec(300, MeasureSpec.EXACTLY); child.measure(width, height); totalWidth += width; if (height > totalHeight) { //height of the container, will be the largest height. totalHeight = child.getMeasuredHeight(); } } setMeasuredDimension(totalWidth, totalHeight);
再次,每个子view都是300px,ViewGroup 把它们紧紧的包裹住。
就如你看到的,第二个view还是渲染了我们给它的padding。
Putting it all together
现在,我们有了为ViewGroup中的子view提供大小和布局信息的基本工具。你应该大致知道了我们如何使用它来提高布局的渲染速度。在第三部分,我们看看使用我们自定义的ViewGroup 创建HorizontalBarView 并和使用默认的view相比较。