LayoutInflater Factory创建自定义View


我们每一个安卓开发者都知道xml布局可以显示界面,那么作为有追求的开发者我们要知其然更要知其所以然。搞清楚xml布局在源码层面最终是通过什么方式转换成什么?了解背后原理后又能带来什么样的收益?带着种种疑问我们来深究源码去揭开其魅力的面纱。

一、LayoutInflater类

LayoutInflater被用在哪里

有如下两个地方:

  1. LayoutInflater用于代码动态创建View
  2. LayoutInflater用于Activity界面初始化View

下面就让我们来一一介绍

1. LayoutInflater用于代码动态创建View

以下是一般从xml布局文件创建View对象的方法

方法一:
	View view = LayoutInflater.from(context()).inflate(layoutResId, viewGroup, false);

方法二:
	View view = View.inflate(viewGroup.getContext(), layoutResId, null);

方法三:
	View view = ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(viewGroup.getContext(), layoutResId, null);

这三个方法内部都是context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)拿到LayoutInflater服务实现填充xml布局。
最终的调用是LayoutInflater类内部的inflate方法,如下:

	public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
	    final Resources res = getContext().getResources();
	    ......
	    //预编译:如果支持,则从布局文件xml预编译生成的dex文件,通过反射来获取对应的View,来减少xml布局用解析器解析的时间
	    //初始化时设置不支持
	    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
	    if (view != null) {
	        return view;
	    }
	    //如果没有预编译机制看,则获取XML的资源解析器
	    XmlResourceParser parser = res.getLayout(resource);
	    try {
	        return inflate(parser, root, attachToRoot);
	    } finally {
	        parser.close();
	    }
	}

tryInflatePrecompiled()由于目前在release版本不支持,仅支持CTS tests,所以会通过Resources.getLayout方法去获取xml解析器XmlResourceParser,具体怎么获取我们在这里不做展开讨论,接下来将XmlResourceParser作为参数调用inflate()的重载方法

	public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ......
            
            try {
                advanceToRootNode(parser); //通过while循环找到布局根节点,类似<LinearLayout>
                final String name = parser.getName();
                
                if (TAG_MERGE.equals(name)) { //根节点是merge,则遍历布局生成View
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
					// 内部循环调用createViewFromTag()创建View
                    rInflate(parser, root, inflaterContext, attrs, false); 
                } else { //根节点非merge,则通过其他方法创建View对象
                    // Temp is the root view that was found in the xml  1、创建根节点temp
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs); //本篇后文分析方法
                    
                    ......
                    
                    // Inflate all children under temp against its context.  2、遍历布局生成View
                    // 内部调用rInflate()
                    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.
                    if (root != null && attachToRoot) { //将根节点temp添加到root
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            }......

            return result;
        }
    }

逻辑如下:
1、找到布局的根节点XmlPullParser.START_TAG,例如<LinearLayout>
2、布局根节点是merge的话,则通过递归遍历子布局生成View对象;
3、非merge开头的,则通过createViewFromTag()(后文介绍该方法)创建根节点temp,然后再通过遍历子布局生成View对象;
4、将生成的temp对象添加到root下面。

经过上面一些列的操作后,xml属性的布局文件就转为了View对象,里面存储了所有布局标签的节点。

2. LayoutInflater用于Activity界面初始化View

当我们在Activity中onCreate初始化的时候,会调用setContentView,追踪源码我们会发现调用PhoneWindow里面的setContentView(int layoutResID)方法

    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {   //当mContentParent==null,则当前内容没有在窗口出现过,即第一次调用
            installDecor(); //  创建并添加DecorView
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {  //else表示内容添加过,并且不需要动画,移除removeAllViews()
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { //有过度动画标志FEATURE_CONTENT_TRANSITIONS
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());
            transitionTo(newScene); //添加Scene处理过度动画来启动界面,执行完动画后执行inflate()
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);   //无过度动画,资源文件通过LayoutInflater对象转换为View树,并且添加至mContentParent视图中
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

hasFeature(FEATURE_CONTENT_TRANSITIONS)方法,这个是判断内容加载是否要使用过度动画。如果内容已经加载过并且不需要动画则removeAllViews();添加完Content后如有过度动画则添加Scene来过度启动,否则调用LayoutInflater.inflate去转换view。
  xml转换成对象View的整个流程图如下:
在这里插入图片描述

二、Factory接口

LayoutInflater类中有三个关于Factory的全局变量

	@UnsupportedAppUsage
    private Factory mFactory;
    @UnsupportedAppUsage
    private Factory2 mFactory2;
    @UnsupportedAppUsage
    private Factory2 mPrivateFactory;

Factory Factory2是在LayoutInflater.java中定义的两个接口

public interface Factory {
    @Nullable
    View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs);
}

public interface Factory2 extends Factory {
	// parent – The parent that the created view will be placed in; note that this may be null.
    @Nullable
 	View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs);
}

那这个Factory是用来干什么的呢?全文搜索发现在方法tryCreateView中调用

	public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        ......
        
        View view;
        if (mFactory2 != null) {
        	// 调用接口方法onCreateView()创建View
            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);
        }

        return view;
    }

可见调用了Factory接口方法onCreateView返回了View对象,这个tryCreateView方法是由createViewFromTag()方法调用的

	/**
     * Creates a view from a tag name using the supplied attribute set.
     * ......
     */
    @UnsupportedAppUsage
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ......
        try {
            View view = tryCreateView(parent, name, context, attrs);

			// 本文意在研究自定义Factory创建View,以下方法不做详细解析,有兴趣可自行研究
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) { // 原生View
                    	// 最终执行createView()
                        view = onCreateView(context, parent, name, attrs);
                    } else { //自定义View
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } ......
    }

而createViewFromTag是由inflate方法调用的,那么整个调用的回路为:
在这里插入图片描述
可见创建View最终是由Factory接口的实现类来完成的,那么我们就可以通过自定义LayoutInflater.Factory类来控制实现View对象,可以生成不同属性的View对象。那么实现方案可如下蓝色部分:
在这里插入图片描述
在setContentView之前将我们自定义的Factory实现类mFactory传递给LayoutInflater,当界面布局通过LayoutInflater去加载View的时候会直接使用我们前面传递过去的mFactory对象来回调,来达到加载我们自定义View的目的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值