Android 源码系列之<四>从源码的角度深入理解LayoutInflater.Factory之主题切换(上)

来源:互联网 发布:淘宝不开发票怎么办 编辑:程序博客网 时间:2024/05/20 18:52

        转载请注明出处:http://blog.csdn.net/llew2011/article/details/51252401

        现在越来越多的APP都加入了主题切换功能或者是日间模式和夜间模式功能切换等,这些功能不仅增加了用户体验也增强了用户好感,众所周知QQ和网易新闻的APP做的用户体验都非常好,它们也都有日间模式和夜间模式的主题切换功能。体验过它们的主题切换后你会发现大部分效果是更换相关背景图片、背景颜色、字体颜色等来完成的,网上这篇文章对主题切换讲解的比较不错,今天我们从源码的角度来学习一下主题切换功能,如果你对这块非常熟悉了,请跳过本文(*^__^*) …

        在开始讲解主题切换之前我们先看一下LayoutInflater吧,大家都应该对LayoutInflater的使用非常熟悉了(如果你对它的使用还不是很清楚请自行查阅)。LayoutInflater的使用场合非常多,常见的比如在Adapter的getView()方法中,在Fragment中的onCreateView()中使用等等,总之如果我们想要把对应的layout.xml文件渲染成对应的View层级视图,离开LayoutInflater是不行的,那么我们如何获取LayoutInflater实例并用其来渲染成对应的View实例对象呢?一般有以下几种方式:

  1. 调用Context.getSystemService()方法
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);View rootView = inflater.inflate(R.layout.view_layout, null);
  2. 直接使用LayoutInflater.from()方法
    LayoutInflater inflater = LayoutInflater.from(context);View rootView = inflater.inflate(R.layout.view_layout, null);
  3. 在Activity下直接调用getLayoutInflater()方法
    LayoutInflater inflater = getLayoutInflater();View rootView = inflater.inflate(R.layout.view_layout, null);
  4. 使用View的静态方法View.inflate()
    rootView = View.inflate(context, R.layout.view_layout, null);
        以上4种方式都可以渲染出一个View实例出来但也都是借助LayoutInflater的inflate()方法来完成的,我们先看一下方式2中LayoutInflater.from()是怎么做的,代码如下:
/** * Obtains the LayoutInflater from the given context. */public static LayoutInflater from(Context context) {    LayoutInflater LayoutInflater =            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);    if (LayoutInflater == null) {        throw new AssertionError("LayoutInflater not found.");    }    return LayoutInflater;}
        LayoutInflater.from()方法只不过是对方式1的一层包装,最终还是通过调用Context的getSystemService()方法获取到LayoutInflater实例对象,然后通过返回的LayoutInflater实例对象调用其inflate()方法来完成对xml布局文件的渲染并生成相应的View对象。通过和方式1对比你会发现,这两种方式中的Context如果是相同的那么获取的LayoutInflater对象应该是同一个。然后我们在看一下方式3中的实现部分,方式3是在Activity中直接调用Activity的getLayoutInflater()方法,源码如下:
/** * Convenience for calling * {@link android.view.Window#getLayoutInflater}. */public LayoutInflater getLayoutInflater() {    return getWindow().getLayoutInflater();}
        通过源码发现Activity的getLayoutInflater()方法辗转调用到了getWindow()的getLayoutInflater()方法,getWindow()方法返回一个Window类型的对象,其中Window为抽象类在Android中该类的实现类是PhoneWindow,也就是说getWindow().getLayoutInflater()方法最终调用的是PhoneWindow的getLayoutInflater()方法,我们看一下PhoneWindow类中该方法的实现过程,代码如下:
/** * Return a LayoutInflater instance that can be used to inflate XML view layout * resources for use in this Window. * * @return LayoutInflater The shared LayoutInflater. */@Overridepublic LayoutInflater getLayoutInflater() {    return mLayoutInflater;}
        在PhoneWindow类中直接返回了mLayoutInflater对象,那么mLayoutInflater是在何时何地完成初始化的呢?我们继续查看mLayoutInflater的初始化在哪完成的,通过查看代码发现是在PhoneWindow的构造方法中完成初始化的,代码如下:
public PhoneWindow(Context context) {    super(context);    mLayoutInflater = LayoutInflater.from(context);}
        我们暂且不关心PhoneWindow是何时何地完成初始化的,我们只关心mLayoutInflater的初始化也是直接调用LayoutInflater.from()方法来完成的,这种方式和方式2是一样的,都是借助传递进来的context调用其getSystemService()方法获取到LayoutInflater实例,也就是说只要PhoneWindow中传递进来的context和方式1、方式2是相同的,那么可以确定获取到的mLayoutInflater的实例就是同一个。接着我们看方式4的通过调用View的静态方法inflate()的内部流程是怎样的,代码如下:
/** * Inflate a view from an XML resource.  This convenience method wraps the {@link * LayoutInflater} class, which provides a full range of options for view inflation. * * @param context The Context object for your activity or application. * @param resource The resource ID to inflate * @param root A view group that will be the parent.  Used to properly inflate the * layout_* parameters. * @see LayoutInflater */public static View inflate(Context context, int resource, ViewGroup root) {    LayoutInflater factory = LayoutInflater.from(context);    return factory.inflate(resource, root);}
        在View的inflate()静态方法中先是根据传递进来的context通过LayoutInflater.from()方法来获取一个LayoutInflater实例对象,然后调用LayoutInflater的inflate()方法来完成把layout.xml布局文件渲染成对应的View层级视图然后返回。
        通过对以上代码的分析我们可以得以下出结论:前边说的无论以哪种方式来渲染View视图都会先获取到LayoutInflater的实例,然后通过调用该实例的inflate()方法把xml布局文件渲染出相应的View层级视图,而获取LayoutInflater实例是需要Context的,那也就是说如果传入的Context对象是同一个那么获取的LayoutInflater实例也是相同的。这也是我用不小的篇幅从源码的角度说明这一点的原因所在。
        现在我们已经清楚了渲染View是由LayoutInflater来完成的,那么在Activity的onCreate()方法中通过调用setContentView()为当前Activity设置显示内容是不是也是通过LayoutInflater的inflater()方法完成的呢?我们接着看代码,看看Activity的setContentView()里是如何操作的,代码如下:
/** * Set the activity content from a layout resource.  The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. *  * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */public void setContentView(int layoutResID) {    getWindow().setContentView(layoutResID);    initActionBar();}
        setContentView()方法中只是做了一个中转,接着是调用Window实例的setContentView()方法,刚刚也说过Window为抽象类,它的实现类为PhoneWindow,那也就是最终调用的是PhoneWindow的setContentView()方法,我们看一下PhoneWindow的setContentView()方法,源码如下:
@Overridepublic void setContentView(int layoutResID) {    if (mContentParent == null) {        installDecor();    } else {        mContentParent.removeAllViews();    }    // 这里同样是调用了LayoutInflater的inflate()方法    mLayoutInflater.inflate(layoutResID, mContentParent);    final Callback cb = getCallback();    if (cb != null && !isDestroyed()) {        cb.onContentChanged();    }}
        从源码可以看到在PhoneWindow的setContentView()方法中也同样使用的是LayoutInflater的inflate()方法。到这里我们就可以总结出结论:无论是我们自己渲染View还是说为Activity设置显示内容都是借助LayoutInflater来完成的,而获取LayoutInflater最终都是通过Context.getSystemService()来得到的,如果Context相同,那么获取的LayoutInflater的实例是相同的。
        好了,用了不少篇幅讲解了有关LayoutInflater的知识都是给主题切换功能做铺垫的,那怎么利用LaoutInflater来完成主题切换功能呢?别着急,我们再看一下LayoutInflater的源码,打开LayoutInflater的源码你会发现,其内部定义了Factory,Factory2等接口,这两个接口是干嘛的了?其实他们俩功能是一样的,Factory2是对Factory的完善,先看Factory的定义说明,代码如下:
public interface Factory {    /**     * Hook you can supply that is called when inflating from a LayoutInflater.     * You can use this to customize the tag names available in your XML     * layout files.     *      * <p>     * Note that it is good practice to prefix these custom names with your     * package (i.e., com.coolcompany.apps) to avoid conflicts with system     * names.     *      * @param name Tag name to be inflated.     * @param context The context the view is being created in.     * @param attrs Inflation attributes as specified in XML file.     *      * @return View Newly created view. Return null for the default     *         behavior.     */    public View onCreateView(String name, Context context, AttributeSet attrs);}
        接口Factory中定义了onCreateView()方法,该方法返回一个View实例。我们看看该方法的说明,大致意思是说:当我们使用LayoutInflater来渲染View的时候此方法可以支持做Hook操作,我们可以在xml布局文件中使用自定义标签,需要注意的是不要使用系统名字。那么这里究竟该如何使用了?我们先梳理一下使用LayoutInflater渲染View的流程,以方式2为例子做说明吧,在方式2中rootView是由inflater.inflate()方法生成的,我们进入inflate()方法中看一下其内部的执行流程,代码如下:
/** * Inflate a new view hierarchy from the specified xml resource. Throws * {@link InflateException} if there is an error. *  * @param resource ID for an XML layout resource to load (e.g., *        <code>R.layout.main_page</code>) * @param root Optional view to be the parent of the generated hierarchy. * @return The root View of the inflated hierarchy. If root was supplied, *         this is the root View; otherwise it is the root of the inflated *         XML file. */public View inflate(int resource, ViewGroup root) {    return inflate(resource, root, root != null);}
        inflate()方法中什么都没做直接调用了其同名的重载方法inflate(),我们接着往里跟进,代码如下:
/** * Inflate a new view hierarchy from the specified xml resource. Throws * {@link InflateException} if there is an error. *  * @param resource ID for an XML layout resource to load (e.g., *        <code>R.layout.main_page</code>) * @param root Optional view to be the parent of the generated hierarchy (if *        <em>attachToRoot</em> is true), or else simply an object that *        provides a set of LayoutParams values for root of the returned *        hierarchy (if <em>attachToRoot</em> is false.) * @param attachToRoot Whether the inflated hierarchy should be attached to *        the root parameter? If false, root is only used to create the *        correct subclass of LayoutParams for the root view in the XML. * @return The root View of the inflated hierarchy. If root was supplied and *         attachToRoot is true, this is root; otherwise it is the root of *         the inflated XML file. */public View inflate(int resource, ViewGroup root, boolean attachToRoot) {    if (DEBUG) System.out.println("INFLATING from resource: " + resource);    XmlResourceParser parser = getContext().getResources().getLayout(resource);    try {        return inflate(parser, root, attachToRoot);    } finally {        parser.close();    }}
        在该inflate()方法中通过调用getContext().getResource().getLayout()的方式根据传递进来的布局资源ID生成一个XmlResourceParser实例对象parser,这个parser就是用来解析布局文件的(有关在Java中如何解析xml文件,请自行查阅,这里不再介绍),根据资源ID获取到解析器parser后调用了参数有XmlPullParser的重载方法inflate(),我们继续进入该代码中看一下执行流程,代码如下:
/** * Inflate a new view hierarchy from the specified XML node. Throws * {@link InflateException} if there is an error. * <p> * <em><strong>Important</strong></em>   For performance * reasons, view inflation relies heavily on pre-processing of XML files * that is done at build time. Therefore, it is not currently possible to * use LayoutInflater with an XmlPullParser over a plain XML file at runtime. *  * @param parser XML dom node containing the description of the view *        hierarchy. * @param root Optional view to be the parent of the generated hierarchy (if *        <em>attachToRoot</em> is true), or else simply an object that *        provides a set of LayoutParams values for root of the returned *        hierarchy (if <em>attachToRoot</em> is false.) * @param attachToRoot Whether the inflated hierarchy should be attached to *        the root parameter? If false, root is only used to create the *        correct subclass of LayoutParams for the root view in the XML. * @return The root View of the inflated hierarchy. If root was supplied and *         attachToRoot is true, this is root; otherwise it is the root of *         the inflated XML file. */public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {    synchronized (mConstructorArgs) {        final AttributeSet attrs = Xml.asAttributeSet(parser);        Context lastContext = (Context)mConstructorArgs[0];        mConstructorArgs[0] = mContext;        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("**************************");            }            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, attrs, false);            } else {                // Temp is the root view that was found in the xml                View temp;                if (TAG_1995.equals(name)) {                    temp = new BlinkLayout(mContext, attrs);                } else {                    temp = createViewFromTag(root, name, attrs);                }                ViewGroup.LayoutParams params = null;                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) {                        // 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");                }                // Inflate all children under temp                rInflate(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) {                    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;                }            }        } catch (XmlPullParserException e) {            InflateException ex = new InflateException(e.getMessage());            ex.initCause(e);            throw ex;        } catch (IOException e) {            InflateException ex = new InflateException(                    parser.getPositionDescription()                    + ": " + e.getMessage());            ex.initCause(e);            throw ex;        } finally {            // Don't retain static reference on context.            mConstructorArgs[0] = lastContext;            mConstructorArgs[1] = null;        }        return result;    }}
        该方法代码有点长,但也是我们今天要讲解的重点,主要逻辑就是递归解析布局文件并创建View树结构,然后返回该View树结构。该段代码中先通过Xml类的静态方法生成一个AttributeSet实例对象attrs,AttributeSet对象我们应该很熟悉,里边主要包含了相关属性的键值对。接下来就是通过parser解析器循环遍历查询布局文件的根节点,若没有查询到就会抛出异常。遍历完成之后获取到根节点名字存储在变量name中,然后进行判断。如果当前根节点标签名字是mege标签就走if()语句,否则进入else语句。由于我们在布局文件中没有使用merge标签,所以直接进入else语句中。进入else语句后,先定义值为null的临时变量temp,接着开始做判断,如果当前根节点标签名字为BlinkLayout就进入if语句,因为我们没有使用这个标签就进入else语句,在else语句中通过调用createViewFromTag()来创建一个View并赋值给temp。接下来又是条件判断,因为传递进来的root为空,所以跳过if(root != null)的判断语句,接着执行rInflate()方法(该方法是来循环渲染包含的所有的子视图的)。执行完成后返回temp的值。
        我们进入crateViewFromTag()方法中看一下里边的执行流程,代码如下:
View createViewFromTag(View parent, String name, AttributeSet attrs) {    if (name.equals("view")) {        name = attrs.getAttributeValue(null, "class");    }    if (DEBUG) System.out.println("******** Creating view: " + name);    try {        View view;        if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);        else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);        else view = null;        if (view == null && mPrivateFactory != null) {            view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);        }                if (view == null) {            if (-1 == name.indexOf('.')) {                view = onCreateView(parent, name, attrs);            } else {                view = createView(name, null, attrs);            }        }        if (DEBUG) System.out.println("Created view is: " + view);        return view;    } catch (InflateException e) {        throw e;    } catch (ClassNotFoundException e) {        InflateException ie = new InflateException(attrs.getPositionDescription()                + ": Error inflating class " + name);        ie.initCause(e);        throw ie;    } catch (Exception e) {        InflateException ie = new InflateException(attrs.getPositionDescription()                + ": Error inflating class " + name);        ie.initCause(e);        throw ie;    }}
        方法createViewFromTag()主要流程就是通过标签名name来创建相应View实例对象并返回。在该方法中首先根据Factory实例对象来创建View,如果创建成功就直接返回,否则执行系统默认创建View流程。这里需要强调一点,LayoutInflater内部定义了一个boolean类型的mFactorySet开关,其值默认值为false,当我们调用过setFactory()或者是setFactory2()后mFactorySet为true,若我们再次调用这俩方法时会抛出异常,也就是说每一个LayoutInflater实例对象只能赋值一次Factory,若再想赋成其他值只能通过反射先把mFactorySet的值置为false防止抛异常。系统默认创建View流程是先通过判断标签名称中有没有包含".",如果没有包含就把标签名添加前缀"android.view.",最终调用LayoutInflater的createView()方法,注意该方法是public并且是final类型的,是系统默认的创建View的方式,创建完成之后返回该view。
        到这里我们已经清楚了LayoutInflater根据xml布局文件来渲染View视图的主要流程:先是通过布局文件的资源ID创建一个XmlResourceParser解析器对象parser,再是利用parser递归解析xml布局文件,然后根据解析出的标签名来创建相关View,最终返回层级视图View。如果LayoutInflater中设置了Factory,那么在创建每一个View时都会调用该Factory的onCreateView()方法,这个方法就是我们的入口点,如果想在每一个View创建之前做点处理,只需要在Factory的onCreateView()方法中做相关逻辑操作...
        既然已经找到了创建View的切入口,那怎么样才能实现主题切换功能呢?主题切换通常是更改背景以及文字颜色等,在做更改之前要先知道哪些View需要更改,那我们怎么才能知道布局文件中的View需要做主题切换了?自定义属性是推荐的做法,当布局文件中使用了自定义属性就表示该View是做主题切换功能的,在该View创建后把它装入集合中,当需要主题切换时循环遍历该集合更改View相关属性就好了...
        好了,由于篇幅的缘故,本篇博文先到这里,我会在下篇文章Android 源码系列之<五>从源码的角度深入理解LayoutInflater.Factory之主题切换(中)中以案例的形式演示如何利用LayoutInflater的Factory接口实现切换主题的功能,敬请期待……




7 0
原创粉丝点击