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相关属性就好了...

好了,由于篇幅的缘故,本篇博文先到这里,我会在下一篇文章中以案例的形式演示如何利用LayoutInflater的Factory接口实现切换主题的功能,敬请期待……

时间: 2024-07-28 14:11:20

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

Android 源码系列之&lt;五&gt;从源码的角度深入理解LayoutInflater.Factory之主题切换(中)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51287391 在上篇文章中我们主要讲解了LayoutInflater渲染xml布局文件的流程,文中讲到如果在渲染之前为LayoutInflater设置了Factory,那么在渲染每一个View视图时都会调用Factory的onCreateView()方法,因此可以拿onCreateView()方法做切入口实现主题切换功能.如果你不清楚LayoutInflater的渲染流程,请点击这里.

Android 源码系列之&lt;十一&gt;从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(下)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52843637 在上篇文章Android 源码系列之<十>从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)中我们讲解了通过AccessibilityService实现自动安装APK小外挂的操作流程,如果你还没有看过上篇文章请点击这里.在这篇文章中我将带领小伙伴从源码的角度来深入学习一下AccessibilityServie的技术实现原理,希望这

hbase源码系列(五)单词查找树

在上一章中提到了编码压缩,讲了一个简单的DataBlockEncoding.PREFIX算法,它用的是前序编码压缩的算法,它搜索到时候,是全扫描的方式搜索的,如此一来,搜索效率实在是不敢恭维,所以在hbase当中单独拿了一个工程出来实现了Trie的数据结果,既达到了压缩编码的效果,亦达到了方便查询的效果,一举两得,设置的方法是在上一章的末尾提了. 下面讲一下这个Trie树的原理吧. hbase源码系列(五)单词查找树,布布扣,bubuko.com

Android 源码系列之&lt;十三&gt;从源码的角度深入理解LeakCanary的内存泄露检测机制(中)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52958563 在上篇文章Android 源码系列之<十二>从源码的角度深入理解LeakCanary的内存泄露检测机制(上)中主要介绍了Java内存分配相关的知识以及在Android开发中可能遇见的各种内存泄露情况并给出了相对应的解决方案,如果你还没有看过上篇文章,建议点击这里阅读一下,这篇文章我将要向大家介绍如何在我们的应用中使用square开源的LeakCanary库来检测应用中出

Spark源码系列(五)RDD是如何被分布式缓存?

这一章想讲一下Spark的缓存是如何实现的.这个persist方法是在RDD里面的,所以我们直接打开RDD这个类. def persist(newLevel: StorageLevel): this.type = { // StorageLevel不能随意更改 if (storageLevel != StorageLevel.NONE && newLevel != storageLevel) { throw new UnsupportedOperationException("C

Android 源码系列之&lt;十&gt;从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52822148 说起外挂特别是玩游戏的小伙伴估计对它很熟悉,肯定有部分小伙伴使用过,至于为什么使用它,你懂得(*^__^*) --我最早接触外挂是在大二的时候,那时候盛行玩QQ农场,早上一睁眼就是打开电脑先把自己的菜收了,收完之后再去偷别人的:后来童靴说非凡软件上有一个偷菜外挂,于是赶紧整了一个,有了外挂之后就告别了体力时代,省时又省力--既然在PC上有外挂,那在智能手机上可以做外挂呢?

我的Android 4 学习系列之开始入手:配置开发环境与理解Hello World!

p { padding-left: 10px; } 目录 如何安装Android SDK.创建开发环境和调试项目 移动设计中一些注意事项 使用Android虚拟设备.模拟器和其他开发工具 如何安装Android SDK.创建开发环境和调试项目 下载和安装Android SDK : 我的是window7系统,当然下载 SDK starter package 最合适了: http://developer.android.com/sdk/index.html 下载完打开压缩包如下: 然后把这个包解压到

Android 源码系列之&lt;七&gt;从源码的角度深入理解IntentService及HandlerThread

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51373243 提起Service大家都很熟悉,它乃Android四(si)大(da)组(jing)件(gang)之一.但是说起IntentService有童靴或许有点陌生,看名字感觉和Service有关连.不错,不仅有关联而且关系还不一般,IntentService是Service的子类,所以它也是正宗的Service,由于IntentService借助了HandlerThread,我

Android 源码系列之&lt;一&gt;从源码的角度深入理解ImageView的ScaleType属性

做Android开发的童靴们肯定对系统自带的控件使用的都非常熟悉,比如Button.TextView.ImageView等.如果你问我具体使用,我会给说:拿ImageView来说吧,首先创建一个新的项目,在项目布局文件中应用ImageView控件,代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.