第二部分:自定义ViewGroup提高性能

原文:#PerfMatters introduction to custom ViewGroups to improve performance — Part 2 

blob.png

许多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相比较。