【安卓开发艺术探索】第4章 View的测量、布局、绘制 笔记

任何的View想要显示到屏幕上,都要经过3个流程:

  • measure 测量宽和高
  • layout 确定左、上、右、底的位置。
  • draw 绘制

而这一章将总结这3个环节的机制,从而可以真正自如地去定义一个自己的View或ViewGroup。

View的测量、布局、绘制原理机制

这一系列的过程首先是从ViewRootImpl的一个方法performTraversals开始进行的,这个方法代码比较多,简单说会按顺序依次调用performMeasureperformLayout,performDrawperformMeasure源码如下:

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

很简单,就是调用mView的measure,这里传的参数都是屏幕的最大宽和高。浏览整个ViewRootImpl源码文件,唯一对mView做有效赋值的是方法setView,毫无疑问这里的mView一定就是DecorView。所以测量的入口就是DecorView的measure方法了。
一般来讲,DecorView由两部分组成,上面的titlebar和下面的contentView,而我们在acitivity中setContentView方法所设定的便是下面这一部分。另外,DecorView继承自FrameLayout,同时重写了onMeasure方法,在这个方法中又调用了super.onMeasure所以FrameLayout的onMeasure也被调用,在这个onMeasure中会对所有子元素进行measure,我们的contentView当然也不例外。
上面说了内部进入measure的过程,读者可能脑袋有点大了,不过我先带来一些总结过的知识:

  • 在View中会默认提供这些方法:measure/onMeasure, layout/onLayout, draw/onDraw/dispatchDraw,这些成对的方法,后者都会被前者所调用。即measure里会调用onMeasure,layout里会调用onLayout,draw里会调用onDraw,再调用dispatchDraw。其中measure方法是不可重写的。onDraw/dispatchDraw默认都是空的。
  • 在具体的View和ViewGroup实现类(子类)中通过重写onMeasure,onLayout,onDraw/dispatchDraw完成自己的视图业务。
  • 在onMeasure中最后一定要用setMeasuredDimension来设置最终的宽和高。如果要实现的是ViewGroup,则有义务去调用子View的measure,这个过程一般可以通过ViewGroup提供的measureChild系列方法来间接调用。如果不调用ChildView的onMeasure,结果就是ChildView均无法显示。如果不去特别对待MeasureSpec的AT_MOST和EXACTLY模式的话,最终结果很可能使得此View的wrap_content和match_parent效果一样。
  • 在onLayout中,如果要实现的是ViewGroup则有义务去调用ChildView的layout方法,传递的参数是此ViewGroup想让孩子所处的左坐标,上坐标,右坐标,下坐标。在使用的过程也可以放心地获取childView的measuredWidth和measuredHeight等。这个方法的重写对于ViewGroup比较重要,用来真正设置childView的位置。
  • 在onDraw/dispatchDraw中,如果要实现的是ViewGroup,一般不加以干涉,ViewGroup会重写dispatchDraw方法,对于大部分ViewGroup的绘制过程基本通用。比如RelativeLayout就没有任何draw方面的方法,全部使用ViewGroup的。而LinearLayout重写了onDraw方法用来绘制dividers,而在dispatchDraw时仍然使用ViewGroup的。如果要实现的是View,则有必要重写onDraw方法,来绘制出自己真正想显示的东西,绘制的东西如果超出了layout的过程的限制,则不会显示。

由此,我们结合上面所说的整个显示过程是从ViewRootImpl的performTravelsals开始,以decorView为根开始进入的,我们就掌握了整个View显示过程的来龙去脉。下面,我简单自定义了一个ViewGroup作为练习。其业务很简单,将每个childView依次向右下方排列,支持wrap_content,支持childView的可见性为gone(这一点需要自己手动实现)。贴代码:

public class TestViewGroup extends ViewGroup {

    final int marginLeftDelta = 20;
    final int marginTopDelta = 40;

    public TestViewGroup(Context context) {
        super(context);
    }

    public TestViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int maxWidth = 0, maxHeight = 0;
        int visibleCount = 0;
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            //needs to check visiablity
            if (child.getVisibility() == GONE) continue;
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + visibleCount * marginLeftDelta);
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + visibleCount * marginTopDelta);
            visibleCount++;
        }
        if (widthMode == MeasureSpec.AT_MOST) widthSize = maxWidth;
        if (heightMode == MeasureSpec.AT_MOST) heightSize = maxHeight;
        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int visibleCount = 0;
        for (int i = 0; i < getChildCount(); i++) {
            //needs to check visiablity
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) continue;
            child.layout(marginLeftDelta * visibleCount, marginTopDelta * visibleCount, marginLeftDelta * visibleCount + child.getMeasuredWidth(), marginTopDelta * visibleCount + child.getMeasuredHeight());
            visibleCount++;
        }
    }
}

测试的布局文件:

    <com.zerastudio.viewtest.TestViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <View
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@color/colorAccent" />

        <View
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="#ffffaa" />

        <View
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@color/colorAccent" />

        <View
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="#ffffaa" />

        <View
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@color/colorAccent" />
    </com.zerastudio.viewtest.TestViewGroup>

在AS的preview就能看到此时的效果了:
这里写图片描述
如果把一个childView的可见性设为gone则:
这里写图片描述

看到右边,确实没有再对visibility为gone的childView进行measure和layout了,draw部分完全使用了ViewGroup提供的。

至此,View的测、布、绘的原理就基本清楚了,但这也只是一个开始,接下来我将继续探索如下的方面:

  1. LinearLayout和RelativeLayout甚至其他Layout的测、布、绘的区别,真正分析其效率差异原因。
  2. RecyclerView和ListView的测、布、绘的区别,以及两者的效率差异原因。
  3. BitmapShader和xfermode,这两者将有助于绘制出更复杂美丽的View。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值