转载请注明出处:http://blog.csdn.net/llew2011/article/details/51252401
现在越来越多的APP都加入了主题切换功能或者是日间模式和夜间模式功能切换等,这些功能不仅增加了用户体验也增强了用户好感,众所周知QQ和网易新闻的APP做的用户体验都非常好,它们也都有日间模式和夜间模式的主题切换功能。体验过它们的主题切换后你会发现大部分效果是更换相关背景图片、背景颜色、字体颜色等来完成的,网上这篇文章对主题切换讲解的比较不错,今天我们从源码的角度来学习一下主题切换功能,如果你对这块非常熟悉了,请跳过本文(*^__^*) …
在开始讲解主题切换之前我们先看一下LayoutInflater吧,大家都应该对LayoutInflater的使用非常熟悉了(如果你对它的使用还不是很清楚请自行查阅)。LayoutInflater的使用场合非常多,常见的比如在Adapter的getView()方法中,在Fragment中的onCreateView()中使用等等,总之如果我们想要把对应的layout.xml文件渲染成对应的View层级视图,离开LayoutInflater是不行的,那么我们如何获取LayoutInflater实例并用其来渲染成对应的View实例对象呢?一般有以下几种方式:
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View rootView = inflater.inflate(R.layout.view_layout, null);
LayoutInflater inflater = LayoutInflater.from(context); View rootView = inflater.inflate(R.layout.view_layout, null);
LayoutInflater inflater = getLayoutInflater(); View rootView = inflater.inflate(R.layout.view_layout, null);
rootView = View.inflate(context, R.layout.view_layout, null);
/** * 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层级视图然后返回。
/** * 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的实例是相同的。
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的值。
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。