Android技能樹 — LayoutInflater Factory小結

青蛙要fly發表於2019-02-27

前言

很久沒寫文章了,所以打算水一篇文章,畢竟這方面知識的文章有很多很多。

前段時間流行起來了突然不願意寫Shape,Selector檔案的文章,然後各種方案,編寫自定義View等。那時候大家應該都看到了一篇:
無需自定義View,徹底解放shape,selector吧。我發現這個想法挺好的,所以今天就一步步來講解下跟這個方案有關的相關基礎知識點,看完後大家基本就會懂了,然後可以自己編寫。

所以我們本文主要學習:

1. LayoutInflater相關知識(⭐️科普為主)

2. setFactory相關知識(⭐️⭐️⭐️本文主要知識點)

3. 實際專案中的用處(⭐️⭐️科普為主️)

Android技能樹 — LayoutInflater Factory小結

估計很多人都會使用AS的Tools — Layout Inspector功能來檢視自己寫的介面結構及控制元件的相應元素。

比如我們寫了很簡單的例子:

public class TestActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
    }
}

複製程式碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="textview"
        />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        />

</LinearLayout>
複製程式碼

然後用AS檢視:

Android技能樹 — LayoutInflater Factory小結

大家有沒有看到有沒有什麼特別的地方:

Android技能樹 — LayoutInflater Factory小結

我們在佈局中寫的是Button,TextView,ImageView,但是在AS的Layout Inspector功能檢視下,變成了AppCompatButton,AppCompatTextView,AppComaptImageView,那到底是我們的按鈕真的已經在編譯的時候自動變成了AppCompatXXX系列,還是隻是單純的在這個工具裡面看的時候我們的控制元件只是顯示給我們看到的名字是AppCompatXXX系列而已。

我們把我們的Activity的父類做下修改,改為:

public class TestActivity extends AppCompatActivity{
    ......
}
變為
public class TestActivity extends Activity{
    ......
}

複製程式碼

我們再來檢視下Layout Inspector介面:

Android技能樹 — LayoutInflater Factory小結

我們可以看到,控制元件就自動變成了我們佈局裡面寫的控制元件名稱了, 那就說明,我們繼承的AppCompatActivity對我們xml裡面寫的控制元件做了替換。

而AppCompatActivity的替換主要是通過LayoutInflater setFactory

Android技能樹 — LayoutInflater Factory小結

正文

1.LayoutInflater相關知識

其實大部分人使用LayoutInflater的話,更多的是使用了inflate方法,用來對Layout檔案變成View:

View view = LayoutInflater.from(this).inflate(R.layout.activity_test,null);
複製程式碼

甚至於我們平常在Activity裡面經常寫的setContentView(R.layout.xxx);方法的內部也是通過inflate方法實現的。

有沒有想過為什麼呼叫了這個方法後,我們就可以拿到了相關的View物件了呢?

其實很簡單,就是我們傳入的是一個xml檔案,裡面通過xml格式寫了我們的佈局,而這個方法會幫我們去解析XML的格式,然後幫我們例項化具體的View物件即可,我們具體一步步來看原始碼:

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


public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
                + Integer.toHexString(resource) + ")");
    }
    
    //"可以看到主要分為2步"
    //"第一步:通過res.getLayout方法拿到XmlResourceParser物件"
    final XmlResourceParser parser = res.getLayout(resource);
    
    try {
    
        //"第二步:通過inflate方法最終把XmlResourceParser轉為View例項物件"
        return inflate(parser, root, attachToRoot);
        
    } finally {
        parser.close();
    }
}
複製程式碼

本來我想大片的原始碼拷貝上來,然後一步步寫上內容,但是後來發現一個講解資源獲取過程的不錯的系列文章,所以我就直接借鑑大佬的,直接貼上鍊接了:

(關於本文的內容相關的,可以著重看下第一篇和第三篇,inflate的原始碼在第三篇)

Android資源管理框架(Asset Manager)(一)簡介

Android資源管理框架(二)AssetManager建立過程

Android資源管理框架(三)應用程式資源的查詢過程


Android技能樹 — LayoutInflater Factory小結

2. Factory相關知識

2.1 原始碼中預設設定的Factory2相關程式碼

我們在前言中的例子中可以看到我們的Activity繼承了AppCompatActivity,我們來檢視AppCompatActivityonCreate方法:

protected void onCreate(@Nullable Bundle savedInstanceState) {
    //"1.獲取代理類物件"
    AppCompatDelegate delegate = this.getDelegate();
    
    //"2.呼叫代理類的installViewFactory方法"
    delegate.installViewFactory();
    
    ......
    ......
    super.onCreate(savedInstanceState);
}
複製程式碼

我們可以看到和ActivityonCreate方法最大的不同就是AppCompatActivityonCreate種的操作都放在了代理類AppCompatDelegate中的onCreate方法中處理了,而AppCompatDelegate是抽象類,具體的實現類是AppCompatDelegateImpl

//"1.獲取代理類具體方法原始碼:"

@NonNull
public AppCompatDelegate getDelegate() {
    if (this.mDelegate == null) {
        this.mDelegate = AppCompatDelegate.create(this, this);
    }

    return this.mDelegate;
}
    
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}    
複製程式碼

我們再來看代理類的installViewFactory方法具體實現:

public void installViewFactory() {

    //`獲取了LayoutInflater物件`
    LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
    
    
    if (layoutInflater.getFactory() == null) {
        //`如果layoutInflater的factory2為null,對LayoutInflater物件設定factory`
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
        Log.i("AppCompatDelegate", "The Activity`s LayoutInflater already has a Factory installed so we can not install AppCompat`s");
    }
}
複製程式碼

AppCompatDelegateImpl自己實現了Fatory2介面,所以就直接setFactory2(xx,this)即可,我們來看下Factory2到底是啥:

public interface Factory2 extends Factory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
複製程式碼

可能很多人在以前看過相關文章,都是Factory介面及方法是setFactory,對於Factory2是一臉懵逼,我們可以看到上面的Factory2程式碼,Factory2其實就是繼承了Factory介面,其實setFactory方法已經被棄用了,而且你呼叫setFactory方法,內部其實還是呼叫了setFactory2方法,setFactory2是在SDK>=11以後引入的:

Android技能樹 — LayoutInflater Factory小結

所以我們就直接可以簡單理解為Factory2類和setFactory2方法是用來替代Factory類和setFactory方法

所以也就執行了AppCompatDelegateImpl裡面的onCreateView方法:

//`呼叫方法1`
public View onCreateView(String name, Context context, AttributeSet attrs) {
    return this.onCreateView((View)null, name, context, attrs);
}

//`呼叫方法2`
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    this.createView(parent, name, context, attrs);
}

//`呼叫方法3`
public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    
    //先例項化mAppCpatViewInflater物件程式碼
    ......
    ......

    //`直接看這裡,最後呼叫了mAppCompatViewInflater.createView方法返回相應的View`
    return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}

複製程式碼

所以通過上面我們可以看到,最終設定的Factory2之後呼叫的onCreateView方法,其實就是呼叫AppCompatDelegateImpl的createView方法(最終呼叫了AppCompatViewInflater類中的createView方法)

所以我們這邊要記住其實就是呼叫AppCompatDelegateImpl的createView方法
所以我們這邊要記住其實就是呼叫AppCompatDelegateImpl的createView方法
所以我們這邊要記住其實就是呼叫AppCompatDelegateImpl的createView方法
重要的事情說三遍,因為後面會用到這塊

我們繼續來分析原始碼,我們跟蹤到AppCompatViewInflater類中的createView方法(這裡以Button為例,其他的程式碼暫時去除):

final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    
    ......
    ......

    View view = null;
    byte var12 = -1;
    switch(name.hashCode()) {
        
    ......
    ......
        
    case 2001146706:
        if (name.equals("Button")) {
            var12 = 2;
        }
    }

    switch(var12) {
        
    ......
    ......
        
    case 2:
        view = this.createButton(context, attrs);
        this.verifyNotNull((View)view, name);
        break;
        
    ......
    ......

    return (View)view;
}
複製程式碼

我們來看createButton方法:

@NonNull
protected AppCompatButton createButton(Context context, AttributeSet attrs) {
    return new AppCompatButton(context, attrs);
}
複製程式碼

所以我們看到了,最終我們的Button替換成了AppCompatButton

2.2 自己實現自定義Factory2

我們現在來具體看下Factory2onCreateView方法,我們自己來實現一個自定義的Factory2類,而不是用系統自己設定的:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {

    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        //`這個方法是Factory介面裡面的,因為Factory2是繼承Factory的`
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
        
        //`這個方法是Factory2裡面定義的方法`
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            Log.e(TAG, "parent:" + parent + ",name = " + name);
            int n = attrs.getAttributeCount();
            for (int i = 0; i < n; i++) {
                Log.e(TAG, attrs.getAttributeName(i) + " , " + attrs.getAttributeValue(i));
            }
            return null;
        }
        
    });

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test1);
}

複製程式碼

我們可以看到Factory2onCreateView方法裡面的屬性parent指的是父View物件,name是當前這個View的xml裡面的名字,attrs 包含了View的屬性名字及屬性值。

列印後我們可以看到列印出來了我們的demo中的Layout佈局中寫的三個控制元件了。

......
......
......

E: parent:android.widget.LinearLayout{4e37f38 V.E...},name = Button
E: layout_width , -2
E: layout_height , -2
E: text , button
E: parent:android.widget.LinearLayout{4e37f38 V.E...},name = TextView
E: layout_width , -2
E: layout_height , -2
E: text , textview
E: parent:android.widget.LinearLayout{4e37f38 V.E...},name = ImageView
E: layout_width , -2
E: layout_height , -2
E: src , @2131361792
複製程式碼

正好的確是我們layout中設定的控制元件的值。我們知道了在這個onCreateView方法中,我們可以拿到當前View的內容,我們學著系統替換AppCompatXXX控制元件的方式更換我們demo中的控制元件,加上這段程式碼:

LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        
        //`我們在這裡對傳遞過來的View做了替換`
        //`把TextView和ImageView 都換成了Button`
        if(name.equals("TextView") || name.equals("ImageView")){
            Button button = new Button(context, attrs);
            return button;
        }

        return null;
    }

});
複製程式碼

我們可以看下效果:

Android技能樹 — LayoutInflater Factory小結

我們知道了在onCreateView中,可以看到遍歷的所有View的名字及屬性引數,也可以在這裡把return的值更改做替換。

但是我們知道系統替換了的AppCompatXXX控制元件做了很多相容,如果我們像上面一樣把TextView和ImageView直接換成了Button,那麼系統也因為我們設定過了Factory2,就不會再去設定了,也就不會幫我們自動變成AppCompatButton,而是變成了三個Button。

Android技能樹 — LayoutInflater Factory小結

所以我們不能單純盲目的直接使用我們的Factory2,所以我們還是用的系統最終構建View的方法,只不過在它構建前,更改引數而已,這樣最終還是會跑系統的程式碼。

我們前面程式碼提過最終設定的Factory2之後呼叫的onCreateView方法,其實就是呼叫AppCompatDelegateImplcreateView方法(就是前面講的,重要的事情說三遍那個地方,忘記的可以回頭再看下)

所以我們可以修改相應的控制元件的引數,最後再把修改過的內容重新還給AppCompatDelegateImplcreateView方法去生成View即可,這樣系統原本幫我們做的相容性也都還在。

所以我們這裡要修改程式碼為:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {

    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        //`這個方法是Factory介面裡面的,因為Factory2是繼承Factory的`
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
        
        //`這個方法是Factory2裡面定義的方法`
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            if(name.equals("TextView") || name.equals("ImageView")){
                name = "Button";
            }

            //`我們只是更換了引數,但最終例項化View的邏輯還是交給了AppCompatDelegateImpl`
            AppCompatDelegate delegate = getDelegate();
            View view = delegate.createView(parent, name, context, attrs);
            return view;
        }
    });

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test1);
}

複製程式碼

我們最終可以看到:

Android技能樹 — LayoutInflater Factory小結

按鈕也的確都變成了AppCompatButton。

總結:設定Factory2更像是在系統填充View之前,先跑了一下onCreateView方法,然後我們可以在這個方法裡面,在View被填充前,對它進行修改。


Android技能樹 — LayoutInflater Factory小結

3. 實際專案中的用處

其實以前在一些文章中也看到過,說什麼突然你想全域性要替換ButtonTextView,這樣更方便什麼的,但是單純這種直接整個控制元件替換我個人更喜歡去xml檔案裡面改,因為一般一個app是團隊一起開發,然後你這麼處理,後期別人維護時候,看了xml,反而很詫異,後期維護我個人感覺不方便。

所以我這個列舉了幾個常用的功能:

3.1. 全域性替換字型等屬性

因為字型等是TextView的一個屬性,為了加一個屬性,我們就沒必要去全部的佈局中進行更改,只需要上我們的onCreateView中,發現是TextView,就去設定我們對應的字型。

public static Typeface typeface;
@Override
protected void onCreate(Bundle savedInstanceState)
{
    if (typeface == null){
        typeface = Typeface.createFromAsset(getAssets(), "xxxx.ttf");
    }
    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }

        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            AppCompatDelegate delegate = getDelegate();
            View view = delegate.createView(parent, name, context, attrs);

            if ( view!= null && (view instanceof TextView)){
                ((TextView) view).setTypeface(typeface);
            }
            return view;
        }

    });
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}
複製程式碼

3.2 動態換膚功能

這塊動態換膚功能,網上的文章也很多,但是基本的原理都一樣,也是用了我們本文的知識,和上面的更換字型類似,我們可以對做了標記的View進行識別,然後在onCreateView遍歷到它的時候,更改它的一些屬性,比如背景色等,然後再交給系統去生成View。

具體可以參考下:Android動態換膚原理解析及實踐

3.3 無需編寫shape、selector,直接在xml設定值

估計前端時間大家在掘金都看到過這篇文章:

無需自定義View,徹底解放shape,selector吧

裡面講到我們如果要設定控制元件的角度等屬性值,不需要再去寫特定的shape或者selector檔案,直接在xml中寫入:

Android技能樹 — LayoutInflater Factory小結

初步一看是不是感覺很神奇?what amazing !!

其實核心也是使用了我們今天講到的知識點,自定義Factory類,只需要在onCreateView方法裡面,判斷attrs的引數名字,比如發現名字是我們制定的stroke_color屬性,就去通過程式碼手動幫他去設定這個值,我們來檢視下它的部分程式碼,我們直接看onCreateView方法即可:

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    
    ......
    ......

    if (typedArray.getBoolean(R.styleable.background_ripple_enable, false) &&
        typedArray.hasValue(R.styleable.background_ripple_color)) {
        int color = typedArray.getColor(R.styleable.background_ripple_color, 0);
            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                Drawable contentDrawable = (stateListDrawable == null ? drawable : stateListDrawable);
                RippleDrawable rippleDrawable = new RippleDrawable(ColorStateList.valueOf(color), contentDrawable, contentDrawable);
                view.setClickable(true);
                view.setBackground(rippleDrawable);
            } else {
                StateListDrawable tmpDrawable = new StateListDrawable();
                GradientDrawable unPressDrawable = DrawableFactory.getDrawable(typedArray);
                unPressDrawable.setColor(color);
                tmpDrawable.addState(new int[]{-android.R.attr.state_pressed}, drawable);
                tmpDrawable.addState(new int[]{android.R.attr.state_pressed}, unPressDrawable);
                view.setClickable(true);
                view.setBackground(tmpDrawable);
            }
        }
        return view;
        
        ......
        ......
    }
複製程式碼

是不是這麼看,大家基本就懂了原理,這樣你再去看它的庫,或者要加上什麼自己特定的屬性,都有能力自己去進行修改了。

3.4 XXXXX

當然還有很多奇思妙想的用處,只要大家想象力夠多,就可以在這中間做各種騷操作。


Android技能樹 — LayoutInflater Factory小結

結語:

不小心把文章就水完了…….有錯誤歡迎大家指出。

相關文章