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);
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章