第二部分:自定义ViewGroup提高性能
原文:#PerfMatters introduction to custom ViewGroups to improve performance — Part 2
许多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相比较。