Android之自定义View学习(一)

本文深入探讨了Android中LayoutInflater的工作原理,包括其在布局加载中的角色、XmlPullParser的使用、merge标签的理解以及inflate、rInflate、createViewFromTag等核心方法的详细解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android学习系列

Android之Room学习

Android之自定义View学习(一)

Android之自定义View学习(二)

Android之自定义View学习(一)

前言

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

LayoutInflater解析

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

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

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";

2. LayoutIInflater加载布局文件

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

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

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);
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标签

2.2 root和attachToRoot

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

3. 源码解析

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再进行诸多比较等.

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();
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>
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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT 涓涓清泉

感谢打赏,我会更加努力写更好的

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值