LayoutInflater的factory源碼分析

核心知識

  1. 你可以在壓榨布局的時候通過LayoutInflater.Factory針對view的創(chuàng)建進行hook操作

    (比如實現(xiàn)動態(tài)換膚)

  2. LayoutInflater.setFactory 不能在 super.onCreate 之后使用抵乓。

    (因為在onCreate時系統(tǒng)會設(shè)置一個factory痕支,如果重復(fù)設(shè)置factory系統(tǒng)將會拋出異常,不過我們可以反射修改LayoutInflater的mFactorySet屬性來避免拋出異常)

  1. AppCompatActivity 為什么 setFactory 净赴?向下兼容新版本中的效果阎肝。

    ( AppCompatActivity 設(shè)置 Factory 是為了將一些 widget 自動變成 兼容widget 瞻讽,例如將 TextView 變成 AppCompatTextView巍举,以便于向下兼容新版本中的效果溶其,在高版本中的一些 widget 新特性就是這樣在老版本中也能展示的。)

  1. LayoutInflater.Factory2 繼承自 LayoutInflater.Factory

createViewFromTag()中的factory

在View創(chuàng)建時有一個createViewFromTag()方法策橘,在這個方法開頭有這么一段源碼

View view;
if (mFactory2 != null) {
    // ① 有mFactory2炸渡,則調(diào)用mFactory2的onCreateView方法
    view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
    // ② 有mFactory,則調(diào)用mFactory的onCreateView方法
    view = mFactory.onCreateView(name, context, attrs);
} else {
    view = null;
} 
//后面的源碼意思是如果沒有factory的實例丽已,就用系統(tǒng)的方式創(chuàng)建view蚌堵。

這段代碼的意思是,如果factory2不為空沛婴,則用factory2的實例創(chuàng)建view吼畏,如果mFactory不為空,則用mFactory的實例創(chuàng)建view嘁灯。 也就是說泻蚊,這兩個方法是用來讓我們覆蓋view創(chuàng)建的入口

LayoutInflater.Factory

LayoutInflater.Factory 中沒有說明丑婿,我們看下它唯一方法的說明:

Hook you can supply that is called when inflating from a LayoutInflater. You can use this to customize the tag names available in your XML layout files.

你可以在壓榨布局的時候通過LayoutInflater.Factory進行hook操作 性雄,你可以使用LayoutInflater.Factory 去自定義xml布局文件中的tag(標簽)名稱

我們來看下這個唯一的方法:

public abstract View onCreateView (String name, Context context, AttributeSet attrs)

那么我們就明白了,如果我們設(shè)置了LayoutInflater Factory 羹奉,在LayoutInflater 的 createViewFromTag 方法中就會通過這個 Factory 的 onCreateView 方法來創(chuàng)建 View秒旋。

Factory 作用

那我們可以進行什么hook操作呢? 舉個簡單的例子:比如你在 XML中 寫了一個 TextView標簽诀拭,然后在 onCreateView 這個回調(diào)里 判斷如果 name 是 TextView 的話可以變成一個Button迁筛,這樣的功能可以實現(xiàn)例如批量更換某一個控件等的用途。例子如下:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.liuzhaofutrue.teststart.MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

接下來我們在 Java 代碼中做修改:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                if(TextUtils.equals(name,"TextView")){
                    Button button = new Button(MainActivity.this);
                    button.setText("我替換了TextView");
                    button.setAllCaps(false);
                    return button;
                }
                return getDelegate().createView(parent, name, context, attrs);
            }

            @Override
            public View onCreateView(String name, Context context, AttributeSet attrs) {
                return null;
            }
        });

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

可以看到炫加,本來在布局文件中需要展示的是一個 TextView瑰煎,但是現(xiàn)在卻被改造成了一個 Button。

img

LayoutInflaterCompat

LayoutInflater.Factory2 是API 11 被加進來的俗孝,那么 LayoutInflaterCompat 就是拿來做兼容的類酒甸。我們來看下它最重要的兩個方法:

@Deprecated
public static void setFactory(
     @NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
        IMPL.setFactory(inflater, factory);
}

public static void setFactory2(
     @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
        IMPL.setFactory2(inflater, factory);
}

可以看到 setFactory 已經(jīng)被標記為過時,更建議使用 setFactory2 方法赋铝。

  static final LayoutInflaterCompatBaseImpl IMPL;
    static {
        if (Build.VERSION.SDK_INT >= 21) {
            IMPL = new LayoutInflaterCompatApi21Impl();
        } else {
            IMPL = new LayoutInflaterCompatBaseImpl();
        }
    }
    
    @RequiresApi(21)
    static class LayoutInflaterCompatApi21Impl extends LayoutInflaterCompatBaseImpl {
        @SuppressWarnings("deprecation")
        @Override
        public void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
            inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
        }

        @Override
        public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
            inflater.setFactory2(factory);
        }
    }

這里調(diào)用 setFactory 實際上還是調(diào)用的 setFactory2 方法插勤。

LayoutInflater.setFactory 使用注意

如果我們將LayoutInflater.setFactory 挪到 super.onCreate 的后面可以嗎? 程序竟然報錯了,我們看下Log:

 Process: com.example.teststart, PID: 24132
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.teststart/com.example.teststart.MainActivity}: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2876)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2941)
     Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
        at android.view.LayoutInflater.setFactory2(LayoutInflater.java:317)
        at com.example.teststart.MainActivity.onCreate(MainActivity.java:18)
        at android.app.Activity.performCreate(Activity.java:6765)

說明是 LayoutInflater 已經(jīng)被設(shè)置了一個 Factory农尖,而我們再設(shè)置的時候就會報錯析恋。我們跟蹤下 LayoutInflater.from(this).setFactory2 方法

private boolean mFactorySet;
    public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

可以通過這個 mFactorySet 變量看出 setFactory2 方法只能被調(diào)用一次,重復(fù)設(shè)置則會拋出異常盛卡。那Factory2是被誰設(shè)置了呢助隧? 我們來看下 AppCompatActivity 的 onCreate 方法

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        if (delegate.applyDayNight() && mThemeId != 0) {
            // If DayNight has been applied, we need to re-apply the theme for
            // the changes to take effect. On API 23+, we should bypass
            // setTheme(), which will no-op if the theme ID is identical to the
            // current theme ID.
            if (Build.VERSION.SDK_INT >= 23) {
                onApplyThemeResource(getTheme(), mThemeId, false);
            } else {
                setTheme(mThemeId);
            }
        }
        super.onCreate(savedInstanceState);
    }

其中會調(diào)用 delegate.installViewFactory(); 最終會調(diào)用到AppCompatDelegateImplV9 的 installViewFactory方法;

@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

可以看到:

如果 layoutInflater.getFactory() 為空滑沧,則 AppCompatActivity 會自動設(shè)置一個 Factory2并村,難怪我們在 super.onCreate 之后調(diào)用會報錯

所以我們明白了滓技,為什么我們在 super.onCreate 之前設(shè)置 Factory之后哩牍,系統(tǒng)再次設(shè)置 Factory 的時候不會拋出異常

AppCompatActivity 為什么 setFactory

那么為什么 AppCompatActivity 會自動設(shè)置一個 Factory呢?順著 AppCompatDelegateImplV9 的 installViewFactory方法繼續(xù)跟蹤令漂,走到了 onCreateView 方法膝昆,它最終會調(diào)用到 AppCompatViewInflater 的 createView 方法

 public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        View view = null;
        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            ......
        }
        return view;
    }

原來 AppCompatActivity 設(shè)置 Factory 是為了將一些 widget 自動變成 兼容widget (例如將 TextView 變成 AppCompatTextView)以便于向下兼容新版本中的效果叠必,在高版本中的一些 widget 新特性就是這樣在老版本中也能展示的荚孵。

那如果我們設(shè)置了自己的 Factory 豈不是就避開了系統(tǒng)的兼容?其實系統(tǒng)的兼容我們?nèi)匀豢梢员4嫦聛砟铀簦驗橄到y(tǒng)是通過 AppCompatDelegate.onCreateView 方法來實現(xiàn) widget 兼容的处窥,那我們就可以在設(shè)置 Factory 的時候先調(diào)用 AppCompatDelegate.onCreateView 方法嘱吗,再來做我們的處理玄组。

 LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            // 調(diào)用 AppCompatDelegate 的createView方法
            getDelegate().createView(parent, name, context, attrs);
            // 再來執(zhí)行我們的定制化操作
            return null;
        }
    
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    });
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谒麦,隨后出現(xiàn)的幾起案子俄讹,更是在濱河造成了極大的恐慌,老刑警劉巖绕德,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件患膛,死亡現(xiàn)場離奇詭異,居然都是意外死亡耻蛇,警方通過查閱死者的電腦和手機踪蹬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來臣咖,“玉大人跃捣,你說我怎么就攤上這事《嵘撸” “怎么了疚漆?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我娶聘,道長闻镶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任丸升,我火速辦了婚禮铆农,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狡耻。我一直安慰自己顿涣,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布酝豪。 她就那樣靜靜地躺著涛碑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪孵淘。 梳的紋絲不亂的頭發(fā)上蒲障,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音瘫证,去河邊找鬼揉阎。 笑死,一個胖子當(dāng)著我的面吹牛背捌,可吹牛的內(nèi)容都是我干的毙籽。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼毡庆,長吁一口氣:“原來是場噩夢啊……” “哼坑赡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起么抗,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤毅否,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蝇刀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體螟加,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年吞琐,在試婚紗的時候發(fā)現(xiàn)自己被綠了捆探。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡站粟,死狀恐怖黍图,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情卒蘸,我是刑警寧澤雌隅,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布翻默,位于F島的核電站,受9級特大地震影響恰起,放射性物質(zhì)發(fā)生泄漏修械。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一检盼、第九天 我趴在偏房一處隱蔽的房頂上張望肯污。 院中可真熱鬧,春花似錦吨枉、人聲如沸蹦渣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柬唯。三九已至,卻和暖如春圃庭,著一層夾襖步出監(jiān)牢的瞬間锄奢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工剧腻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拘央,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓书在,卻偏偏與公主長得像灰伟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子儒旬,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容