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

        转载请注明出处: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.
 */
@Override
public 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()方法,源码如下:
@Override
public 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接口实现切换主题的功能,敬请期待……




发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/51252401