前言
好久没有写博客了,都感觉有些生疏了。 总觉的人对自己要求高一些比较好,这样才进步比较快。接下来会继续给大家带来一些更有用的知识。 个人水平有限,如果感觉我的博客对您有用处,那就留个言给下鼓励;如果那里写的有误,请各位看客老爷多多拍砖! 注意:此处我使用的IDE是Android Studio
入门
相信每个人在学习Android时,都创建过很多Demo工程,那么下面的代码,大家一定非常眼熟了。
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 这句代码是为了讲解方便加的。目的是去掉标题。 requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); } }
代码的主要目的是,将布局文件activity_main.xml
展现到屏幕上。运行后的效果如下: 使用过View.inflate()
方法的朋友,肯定知道这个方法可以将一个XML布局文件,填充为一个View对象,相应的上面的代码就可以转化为这样子了:
View view = View.inflate(this, R.layout.activity_main, null); setContentView(view);
是不是感觉到了一丝丝的奇怪? Google作为一个伟大公司,里面的程序员也绝对都是大牛级别的人物,他们对方法的命名是肯定可以起到“见名知意”的作用的。 那么,问题来了。我们明明是给MainActivity设置activity_main布局,应该使用这样命名的方法setView(view)
才会显得更专业呀! 这里为什么使用的是setContentView(view)
呢? 其实,我们的布局都是被放置在一个FrameLayout的布局中的,由于此处就是给FrameLayout设置内容,那么使用setContentView(view)
也就不奇怪了? 在activity_main.xml中给根据加上id,获取一下它的父亲来看看它到底是什么吧。
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); View view = View.inflate(this, R.layout.activity_main, null); setContentView(view); RelativeLayout relativeLayout = (RelativeLayout) findViewById(R.id.rl_main); ViewParent parent = relativeLayout.getParent(); System.out.println(parent); }
打印的结果如下: 可以看到,activity_main.xml被转化成为一个View之后,确实放置在FrameLayout中了。看到此处,相信你一定明白了,为什么调用的是setContentView(view)
而不是setView()
了。 当然,到目前为止还都是开胃菜! 应用从启动到页面展示,Android系统都为我们额外做了哪些工作,请继续往下看!
视图树
为了搞清Android系统都为我们做了哪些额外的工作以及应用的视图树是怎么样的,就需要用到SDK的一个工具了,它放在了SDK目录下:/SDK/tools/hierarchyviewer.bat。 双击打开后,会看到如下的视图,其中黑色显示的是当前手机或者模拟器正在运行的APP。 双击进入黑色条目进入后,会看到下面的视图。这里面就是视图树了。 这个工具可以帮我们显示项目的View层级关系,在代码中由于去除掉了标题栏,所以显得清爽了很多。而绿色被选中的,就是我们R.layout.main布局文件了,右下方红色的代表屏幕上布局文件的区域。 就如我们上面所说的,Relativilayout布局是被嵌套在一个id为content的FrameLayout中的,这样也可以印证,在onCreate()
方法中,为什么设置布局的方法叫做setContetnView(view)
,而不是setView
(view)了。而ViewStub
是一个懒加载的空间,不占大小,默认为0,我们也就不用关心了。 从左侧开始看,当我们选中PhoneWindow$DectorView时,会发现右侧整个APP代表的屏幕空间都会以红色为边框亮起,这样就间接说明了DectorView是根布局了(暂且这么说,其实顶端还有个ViewRoot)。 补充一下,在这个视图中,PhoneWindow$DectoryView
这种形式的,$
之前是代表着一个类,$
之后是代表这个这个类中的一个内部类。
源码分析
了解这些,还远远不够,再进一步看看源码吧。此处,我的源码版本是API22的,使用的IDE是Android Studio。 点击setContentView(view)
方法,看一下内部实现,会发现调用了getWindow()
然后调用了其内部的setContentView()
方法。
public void setContentView(View view) { getWindow().setContentView(view); initWindowDecorActionBar(); }
而getWindow()
返回一个mWindow,mWindow是一个Window的变量,我们继续跟进一下,看看一下Widnow的源码,可以发现Window是一个抽象类,setContentView()
也是要求子类实现的抽象方法。
public abstract void setContentView(int layoutResID);
发现Window是一个抽象类,那么肯定会有它的实现类,在AS中,Ctrl+H,会发现Window的默认实现类是PhoneWindow,由于PhoneWindow被Google隐藏了,在Eclipse中,无法直接看到。 找到PhoneWindow中的setContentView()
方法,app第一次加载时mContentParent肯定为null,所以会调用installDecor()
。走完installDecor()
方法后,mContentParent也就有值了(可以推测出mContentParent就是FrameLayout),在第二个★处,使用布局填充器将activity_main.xml转化为View对象,并放置到mContentParent中。
@Override public void setContentView(int layoutResID) { if (mContentParent == null) { // ★ installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { // ★ mLayoutInflater.inflate(layoutResID, mContentParent); } final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }
installDecor()
是初始化装饰的意思,可以推测出,在这个方法中Android系统为我们做了很多操作,我们跟进一下。这个方法有200多行,此处只给出重要逻辑。
private void installDecor() { if (mDecor == null) { // ★ 生成装饰 mDecor = generateDecor(); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } if (mContentParent == null) { // ★ mContentParent = generateLayout(mDecor); // Set up decor part of UI to ignore fitsSystemWindows if appropriate. mDecor.makeOptionalFitsSystemWindows(); ...... } }
当首次进入此方法时,mDecor肯定为null,那么必然会进入到generateDecor()
方法,见其名知其意,就是生成装饰的意思。继续跟进一下。
protected DecorView generateDecor() { return new DecorView(getContext(), -1); }
方法很简单,new一个DecorView对象,第一个参数是上下文,那么第二个-1是代表什么呢?打开继承树,可以看到DecorView继承自一个FrameLayout,而FrameLayout是ViewGroup的孩子。
在ViewGroup中可以找到如下信息:
public static final int MATCH_PARENT = -1; public static final int WRAP_CONTENT = -2;
传入-1说明,DécorView对象默认填充屏幕,这也与我们在Hierarchy-Viwer中看到的现象一致。拿到mDecor后,继续向下走,由于mContentParent肯定为null,会走到 generateLayout(mDecor);
方法中,并将mDecor传入。我们继续跟进,方法的含义是,根据装饰生成布局,源码很长,此处只给出关键一些的。
protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. // 省略若干代码 ... ... ... // Inflate the window decor. int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleIconsDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_title_icons; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); // System.out.println("Title Icons!"); } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) { // Special case for a window with only a progress bar (and title). // XXX Need to have a no-title version of embedded windows. layoutResource = R.layout.screen_progress; // System.out.println("Progress!"); } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { // Special case for a window with a custom title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogCustomTitleDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_custom_title; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { // If no other features and not embedded, only need a title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleDecorLayout, res, true); layoutResource = res.resourceId; } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { layoutResource = a.getResourceId( R.styleable.Window_windowActionBarFullscreenDecorLayout, R.layout.screen_action_bar); } else { // ★★★★★ layoutResource = R.layout.screen_title; } // System.out.println("Title!"); } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { layoutResource = R.layout.screen_simple_overlay_action_mode; } else { // Embedded, so no decoration is needed. layoutResource = R.layout.screen_simple; // System.out.println("Simple!"); } mDecor.startChanging(); View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; // 省略若干代码 ... ... return contentParent; }
我们在之前使用了requestWindowFeature(Window.FEATURE_NO_TITLE);
给窗体设置了一个装饰,我们直接来到关键的代码处。在此处会不断的判断features到底是什么东西,经过一堆判断后,会达到上面代码中★★★★★处。
if ((features & (1 << FEATURE_NO_TITLE)) == 0) { ... ... // ★★★★★ layoutResource = R.layout.screen_title; }
这样走到这个if中,并得到一个layoutResource,它其实就是一个XML的布局,我们接着看一下R.layout.screen_title是什么样的布局。 R.layout.screen_title内容如下,一个LinearLayout中包含着两个FragmenLayout和一个ViewStub,由于我们设置了没有Title,那么第二个FrameLayout就不会显示出来,第三个就是我们id=content的帧布局。看到此处,是不是渐渐有些清晰了?
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:fitsSystemWindows="true"> <!-- Popout bar for action modes --> <ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionBarTheme" /> <FrameLayout android:layout_width="match_parent" android:layout_height="?android:attr/windowTitleSize" style="?android:attr/windowTitleBackgroundStyle"> <TextView android:id="@android:id/title" style="?android:attr/windowTitleStyle" android:background="@null" android:fadingEdge="horizontal" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout>
在generateLayout(DecorView decor)
方法的如下代码中,将screen_titlle.xml文件转化成的View对象添加到了decor中,也就是PhoneWindow$DecorView
中。
View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in;
最后,再回到PhoneWindow的setContentView(resId)
方法中,在378行通过mLayoutInflater.inflate(layoutResID, mContentParent);
将我们自己的布局文件,放置到mContentParent中。 至此,整个View界面的挂载与显示就结束了。
如果感觉,本篇博客讲的对你还有益处,请多多留言;如果讲的有误,也请多多拍砖,谢谢大家了!!!