View.inflate() 的前世今生

誤用 LayoutInflater 的 inflate() 方法已經不是什麼稀罕事兒了……

做 Android 開發做久了,一定會或多或少地對佈局的渲染有一些懵逼:

  1. View.inflate()LayoutInflator.from().inflate() 有啥區別?
  2. 調用 inflate() 方法的時候有時候傳 null,有時候傳 parent 是爲啥?
  3. 用 LayoutInflater 有時候還可能傳個 attachToRoot ,這又是個啥?

接下來我們就從源碼的角度來尋找一下這幾個問題的答案,後面再用幾個示例來驗證我們的猜想。

話不多說,Let's go !

基本介紹

先來看一下這個方法具體做了什麼:

/**
 * 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.
 */
public static View inflate(Context context, int resource, ViewGroup root) {
    LayoutInflater factory = LayoutInflater.from(context);
    return factory.inflate(resource, root);
}

當我們查看源碼,就會發現,這個方法的內部實際上就是調用了 LayoutInflater 的 inflate 方法。正如此方法的註釋所言,這是一個方便開發者調用的 LayoutInflater 的包裝方法,而 LayoutInflater 本身則爲 View 的渲染提供了更多的選擇。

那麼我們現在的問題就變成了, LayoutInflater 又做了什麼?

繼續追蹤代碼,我們會發現, LayoutInflator.from().inflate() 是這個樣子的:

// LayoutInflator#inflate(int, ViewGroup)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
}

啥?重載?

// LayoutInflator#inflate(int, ViewGroup, boolean)
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

這裏我們看到,通過層層調用,最終會調用到 LayoutInflator#inflate(int, ViewGroup, boolean) 方法,很明顯,這個方法會將我們傳入的佈局 id 轉換爲 XmlResourceParser,然後進行另一次,也是最後一次重載。

這個方法就厲害了,這裏基本上包括了我們所有問題的答案,我們繼續往下看。

源碼分析

話不多說,上代碼。接下來我們來逐段分析下這個 inflate 方法:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    final Context inflaterContext = mContext;
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    
    // 默認返回結果爲傳入的根佈局
    View result = root;
  
    // 通過 createViewFromTag() 方法找到傳入的 layoutId 的根佈局,並賦值給 temp
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    ViewGroup.LayoutParams params = null;
  
    // 如果傳入的父佈局不爲空
    if (root != null) {
        // 爲這個 root 生成一套合適的 LayoutParams
        params = root.generateLayoutParams(attrs);
        if (!attachToRoot) {
            // 如果沒有 attachToRoot,那爲根佈局設置 layoutparams
            temp.setLayoutParams(params);
        }
    }

    // 如果傳入的父佈局不爲空,且想要 attachToRoot
    if (root != null && attachToRoot) {
        // 那就將傳入的佈局以及 layoutparams 通過 addView 方法添加到父佈局中 
        root.addView(temp, params);
    }

    // 如果傳入的根佈局爲空,或者不想 attachToRoot,則返回要加載的 layoutId
    if (root == null || !attachToRoot) {
        result = temp;
    }
    return result;
}

代碼也分析完了,我再來總結一下:

  • View#inflate 只是個簡易的包裝方法,實際上還是調用的 LayoutInflater#inflate ;

  • LayoutInflater#inflate 由於可以自己選擇 root 和 attachToRoot 的搭配(後面有解釋),使用起來更加靈活;

  • 實際上的區別只是在於 root 是否傳空,以及 attachToRoot 真假與否;

  • root 傳空時,會直接返回要加載的 layoutId,返回的 View 沒有父佈局且沒有 LayoutParams;

  • root 不傳空時,又分爲 attachToRoot 爲真或者爲假:

    • attachToRoot = true

    會爲傳入的 layoutId 直接設置參數,並將其添加到 root 中,然後將傳入的 root 返回;

    • attachToRoot = false

    會爲傳入的 layoutId 設置參數,但是不會添加到 root ,然後返回 layoutId 對應的 View;

    這裏需要注意的是,雖然不馬上將 View 添加到 parent 中,但是這裏最好也傳上 parent,而不是粗暴的傳入 null;因爲子 View 的 LayoutParams 需要由 parent 來確定;否則會在手動 addView 時調用 generateDefaultLayoutParams() 爲子 View 生成一個寬高都爲包裹內容的 LayoutParams,而這並不一定是我們想要的。

測試 & 檢驗

單說起來可能有些抽象,下面使用代碼來進行具體的測試與檢驗。

View.inflate(context, layoutId, null)

如之前所說,這實際上調用的是 getLayoutInflater().inflate(layoutId, null) ,結合之前的源碼來看:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    View result = root;
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    if (root == null || !attachToRoot) {
        result = temp;
    }
    return result;
}

很明顯,傳入的 root 爲空,則會直接將加載好的 xml 佈局返回,而這種情況下返回的這個 View 沒有參數,也沒有父佈局。

protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_test);
    View inflateView = View.inflate(this, R.layout.layout_basic_use_item, null);
    Log.e("Test", "LayoutParams -> " + inflateView.getLayoutParams());
    Log.e("Test", "Parent -> " + inflateView.getParent());
}

image

如圖所示,正如我們想的,root 傳 null 時,參數以及父佈局返回結果均爲 null。

View.inflate(context, layoutId, mParent)

按之前分析過的,此方法實際調用的是 getLayoutInflater().inflate(layoutId, root, true) ,再來看源碼:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    final Context inflaterContext = mContext;
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    View result = root; 
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    ViewGroup.LayoutParams params = null;
    if (root != null) {
        params = root.generateLayoutParams(attrs);
    }
    if (root != null && attachToRoot) {
        root.addView(temp, params);
    }
    return result;
}

如源碼所示,返回的 result 會在最開始就被賦值爲入參的 root,root 不爲空,同時 attachToRoot 爲 true,就會將加載好的佈局直接通過 addView 方法添加到 root 佈局中,然後將 root 返回。

protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.layout_test);

    LinearLayout mParent = findViewById(R.id.ll_root);
    View inflateView = View.inflate(this, R.layout.layout_basic_use_item, mParent);

    Log.e("Test", "LayoutParams -> " + inflateView.getLayoutParams());
    Log.e("Test", "Parent -> " + inflateView.getParent());
    Log.e("Test", "inflateView -> " + inflateView);
}

image

如圖示,返回的 View 正是我們傳入的 mParent,對應的 id 是 ll_root,參數也不再爲空。

getLayoutInflater().inflate(layoutId, root, false)

也許會有人問了,現在要麼是 root 傳空,返回 layoutId 對應的佈局;要麼是 root 不傳空,返回傳入的 root 佈局。那我要是想 root 不傳空,但是還是返回 layoutId 對應的佈局呢?

這就是 View#inflate 的侷限了,由於它是包裝方法,因此 attachToRoot 並不能因需定製。這時候我們完全可以自己調用 getLayoutInflater().inflate(layoutId, root, false) 方法,手動的將第三個參數傳爲 false,同時爲這個方法傳入目標根佈局。這樣,我們就可以得到一個有 LayoutParams,但是沒有 parentViewlayoutId 佈局了。

protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_test);
    LinearLayout mParent = findViewById(R.id.ll_root);
    View inflateView = getLayoutInflater().inflate(R.layout.main, mParent, false);
    Log.e("Test", "LayoutParams -> " + inflateView.getLayoutParams());
    Log.e("Test", "Parent -> " + inflateView.getParent());
}

image

與我們分析的一致,有參數,但是沒有父佈局,且返回的就是我們加載的佈局 id。我們在之後可以通過 addView 方法手動將這個佈局加入父佈局中。

這裏還有個要注意的點,那就是 params = root.generateLayoutParams(attrs); 這句代碼,我們會發現,爲 layoutId 設置的 params 參數,實際上是通過 root 來生成的。這也就告訴我們,雖然不馬上添加到 parent 中,但是這裏最好也傳上 parent,而不是粗暴的傳入 null,因爲子 View 的 LayoutParams 需要由 parent 來確定;當然,傳入 null 也不會有問題,因爲在執行 addView() 方法的時候,如果當前 childView 沒有參數,會調用 generateDefaultLayoutParams() 生成一個寬高都包裹的 LayoutParams 賦值給 childView,而這並不一定是我們想要的。

attachToRoot 必須爲 false!

代碼寫多了,大家有時候會發現這個 attachToRoot 也不是想怎樣就怎樣的,有時候它還就必須是 false,不能爲 true。下面我們就來看看這些情況。

  • RecylerView#onCreateViewHolder()

    在爲 RecyclerView 創建 ViewHolder 時,由於 View 複用的問題,是 RecyclerView 來決定什麼時候展示它的子View,這個完全不由我們決定,這種情況下,attachToRoot 必須爲 false:

    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
          LayoutInflater inflater = LayoutInflater.from(getActivity());  
          View view = inflater.inflate(R.layout.item, parent, false);  
          return new ViewHolder(view);  
    }
    
  • Fragment#onCreateView()

    由於 Fragment 需要依賴於 Activity 展示,一般在 Activity 中也會有容器佈局來盛放 Fragment:

    Fragment fragment = new Fragment();
    getSupportFragmentManager()
            .beginTransaction()
            .add(R.id.root_container, fragment)
            .commit(); 
    

    上述代碼中的 R.id.root_container 便爲容器,這個 View 會作爲參數傳遞給 Fragment#onCreateView() :

    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_layout, parentViewGroup, false); 
    }
    

    它也是你在 inflate() 方法中傳入的 ViewGroup,FragmentManager 會將 Fragment 的 View 添加到 ViewGroup 中,言外之意就是,Fragment 對應的佈局展示或者說添加進 ViewGroup 時也不是我們來控制的,而是 FragmentManager 來控制的。

總結一下就是,當我們不爲子 View 的展示負責時,attachToRoot 必須爲 false;否則就會出現對應的負責人,比如上面說的 Rv 或者 FragmentManager,已經把佈局 id 添加到 ViewGroup 了,我們還繼續設置 attachToRoot 爲 true,想要手動 addView,那必然會發生 child already has parent 的錯誤。



作者:Joseph_L
鏈接:https://www.jianshu.com/p/342890fcf5c9
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章