Android之自定义View

422 阅读20分钟

前言

本文档主要用于学习自定义View的相关使用和源码解析。

1. LayoutInflater解析

在学习如何实现自定义View之前,我们需要了解控件是布局如何实现,View加载到布局中以及布局视图本身的加载,这是了解自定义View第一步。

那么关于视图加载的一大利器,我们最不陌生的就是LayoutInflater,下面就将介绍LayoutInflater

1.1 声明定义视图加载器LayoutInflater

LayoutInflater layoutInflater = LayoutInflater.from(this);

以下语句是from方法源码中声明定义LayoutIInflater的方法,同样可以用来声明定义LayoutInflater.

LayoutInflater LayoutInflater =
        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

Context.LAYOUT_INFLATER_SERVICEContext的一种服务之一,用于检索当前上下文的视图加载器.

/**
     * Use with {@link #getSystemService(String)} to retrieve a
     * {@link android.view.LayoutInflater} for inflating layout resources in this
     * context.
     *
     * @see #getSystemService(String)
     * @see android.view.LayoutInflater
     */
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";

1.2 LayoutIInflater加载布局文件

View view =  layoutInflater.inflate(R.layout.XXX);

在源码中,inflate方法被重载了多次,但是其他几类均会调用以下Inflate方法.

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);

1.2.1 XmlPullParser

第一个参数XmlPullParser是要解析的xml布局文件

  1. XML结构

    主要是由 头部声明 + 诸多元素(Element)组成

    元素可分为标签(tag , 分为头标签与尾标签)+内容/子元素

    <?xml version="XXX" encoding="utf-8"?>
    <tag1>
        content1
    </tag1>
    <tag2>
        <tag2.1>
            content2.1
        </tag2.1>
        
        <tag2.2>
            content2.2
        </tag2.2>
    
        <tag2.3>
            content2.3
        </tag2.3>
    
    </tag2>
    
  2. XML解析

    一个元素(包括子元素)就是一个事件,遇到头标签开始解析事件

    XMLPullPareser中有着以下几个事件标识

    START DOCUMENT        文档开始解析
    
    		START_TAG         一个元素标签的开始事件,这时解析该元素的属性值       
    
        TEXT          解析该元素的内容事件,这时解析该元素的内容值       
    
      END_TAG          一个元素标签的结束事件 
    
    END_DOCUMENT          文档解析结束
    
    函数用法
    next解析下一个标签
    nextTag下一个标签
    getEventType获得事件类型
    nextText获得节点内具体内容
    getName获取节点名
  3. Merge标签

1.2.2 root和attachToRoot

后两个参数,root为根容器组件, attachToRoot表示是否添加到root中.

1.3 源码解析

1.3.1 inflate源码

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        //Context
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        
        //保存根容器组件
        View result = root;

        try {
            // Look for the root node.
            //这一部分就是找寻第一个标签
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                   type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            //如果找到文档解析结束也没找到一个头标签,那么就直接抛出异常
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                                           + ": No start tag found!");
            }
			
            //找到标签,获取第一个标签的名字
            final String name = parser.getName();

            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                                   + name);
                System.out.println("**************************");
            }
			
            //是merge标签,则调用rInflate函数,将所有的merge标签下的元素加入到父根容器组件之下
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                                               + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                //如若不是merge标签,则根据标签解析布局中"name"标签并返回一个相应的View变量
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;//ViewGroup的布局参数
				//如果给定父布局,那么调用root的generateLayoutParams()并传入xml的解析属性, 生成布局参数
                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                           root);
                    }
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        //并且attachToRoot为false意味着不添加到根容器组件,那么直接为View添加布局参数
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                }
				//开始加载temp下所有子View
                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);

                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                }

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                //根容器组件不为空,则直接调用根容器组件的addView()
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                //不加载到根容器组件,则返回的结果为原先根据tag生成的View
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            final InflateException ie = new InflateException(e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (Exception e) {
            final InflateException ie = new InflateException(parser.getPositionDescription()
                                                             + ": " + e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        return result;
    }
}

流程如下:

  1. 先判断是否是merge标签,如果是,则直接函数rInflate将所有子View加载到root中
  2. 如若不是merge标签,则根据parser得到的"name"标签创建一个View变量(createViewFromTag)
  3. 调用rInflateChildren将生成的View作为根容器,把新生成的View的子View加载到新生成的View中
  4. 如果给定root,那么attachToRoot默认为true,讲新创建的View变量加载到root中
  5. attachToRoot为false或者无root,则返回的结果为原先根据tag生成的View变量

inflate方法主要涉及到的比较重要的标识TAG_MERGE与函数rInflate与函数createViewFromTag.

createVIewFromTag: 每一个我们所学习的控件(xml文件中的每个标签)都是一个View,该函数又有一个createView函数返回一个View实例.

Merge标签

一般Merge标签是同include标签一起使用.

如下是一个标题栏的布局"title"文件

<?xml version="1.0" encoding="utf-8"?>
<merge>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent"
        android:orientation="horizontal">

        <ImageButton
            android:id="@+id/title_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@drawable/turn_left"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageButton
            android:id="@+id/title_go"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@drawable/turn_right"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="Title Text"
            android:textAllCaps="false"
            android:textSize="24sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/title_go"
            app:layout_constraintStart_toEndOf="@+id/title_back"
            app:layout_constraintTop_toTopOf="parent" />
                
    </androidx.constraintlayout.widget.ConstraintLayout>
</merge>

在activity_main中引入include标签如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
        
    <include layout="@layout/title"/>
   
        <Button
        android:id="@+id/diy_dialog_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="DIY dialog"
        android:textAllCaps="false"
        android:textSize="20sp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_click"/>

</LinearLayout>

Merge标签能够减少层级,优化布局使用,通常与include标签配合使用.

根据源码分析,也就知道优化布局的原因,直接将所有的merge标签下的元素加入到父根容器组件之下,不用自己产生View再进行诸多比较等.

1.3.2 rInflate源码

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
		//获得parser的深度,为后续DFS做准备
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
		//对每一个元素进行解析,最外层深度为0(如最初的头尾标签均是0)
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                //首先判断解析属性中foucusable为true的元素,获取View的焦点
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                //解析View的tag
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {//同时include标签不可以是根元素
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {//如果存在merge,必须是根元素,而merge标签在最初已经进行了处理
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);//根据Tag产生View
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                //递归调用,解析view当中的子view
                rInflateChildren(parser, view, attrs, true);
                //解析后的view加入到父View中去
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

//rInfalteChildren内部也是递归调用rInflate,然后把子View加载到view中,即父容器组件就是view本身
    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

对于XmlPullParser的高度:

  1. 最外层高度为0
  2. 遇到本层END_TAG之前遇到START_TAG则高度+1
  3. 遇到本层END_TAG之后,说明从深层跳到该层,高度-1
/**    
	* <pre>
    * &lt;!-- outside --&gt;     0
    * &lt;root>                  1
    *   sometext                 1
    *     &lt;foobar&gt;         2
    *     &lt;/foobar&gt;        2
    * &lt;/root&gt;              1
    * &lt;!-- outside --&gt;     0
    * </pre>
         */

源码对于getDepth()的注解例子如上.

流程如下:

  1. 对每一个元素进行解析
  2. 如若有需要聚焦属性的元素,那么获取View的焦点
  3. 如若不是,排除include&&merge标签
  4. 2、3排除后则正常解析View的tag
  5. 然后使用createViewFromTag来产生View,并调用rInflate函数进行子View加载到View中
  6. 最后结束递归,调用 终止加载parent.onFinishInflate();

1.3.3 createViewFromTag源码

以上流程将布局所有的View加载完成,而负责一个控件View生成的函数是createViewFromTag.

  View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
		
      	//如果标签与主题相关,则需要将context与themeResId包裹成ContextThemeWrapper。
        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }
      
		//BlinkLayout,闪烁布局,被包含的View,会有闪烁效果,如QQ消息
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }
		//以上部分与生成view不直接关联
      	
      //用户可以设置LayoutInflater的Factory来进行View的解析,但是默认情况下Factory均为空
        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    //使用自定义View是需要在xml指定全路径的,而自定义View的包路径中一定带有着".",com.XXXX.XXXX.XXXX
                    //即可判断内置View还是自定义View
                    if (-1 == name.indexOf('.')) {
                        //内置View控件
                        view = onCreateView(parent, name, attrs);
                    } else {
                        //自定义View控件
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

单个View的解析过程如下:

  1. 判断是否是"view"标签,是的话获取其"class"
  2. 标签的属性是否是Theme Wrapper有关的,是的话,需要将此时的上下文与相应的主题打包成ContextThemeWrapper
  3. LayoutInflator有着多种工厂可以在初始化的时候进行设置,但是默认状态时没有设置的.
  4. 不设置则开始判断是内置View还是自定义View,如果是内置View则调用onCreateView; 自定义View则调用createView.
  5. 最后返回View

view标签

<view
      class="LinearLayout"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>

view标签需要指定class,用以表明是哪一种控件.

BlinkLayout

闪烁布局,与其他布局一样使用.

<blink
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <Button
                android:layout_width="wrap_content"
    			android:layout_height="wrap_content"
            	android:text="闪烁的按钮"/>
</blink>

1.3.4 createView源码

onCreateViewcreateView的区别主要在内置View会加上"android.view."前缀

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

onCreateView的实质还是调用了createView方法,因此来看下createView源码

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    //从cache中获得构造函数
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
    }
    Class<? extends View> clazz = null;

    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

        if (constructor == null) {//没有缓存,补全路径找寻构造函数
            // Class not found in the cache, see if it's real, and try to add it
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);

            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);//放到缓存中去
        } else {
            // If we have a filter, apply it to cached constructor
            if (mFilter != null) {
                // Have we seen this name before?
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) {
                    // New class -- remember whether it is allowed
                    clazz = mContext.getClassLoader().loadClass(
                            prefix != null ? (prefix + name) : name).asSubclass(View.class);

                    boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                    mFilterMap.put(name, allowed);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                } else if (allowedState.equals(Boolean.FALSE)) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
        }

        Object lastContext = mConstructorArgs[0];
        if (mConstructorArgs[0] == null) {
            // Fill in the context if not already within inflation.
            mConstructorArgs[0] = mContext;
        }
        Object[] args = mConstructorArgs;
        args[1] = attrs;
	//根据构造函数,构建View实例
        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) {
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        mConstructorArgs[0] = lastContext;
        return view;

    } catch (NoSuchMethodException e) {
        final InflateException ie = new InflateException(attrs.getPositionDescription()
                + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;

    } catch (ClassCastException e) {
        // If loaded class is not a View subclass
        final InflateException ie = new InflateException(attrs.getPositionDescription()
                + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    } catch (ClassNotFoundException e) {
        // If loadClass fails, we should propagate the exception.
        throw e;
    } catch (Exception e) {
        final InflateException ie = new InflateException(
                attrs.getPositionDescription() + ": Error inflating class "
                        + (clazz == null ? "<unknown>" : clazz.getName()), e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

2. 自定义View初体验

2.1 View类简介

  • 直观上:视图上的各种控件包括布局都是一种View
  • 代码上:View类是Android所有组件控件间接或者直接的父类

下图为View的继承关系图,红色控件为常用控件

img

自定义View,顾名思义,就是写自己所需要的控件。下面开始View第一步。

2.2 自定义View构造函数

观看控件的源码,控件都是或直接或间接的继承了View类进行操作。因此第一步继承View类,并继承四种构造函数如下。

public class MyTestView extends View {
    public static String TAG = "View";
    
    //第一类,MyTestView在代码中创建
    public MyTestView(Context context) {
        super(context);
    }
    
	//第二类,MyTestView在.xml的布局文件中创建
    //自定义属性从AttributeSet传入
    public MyTestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    //第三类,MyTestView有自定义style属性时调用
    public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //第四类,MyTestView设置自定义style resource文件时调用
    public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

2.3 绘制自定义View

绘制自定义View主要用到以下函数

public class MyTestView extends View {
	......
    @Override
    //用于被内部调用测量视图大小
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
      
    @Override
    //用于被内部调用安排视图位置
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    //用于被内部调用绘制视图样式
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

    }
}

2.3.1 测量View

View系统的绘制流程会从ViewRoot(源码位置是ViewRootImpl.java)的performTraversals()方法中开始调用performMeasure(),进而调用View的measure()方法。

然后onMeasure方法在源码中被measure方法调用,用来测量自定义View的大小,并且在measure方法注解中提及,想要自定义View必须要重写onMeasure方法。

同时,onMeasure方法的参数widthMeasureSpecheightMeasureSpec也有着重要意义,这两个参数是从MeasureSpec得到。

2.3.1.1 MeasureSpec

MeasureSpec封装了父类给子类的布局要求,widthMeasureSpecheightMeasureSpec即对宽度、高度的要求。

MeasureSpec实质上是一个32位int值,由测量模式SpecMode和测量模式下的大小值SpecSize组成,高两位是测量模式,有三种模式,低30位是测量模式下的大小值。

UNSPECIFIED

父类不指定尺寸也不限制尺寸,随便继承的子类如何定义尺寸。

EXACTLY

父类指定子类具体尺寸

AT_MOST

父类指定子类的最大尺寸

定义如下:

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        public static final int EXACTLY     = 1 << MODE_SHIFT;

        public static final int AT_MOST     = 2 << MODE_SHIFT;
     	......
 }
2.3.1.2 DecorView

正常情况下,我们直接按照MeasureSpec来进行指定即可。对于一般View,确实好像没有问题,但是仔细思考一下,那么XML中指定属性match_parentwrap_content从哪儿得到的?LinearLayout是个ViewGroupViewGroup继承于View,当Activity启动时,最开始的根视图是谁呢,这个根视图又是如何获得宽高的呢?

这时候就要引入LayoutParamsDecorView的概念。

首先LayoutParams比较简单,就是布局所需要的宽高设置。

ViewGroup.java
    
public static class LayoutParams {
	......
    @SuppressWarnings({"UnusedDeclaration"})
    @Deprecated
    public static final int FILL_PARENT = -1;

    public static final int MATCH_PARENT = -1;

    public static final int WRAP_CONTENT = -2;
    ......
}

因此对于一般View,它的宽高是由父容器的MeasureSpec 和自身 LayoutParams一起决定的。关于XML中标签指定宽高的问题已经解决了。

那么进入第二个问题,根视图是谁的呢?它是如何获取宽高呢?

最顶层也就是最外层的根视图我们称之为DecorView.

如下图,Activity是一个PhoneWindow实例,其布局形式即为DecorViewDecorView是一个FrameLayout布局,有标题栏(ActionBar)和内容视图(ContentView, 也就是每个活动我们所调用的函数SetContentView) 在这里插入图片描述

那么现在来看,DecorView又是如何获得宽高的呢?

ViewRootImplmeasureHierarchy中有着如下一段代码,就是DecorViewMeasureSpec的赋值。

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

再来看下getRootMeasureSpec的实现:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
		
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

可以看出,三种模式,且都和windowSize有关,即可以理解为根视图/最外层视图的宽高是由窗口尺寸和自身LayoutParams决定的。

我们可以得出结论:

  • 对于一般视图View,它的宽高是由父容器的MeasureSpec 和自身 LayoutParams一起决定的。
  • 对于根视图DecorView,它的宽高是由窗口尺寸和自身 LayoutParams一起决定的。
2.3.1.3 onMeasure

根据getDefaultSize函数来给定高度或者宽度的大小,然后使用setMeasuredDimension函数来指定自定义View的(尺寸)高度、宽度

getSuggestedMinimumWidth用来获取内容或者背景尺寸二者中的较大值。

   	   public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                //父类子类不同种View 则调整宽高
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }

            // Suppress sign extension for the low bytes
            long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
            if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

            final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

            // Optimize layout by avoiding an extra EXACTLY pass when the view is
            // already measured as the correct size. In API 23 and below, this
            // extra pass is required to make LinearLayout re-distribute weight.

            //宽高是否发生了变化
            final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                    || heightMeasureSpec != mOldHeightMeasureSpec;
			
           //是否是EXACT模式
           final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
           
           //匹配
            final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                    && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
            final boolean needsLayout = specChanged
                    && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

            if (forceLayout || needsLayout) {
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

                resolveRtlPropertiesIfNeeded();

                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }

                // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                    throw new IllegalStateException("View with id " + getId() + ": "
                            + getClass().getName() + "#onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
                }

                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
            }

            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;

            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                    (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
       //如果当前View与父类View不是同种View
        if (optical != isLayoutModeOptical(mParent)) {
            //不同就要调整测量值大小
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
	//直接赋值
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
		//打上标识,已经测量该View的大小
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

	//获取默认大小
	//根据三种模式调整再次调整大小
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
		
        //三种模式
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED: //子类自身决定
            result = size;
            break;
        case MeasureSpec.AT_MOST: //父类决定的两种
        case MeasureSpec.EXACTLY: 
            result = specSize;
            break;
        }
        return result;
    }
	//获取内容或者背景尺寸二者中的较大值
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

上述过程就是一个单一View的测量过程,当然对于ViewGroup来说其包含多个子View,因此在ViewGroup的源码中有measureChildren来测量子View的尺寸。

   protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            //子View的测量过程
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
		//getChildMeasureSpec方法,大致过程也是设置size和mode
        //其中与LayoutParams.MATCH_PARENT以及 LayoutParams.WRAP_CONTENT进行匹配判断
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

2.3.2 布局View

测量好View的大小之后,performTraversals继续会调用performLayout方法,进而调用Viewlayout方法,然后onLayout方法在源码中被layout方法调用,用来在视图中给View布局。因此,该步骤主要是给予View在布局中的位置。

   public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
	//记录四个坐标,左上、右下
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
		//判断当前视图大小是否发生了变化,发生变化需要对当前视图重新绘制
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //如果发生了变化,确定View在布局中的位置
            onLayout(changed, l, t, r, b);
            ......
        }
	......
    }

setOptionalFramesetFrame函数的主要目的就是给View确定位置并判断位置是否变化,setOptionalFrame内核也是调用setFrame函数,因此直接分析setFrame源码。

   protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
		//判断位置是否变化,变化则需要重新绘制
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
			
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            //存储新的位置并设置
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
			......
        }
        return changed;
    }

对于我们来说,我们仅需在代码中重写onLayout()函数,如下。

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

接着,我们追溯到View的onLayout源码,发现......

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

???小朋友你是否有很多问号,突然想了想,好像确实应该为空,因为自定义View的位置本该就由其父布局决定,即父布局(一般继承ViewGroup)决定其子View布局(一般继承View类)。那就到ViewGroup里一探究竟吧。

    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

然而貌似问号越来越多,ViewGroup的layout最关键的部分还是调用其父类Viewlayout函数(是的,ViewGroup的父类也是View)来确定自身位置。而ViewGrouponLayout函数却是一个抽象方法??

仔细考虑一下,ViewGrouponLayout()函数是抽象方法是正确的,因为每个ViewGroup都有着自己的独特布局,如LinearLayoutRelativeLayout等等对于子View的布局规则是不同的,所以写成抽象方法后方便后来继承者自定义自己的布局规则。

接下来看看LinearLayoutonLayout实现

LinearLayout有两种布局形式,以其中一种作为例子分析。

为了好理解布局中一些属性(如margin、padding等),从浏览器中抠出一张图仅供参考。

浏览器body结构图

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);//纵向线性布局
        } else {
            layoutHorizontal(l, t, r, b);//横向线性布局
        }
    }

    /**
     * Position the children during a layout pass if the orientation of this
     * LinearLayout is set to {@link #VERTICAL}.
     *
     * @see #getOrientation()
     * @see #setOrientation(int)
     * @see #onLayout(boolean, int, int, int, int)
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;//左内填充

        int childTop;//上位置
        int childLeft;//左位置

        // Where right end of child should go
        final int width = right - left;//宽度
        int childRight = width - mPaddingRight;//去掉内填充的右位置

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;//内容部分要用宽度去除内填充的长度
		//获得子View数量 getVirtualChildCount()调用的就是getChildCount()
        final int count = getVirtualChildCount();
        
        //对齐方式,通过改变子ViewTop值
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
		//循环遍历子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);//子View的Top是基于上一个的,nullChild的值为0
            } else if (child.getVisibility() != GONE) {//这就解释了为什么GONE状态不会占用布局内容
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                //确定子View位置,并判断是否变化
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);//下一个子View的Top位置

                i += getChildrenSkipCount(child, i);
            }
        }
    }

最后做一个小小的实践。 布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.myviewlearning.TestLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>
</com.example.myviewlearning.TestLayout>

TestLayout.java如下:

public class TestLayout extends ViewGroup {

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

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

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

    public TestLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
	//上述四类和继承View的自定义View相同
    
    //注意一个顺序,在Measure测量以前,getMeasureWidth和getMeasureHeight两个函数返回值为0
    //这里只给了一个子View
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        //循环遍历子View
        for(int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);//测量子View
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
           View childView = getChildAt(0);
           childView.layout(0, 0, childView.getMeasuredWidth() + 100, childView.getMeasuredHeight() + 100);//给子View安排位置
			Log.d("View Size", "Width: "+ Integer.toString(childView.getWidth()) + " height:" + Integer.toString(childView.getHeight()));
            Log.d("View MeasuredSize", "MeasureWidth: "+ Integer.toString(childView.getMeasuredWidth()) + " height:" + Integer.toString(childView.getMeasuredHeight()));
        }
    }
}

onLayout运行结束后,我们可以通过getWidthgetHeight方法来获取子View的高和宽。

注意:

childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());//给子View安排位置

一般来说childView.getWidth()childView.getMeasuredWidth()会相同,原因是上述给子View布局的这句代码。

getWidth() = childView.getMeasuredWidth() - 0(就是 right - left);

但是实际上两者意义是不同的。

childView.getWidth()主要是用来表示当前childView在该布局中的宽度,只有在layout过程过后才有值。

childView.getMeasuredWidth()主要是用来测量视图本身的大小,在measure之后即可获取到值。

2.3.3 绘制View

测量好View(measure),给View布局好位置(layout),ViewRoot中会继续执行调用performDraw

private void performDraw() {
	......
    try {
        boolean canUseAsync = draw(fullRedrawNeeded);
		......
		}
    ......
}

performDraw调用自身的draw方法,在drawSoftware中创建出一个Canvas对象进行一些基本绘制(如背景颜色)并且真正的调用View类中的draw方法,传入创建的Canvas对象。

ViewRootImpl.java
    
private boolean draw(boolean fullRedrawNeeded) {
    .......
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                    scalingRequired, dirty, surfaceInsets)) {
                return false;
            }
        }
    }
......
}

  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

        // Draw with software renderer.
        final Canvas canvas;
		......
        try {
            if (DEBUG_ORIENTATION || DEBUG_DRAW) {
                Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
                        + canvas.getWidth() + ", h=" + canvas.getHeight());
                //canvas.drawARGB(255, 255, 0, 0);
            }

            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

            ......
            try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;

                mView.draw(canvas);

                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            } finally {
                if (!attachInfo.mSetIgnoreDirtyState) {
                    // Only clear the flag if it was not set during the mView.draw() call
                    attachInfo.mIgnoreDirtyState = false;
                }
            }
        } finally {
			......
        }
        return true;
    }

然后调用View的draw()方法来执行具体的开始绘制(draw)View,最后onDraw方法被View类中的draw方法调用进行内容绘制,内容绘制也是最关键的一步。

绘制过程主要分六步骤,其中第二、第五步骤相对使用较少。

    View.java
	public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *      1. 绘制背景
         *      2. 保存当前canvas,非必须
         *      3. 绘制View的内容
         *      4. 绘制子View
         *      5. 绘制边缘、阴影等效果,非必须
         *      6. 绘制装饰,如滚动条等
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);
            
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            .......
            // we're done...
            return;
        }
		......
    }

  private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();
		......
    }

    void setBackgroundBounds() {
        if (mBackgroundSizeChanged && mBackground != null) {
            mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            rebuildOutline();
        }
    }

从代码中看出,第一步,背景的绘制实际上调用了一个Drawable对象mBackground进行背景绘制,然后根据layout过程确定的视图位置(mLeft mRight mTop mBottom)来设置背景的绘制区域,之后再调用onDraw方法来完成背景的绘制工作。

而这个mBackground对象其实就是在XML中通过android:background属性设置的图片或颜色。当然也可以在代码中通过setBackgroundColor()setBackgroundResource()等方法进行赋值。

跳过第二步,来到第三步骤,调用onDraw方法对View内容进行绘制,但是有了onLayout经验,这里onDraw方法同样需要被重写。

第四步,进行子View绘制处理,当然对于View来说,dispatchDraw是空方法,因为没有子View,但是对于ViewGroup来说,dispatchDraw还是比较复杂的。

跳过第五步,来到最后一步,对视图的滚动条进行装饰,从这里其实就可以看出,其实所有的控件都是有着自己的滚动条的,只不过被隐藏了起来。

最后同样来个小实践。 布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/main_layout"
    tools:context=".MainActivity">

    <com.example.myviewlearning.MyTestView
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:background="#000000"/>


</LinearLayout>

onDraw()重写如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height)/2;
        
       	//画圆
        canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, radius, mPaint);
        
        //设置text和textColor
        mPaint.setTextSize(88);
        String text = "Hi, my view";
        mPaint.setColor(Color.WHITE);
        canvas.drawText(text,width/3, height/2, mPaint);

    }

2.3.4 绘制自定义View总结

到此为止,我们就明白了,要想能够自定义一个View,少不了这三个步骤。

测量------>布局------>绘制测量------>布局------>绘制