Android完美解析setContentView 你真的理解setContentView吗?
导读:
本篇文章的前半部分为源码分析,后半部分为一个例子,在例子中我们会遇到一些问题,从而回答前半部分留下的问题!
源码分析:
说到Activity的setContentView,咱们直接找到一个Activity中的setContentView点进去看看!
public void setContentView(View view) { getWindow().setContentView(view); initActionBar(); }
点进来之后我们发现它里边调用了getWindow.setContentView,我们点击getWindow看看里面是什么!
public Window getWindow() { return mWindow; }
返回了一个Window对象,这个mWindow就是Window的子类PhoneWindow,每一个Activity都有一个PhoneWindow对象,至于他们是怎么联系起来的我们就不去研究了,好了现在我们来到了第一层!
我们在PhoneWindow中找到了setContentView的实现
public class PhoneWindow extends Window implements MenuBuilder.Callback { //... //... //... //老大 @Override public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor(); } else { mContentParent.removeAllViews(); } mLayoutInflater.inflate(layoutResID, mContentParent); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } } //老二 @Override public void setContentView(View view) { setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } //和老三 @Override public void setContentView(View view, ViewGroup.LayoutParams params) { if (mContentParent == null) { installDecor(); } else { mContentParent.removeAllViews(); } mContentParent.addView(view, params); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } } //... //... //...}
我们看到首先判断了mContentParent,那么这个mContentParent是个什么呢?当mContentParent为空的时候,会执行installDecor()方法,那么我们肯定是到installDecor中去找答案咯,点进去!
private void installDecor() { if (mDecor == null) { mDecor = generateDecor(); //... } } if (mContentParent == null) { mContentParent = generateLayout(mDecor); //... } } } }
我把代码能删的都给删了,我们看见mContentParent为空的时候,会执行generateLayout()方法,同时需要传入一个mDecor,那么mDecor是什么东西呢,我们往上面看,mDecor是通过generateDecor()方法创建出来的,那我们自然得先到generateDecor()中一探究竟!
protected DecorView generateDecor() { return new DecorView(getContext(), -1); }
new了一个DecorView对象,DecorView就是我们界面中最顶层的View了,这个View的结构是这样的!
DecorView继承于FrameLayout,然后它有一个子view即LinearLayout,方向为竖直方向,其内有两个FrameLayout,上面的FrameLayout即为TitleBar之类的,下面的FrameLayout即为我们的ContentView,所谓的setContentView就是往这个FrameLayout里面添加我们的布局View的!现在我们可以画出第二层了!
好了,现在mDecor有了,终于可以进入到generateLayout(mDecor);看看了!
protected ViewGroup generateLayout(DecorView decor) {//...//省略一些设置Window样式的代码,直接来看我们最关心的代码! ViewGroup contentParent =(ViewGroup)findViewById(ID_ANDROID_CONTENT); //... return contentParent; } }
ID_ANDROID_CONTENT就是R.id.content,就是这个FrameLayout
我们看到contentParent就是这个FrameLaout!所以这下我们清楚了,mContentParent就是这个FrameLayout,就是我们的ContentView,现在回到PhoneWindow中的setContentView方法中!
public class PhoneWindow extends Window implements MenuBuilder.Callback { //... //... //... //老大 @Override public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor(); } else { mContentParent.removeAllViews(); } mLayoutInflater.inflate(layoutResID, mContentParent); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } } //老二 @Override public void setContentView(View view) { setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } //和老三 @Override public void setContentView(View view, ViewGroup.LayoutParams params) { if (mContentParent == null) { installDecor(); } else { mContentParent.removeAllViews(); } mContentParent.addView(view, params); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } } //... //... //...}
我们先来看老大,首先会先判断mContentParent是否为空,如果为空说明我们还没有DecorView,然后调用installDecor,之后我们的DecorView就准备好了,mContentParent也指向了我们的ContentView,由于是新建的,我们的mContentParent中肯定没有子View,如果不是新建的,我们要先把mContentParent中的子View全部清干净。接下来通过反射加载到我们传入的布局,接着下面会通过调用getCallBack得到一个CallBack对象cb,其实这个cb就是我们的Activity,接着会调用Activity的onContentChanged方法,这个方法是一个空实现,在后面的例子中我们会用到这个方法!
通过这一系列的过程,我们自己的View就被加载到那个FrameLayout中了,至此我们的布局就显示到屏幕上了!
老二和老三也非常的清晰,我们不是传入布局的id,而是传入一个View,mContentParent通过addView(view)来加载布局,那么这个和老大通过反射加载布局有什么区别吗? 答案肯定是有!我会通过一个例子来说明!
例子:
我们现在就来模拟一个需求,比如用户在MainActivity填写一个表单,这个表单有姓名和电话两个字段,当用户填完之后我们要进行提交,但是在提交之前我们希望有一个确认表单的页面来让用户确认一下信息是否填对,如果需要修改可以点击重填来修改,如果没问题就点击提交,然后跳到SecondActivity提示提交成功。
有问题版本
首先我们先来看一个有问题的版本,首先我们进入到填写表单的页面,填写完之后点击提交进入确认表单页面,然后点击重填,发现回来之后姓名栏和手机栏都是空的,然而我们确实在onContentChanged中为他们赋值了,不管了,再次填写,填完了点击提交,发现提交也点不了了,怎么点都没有反应!这是怎么回事呢!我们带着问题来看代码!
public class MainActivity extends Activity implements OnClickListener{ private static final int LAYOUT_FILL = 0; private static final int LAYOUT_CONFIRM = 1; private EditText et_name; private EditText et_phone; private Button bt_ok; private TextView tv_name; private TextView tv_phone; private Button bt_refilling; private Button bt_confirm; private String name; private String phone; private int currentLayout; private LayoutInflater mInflater; private View confirmView; private InputMethodManager imm; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //调用setContentView(int id) 加载填写表单布局 setContentView(R.layout.activity_main); initViews(); //注册监听器 registerListeners(); //初始化当前布局为填写表单布局 currentLayout = LAYOUT_FILL; } /** * 初始化布局 */ private void initViews() { imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); //初始化布局加载器 mInflater = LayoutInflater.from(this); //加载确认表单页面 confirmView =mInflater.inflate(R.layout.activity_confirm, null); //拿到填写表单页面中的控件 et_name = (EditText)findViewById(R.id.et_name); et_phone = (EditText)findViewById(R.id.et_phone); bt_ok = (Button)findViewById(R.id.bt_ok); //拿到确认表单页面中的控件 tv_name = (TextView)confirmView.findViewById(R.id.tv_confirm_name); tv_phone = (TextView)confirmView.findViewById(R.id.tv_confirm_phone); bt_refilling = (Button)confirmView.findViewById(R.id.bt_refilling); bt_confirm = (Button)confirmView.findViewById(R.id.bt_comfirm); } /** * 注册监听器 */ private void registerListeners() { bt_ok.setOnClickListener(this); bt_refilling.setOnClickListener(this); bt_confirm.setOnClickListener(this); } /** * 点击事件 */ @Override public void onClick(View v) { if (currentLayout == LAYOUT_FILL) {//如果当前页面是填写表单页面 switch (v.getId()) { case R.id.bt_ok://点击提交按钮 if (TextUtils.isEmpty(et_name.getText().toString())) { Toast.makeText(MainActivity.this, "姓名不能为空", Toast.LENGTH_SHORT).show(); return; } if (TextUtils.isEmpty(et_phone.getText().toString())) { Toast.makeText(MainActivity.this, "手机号不能为空",Toast.LENGTH_SHORT).show(); return; } //隐藏软键盘 imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); //将EditText内容保存到变量中 name = et_name.getText().toString(); phone = et_phone.getText().toString(); //将当前页面改为确认表单页面 currentLayout = LAYOUT_CONFIRM; //调用setContentView(View view),显示确认表单页面 setContentView(confirmView); break; } }else if (currentLayout == LAYOUT_CONFIRM) {//如果当前页面为确认表单页面 switch (v.getId()) { case R.id.bt_refilling://重填按钮 //调用setContentView(int id),显示填写表单页面 setContentView(R.layout.activity_main); //将当前页面改为填写表单页面 currentLayout = LAYOUT_FILL; break; case R.id.bt_comfirm://确认表单页面的最终提交按钮 //跳到SecondActivity Intent intent = new Intent(MainActivity.this,SecondActivity.class); startActivity(intent); break; } } } /** * 调用了setContentView后会回调此方法 */ @Override public void onContentChanged() { super.onContentChanged(); if (currentLayout == LAYOUT_CONFIRM) {//如果当前页面是确认表单页面 if (!TextUtils.isEmpty(name)) { //如果填写表单页面中的姓名不为空,我们将姓名一栏setText上 tv_name.setText(name); } if (!TextUtils.isEmpty(phone)) { //如果填写表单页面中的电话不为空,我们将电话一栏setText上 tv_phone.setText(phone); } }else if (currentLayout == LAYOUT_FILL) {//如果当前页面是填写表单页面 //如果是第一次启动这个页面,我们判断name和phone是空,所以就不做任何的操作 //如果是从确认表单页面点击重填按钮再次返回到填写表单页面时,我们就将刚刚填过 //的信息再次填上,省的用户再重新填一遍 if (!TextUtils.isEmpty(name)) { et_name.setText(name); } if (!TextUtils.isEmpty(phone)) { et_phone.setText(phone); } } }}
那么问题就出现在了setContentView上面,我们在点击了重填按钮后,我们的setContentView使用的是老大,即setContentView(int id),回想刚才我们分析的源码,老大是通过反射拿到我们的view,而每次反射拿到的view都不是同一个view,也就是说我们在onCreate中setContentView(R.layout.activity_main)和在点击了重填后setContentView(R.layout.activity_main)实际上是两个View,那么通过findviewById拿到的控件也是两套不同的控件了,所以我们点击了重填后,我们确实是给tv_name和tv_phone赋值了,但是我们显示的View不是原来那个View了,是新的View,那么新的View里面的tv_name和tv_phone是空的!所以显示为空!点击提交按钮也是一个道理!我们给原来的bt_ok设置了监听器,而新的View的bt_ok是没有设置过监听器的,所以点击是没有效果的!说了这么多!有很多重复的话,就是为了给说明白这件事!这个就是老大与老二老三的不同之处!!
修改后:
public class MainActivity extends Activity implements OnClickListener{ private static final int LAYOUT_FILL = 0; private static final int LAYOUT_CONFIRM = 1; private EditText et_name; private EditText et_phone; private Button bt_ok; private TextView tv_name; private TextView tv_phone; private Button bt_refilling; private Button bt_confirm; private String name; private String phone; private int currentLayout; private LayoutInflater mInflater; private View confirmView; private View fillView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //我们将setContentView放到initViews里面去调用 initViews(); //注册监听器 registerListeners(); //初始化当前布局为填写表单布局 currentLayout = LAYOUT_FILL; } /** * 初始化布局 */ private void initViews() { //初始化布局加载器 mInflater = LayoutInflater.from(this); //加载填写表单页面 fillView = mInflater.inflate(R.layout.activity_main, null); //加载确认表单页面 confirmView =mInflater.inflate(R.layout.activity_confirm, null); //调用setContentView(View view)方法,传入一个View setContentView(fillView); //拿到填写表单页面中的控件 et_name = (EditText)fillView.findViewById(R.id.et_name); et_phone = (EditText)fillView.findViewById(R.id.et_phone); bt_ok = (Button)fillView.findViewById(R.id.bt_ok); //拿到确认表单页面中的控件 tv_name = (TextView)confirmView.findViewById(R.id.tv_confirm_name); tv_phone = (TextView)confirmView.findViewById(R.id.tv_confirm_phone); bt_refilling = (Button)confirmView.findViewById(R.id.bt_refilling); bt_confirm = (Button)confirmView.findViewById(R.id.bt_comfirm); } /** * 注册监听器 */ private void registerListeners() { bt_ok.setOnClickListener(this); bt_refilling.setOnClickListener(this); bt_confirm.setOnClickListener(this); } /** * 点击事件 */ @Override public void onClick(View v) { if (currentLayout == LAYOUT_FILL) {//如果当前页面是填写表单页面 switch (v.getId()) { case R.id.bt_ok://点击提交按钮 if (TextUtils.isEmpty(et_name.getText().toString())) { Toast.makeText(MainActivity.this, "姓名不能为空", Toast.LENGTH_SHORT).show(); return; } if (TextUtils.isEmpty(et_phone.getText().toString())) { Toast.makeText(MainActivity.this, "手机号不能为空",Toast.LENGTH_SHORT).show(); return; } //将EditText内容保存到变量中 name = et_name.getText().toString(); phone = et_phone.getText().toString(); //将当前页面改为确认表单页面 currentLayout = LAYOUT_CONFIRM; //调用setContentView(View view),显示确认表单页面 setContentView(confirmView); break; } }else if (currentLayout == LAYOUT_CONFIRM) {//如果当前页面为确认表单页面 switch (v.getId()) { case R.id.bt_refilling://重填按钮 //调用setContentView(View view),显示填写表单页面 setContentView(fillView); //将当前页面改为填写表单页面 currentLayout = LAYOUT_FILL; break; case R.id.bt_comfirm://确认表单页面的最终提交按钮 //跳到SecondActivity Intent intent = new Intent(MainActivity.this,SecondActivity.class); startActivity(intent); break; } } } /** * 调用了setContentView后会回调此方法 */ @Override public void onContentChanged() { super.onContentChanged(); if (currentLayout == LAYOUT_CONFIRM) {//如果当前页面是确认表单页面 if (!TextUtils.isEmpty(name)) { //如果填写表单页面中的姓名不为空,我们将姓名一栏setText上 tv_name.setText(name); } if (!TextUtils.isEmpty(phone)) { //如果填写表单页面中的电话不为空,我们将电话一栏setText上 tv_phone.setText(phone); } }else if (currentLayout == LAYOUT_FILL) {//如果当前页面是填写表单页面 //如果是第一次启动这个页面,我们判断name和phone是空,所以就不做任何的操作 //如果是从确认表单页面点击重填按钮再次返回到填写表单页面时,我们就将刚刚填过 //的信息再次填上,省的用户再重新填一遍 if (!TextUtils.isEmpty(name)) { et_name.setText(name); } if (!TextUtils.isEmpty(phone)) { et_phone.setText(phone); } } }}
更多相关文章
- flutter中实现仿Android端的onResume和onPause方法
- 仿微信-界面动画(5)ViewPager详解
- jQuery Mobile 入门教程
- Activity的四种启动模式及其应用场景
- ReactNative的ViewPagerAndroid简述
- 【Android(安卓)Studio】制作启动画面Splash Screen
- Android的页面布局小结
- android qq 消息页面 editview 获得焦点把 标题栏 顶上去 问题
- android进入页面时焦点不在首页问题