LayoutInflater.setFactory學(xué)習(xí)及進(jìn)階

相信大家對LayoutInflater都不陌生,它經(jīng)常被用來根據(jù)xml生成View。比較熟悉的方法包括:

  • LayoutInflater.from(Context context)
  • inflate(@LayoutRes int resource, @Nullable ViewGroup root)
  • inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

構(gòu)造方法源碼如下,可見LayoutInflater.from(Context context)等同于context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)。

    /**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

除了上述方法,我今天想介紹的是相對不常用的兩個(gè)方法草戈。

  • setFactory(Factory factory)
  • setFactory2(Factory2 factory)

這兩個(gè)方法基本功能一致。系統(tǒng)通過Factory提供了一種hook的方法侍瑟,方便開發(fā)者攔截LayoutInflater創(chuàng)建View的過程唐片。應(yīng)用場景包括1)在XML布局中自定義標(biāo)簽名稱;2)全局替換系統(tǒng)控件為自定義View涨颜; 3)替換app中字體费韭;4)全局換膚等。

Factory與Factory2的區(qū)別

二者都是LayoutInflater類內(nèi)部定義的接口庭瑰。Factory2繼承自Factory接口星持,在API 11(HONEYCOMB)中引入的。Factory2比Factory多增加了一個(gè)onCreateView(View parent, String name, Context context, AttributeSet attrs)弹灭,該方法多了一個(gè)parent钉汗,用來存放構(gòu)建出的View羹令。

Android在v4包中提供了LayoutInflaterCompat來幫助完成兼容性的操作。

  • setFactory(
    @NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory)

    在API Level 26.1.0中被標(biāo)記為Deprecated损痰,官方推薦使用setFactory2方法
  • setFactory2(
    @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory)

Factory接口的定義如下,該接口只有一個(gè)onCreateView方法酒来。

    public interface 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.
         *
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         *
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

Factory2接口的源碼定義如下卢未。

public interface Factory2 extends Factory {
        /**
         * Version of {@link #onCreateView(String, Context, AttributeSet)}
         * that also supplies the parent that the view created view will be
         * placed in.
         *
         * @param parent The parent that the created view will be placed
         * in; <em>note that this may be null</em>.
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }

AppCompatActivity中系統(tǒng)Factory實(shí)現(xiàn)

Activity常用的基類包括Activity,F(xiàn)ragmentActivity和AppCompatActivity堰汉,關(guān)于它們?nèi)叩膮^(qū)別辽社,可以參考我的文章Activity、FragmentActivity和AppCompatActivity的區(qū)別翘鸭。

其中AppCompatActivity在v7包中引入滴铅,查看其源碼,其中onCreate方法設(shè)置了一個(gè)AppCompatDelegate就乓。

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        ...
        super.onCreate(savedInstanceState);
    }

AppCompatDelegate是一個(gè)抽象基類汉匙,其對象實(shí)例根據(jù)手機(jī)sdk版本來初始化,具體可參考源碼生蚁。其中installViewFactory方法的實(shí)現(xiàn)如下噩翠。

    @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");
            }
        }
    }

上述代碼可知,如果AppCompatActivity未在onCreate之前設(shè)置LayoutInflater的Factory邦投,則AppCompatActivity會嘗試設(shè)置一個(gè)Factory2伤锚,其中Factory2在AppCompatDelegate的具體子類代碼中實(shí)現(xiàn)。注意志衣,在API Level 26及以后,LayoutInflaterCompat.setFactory被標(biāo)記為Deprecated屯援,故我參考的v27的源碼中使用的是LayoutInflaterCompat.setFactory2。

根據(jù)Activity念脯、FragmentActivity和AppCompatActivity的區(qū)別狸捅,官方提供的AppCompatDelegate子類實(shí)現(xiàn)薄榛,如AppCompatDelegateImplN。幫助我們實(shí)現(xiàn)了AppCompat風(fēng)格組件的向下兼容,利用AppCompatDelegateImplN提供的Factory2將TextView等組件替換為AppCompatTextView勉失,這樣就可以使用一些新的屬性,如autoSizeMinTextSize腺劣。

Activity中setFactory的兼容性問題

上面也提到過沛贪,通過setFactory或setFactory2可以實(shí)現(xiàn)一些特殊功能,如全局自定義View替換废登,應(yīng)用換膚等淹魄。但是需要注意兼容性問題,保證AppCompat風(fēng)格組件的正確替換堡距。

注意甲锡,需要在調(diào)用super.onCreate(savedInstanceState)之前進(jìn)行LayoutInflaterCompat.setFactory2的設(shè)置兆蕉。否則setFactory并不能進(jìn)行重復(fù)設(shè)置,會導(dǎo)致后設(shè)置的Factory失效缤沦。

探究AppCompatDelegateImplN中Factory2接口的具體實(shí)現(xiàn)虎韵。

    /**
     * From {@link LayoutInflater.Factory2}.
     */
    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // First let the Activity's Factory try and inflate the view
        final View view = callActivityOnCreateView(parent, name, context, attrs);
        if (view != null) {
            return view;
        }

        // If the Factory didn't handle it, let our createView() method try
        return createView(parent, name, context, attrs);
    }

可知最終是調(diào)用AppCompatDelegate實(shí)例中的createView方法進(jìn)行AppCompat組件的繪制。故兼容寫法如下:

public class MainActivity extends AppCompatActivity
{
   private static final String TAG = "MainActivity";

   if (typeface == null)
   {
       typeface = Typeface.createFromAsset(getAssets(), "x x.ttf");
   }

   @Override
   protected void onCreate(Bundle savedInstanceState)
   {
       LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory()
       {
           @Override
           public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
           {
                //你可以在這里直接new自定義View

                //你可以在這里將系統(tǒng)類替換為自定義View

                //appcompat 創(chuàng)建view代碼
               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);
   }

Acitivity中setContentView的調(diào)用流程

以最常用的setContentView(@LayoutRes int layoutResID)方法為起點(diǎn)缸废,跟蹤view的繪制流程

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

關(guān)于Activity中的window實(shí)例創(chuàng)建包蓝,相信大家都有所了解。PhoneWindow是抽象基類window的具體實(shí)現(xiàn)企量,且該類內(nèi)部持有一個(gè)DecorView對象测萎,也即Activity界面的根View。

PhoneWindow的setContentView方法如下:

    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

閱讀代碼届巩,可看到關(guān)鍵的調(diào)用語句mLayoutInflater.inflate(layoutResID, mContentParent)硅瞧,將資源文件構(gòu)建成View樹,并添加到mContentParent視圖中恕汇。其中mLayoutInflater是在PhoneWindow的構(gòu)造函數(shù)中得到實(shí)例對象的LayoutInflater.from(context)腕唧。可以多次調(diào)用setContentView()來顯示界面拇勃,每次繪制之前會調(diào)用removeAllViews來移除原有頁面四苇。

PhoneWindow類的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源碼,原理同上方咆!

最終結(jié)合LayoutInflater的infalte方法月腋,參考Android LayoutInflater源碼解析,真正創(chuàng)建view的方法是LayoutInflater的createViewFromTag方法瓣赂。會依次調(diào)用mFactory2榆骚、mFactory和mPrivateFactory三者之一的onCreateView方法去創(chuàng)建一個(gè)View。如果不存在Factory煌集,則調(diào)用LayoutInflater自身的onCreateView或者createView來實(shí)例化View妓肢。

根據(jù)上面的流程可知,可通過setFactory或setFactory2來攔截view的創(chuàng)建過程苫纤,進(jìn)行一些特殊的操作碉钠。

Activity中onCreateView方法

Activity對象實(shí)現(xiàn)了LayoutInfalter.Factory2接口,提供了onCreateView方法的缺省實(shí)現(xiàn)卷拘。在Activity的attach方法中喊废,為Window的LayoutInflater設(shè)置了mPrivateFactory對象。也可以通過重新Activity的onCreateView方法進(jìn)行特定的操作栗弟。但是攔截時(shí)機(jī)晚于LayoutInfalter的setFactory和setFactory2方法污筷。

根據(jù)AppCompatActivity的學(xué)習(xí)也可知,在未對AppCompatActivity設(shè)置Factory或Factory2時(shí)乍赫,系統(tǒng)通過AppCompatDelegate自動(dòng)設(shè)置了Factory2實(shí)例瓣蛀。故一般的換膚方案都是通過setFactory或setFactory2實(shí)現(xiàn)對view創(chuàng)建過程的侵入陆蟆。


參考文章:
Android 探究 LayoutInflater setFactory
Android應(yīng)用setContentView與LayoutInflater加載解析機(jī)制源碼分析
https://github.com/hongyangAndroid/ChangeSkin
侵入性低擴(kuò)展性強(qiáng)的Android換膚框架XSkinLoader的用法及

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惋增,隨后出現(xiàn)的幾起案子叠殷,更是在濱河造成了極大的恐慌,老刑警劉巖器腋,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溪猿,死亡現(xiàn)場離奇詭異,居然都是意外死亡纫塌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門讲弄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來措左,“玉大人,你說我怎么就攤上這事避除≡跖” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵瓶摆,是天一觀的道長凉逛。 經(jīng)常有香客問我,道長群井,這世上最難降的妖魔是什么状飞? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮书斜,結(jié)果婚禮上诬辈,老公的妹妹穿的比我還像新娘。我一直安慰自己荐吉,他們只是感情好焙糟,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著样屠,像睡著了一般穿撮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上痪欲,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天悦穿,我揣著相機(jī)與錄音,去河邊找鬼勤揩。 笑死咧党,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的陨亡。 我是一名探鬼主播傍衡,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼深员,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蛙埂?” 一聲冷哼從身側(cè)響起倦畅,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绣的,沒想到半個(gè)月后叠赐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屡江,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年芭概,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惩嘉。...
    茶點(diǎn)故事閱讀 40,444評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡罢洲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出文黎,到底是詐尸還是另有隱情惹苗,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布耸峭,位于F島的核電站桩蓉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏劳闹。R本人自食惡果不足惜院究,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望玷或。 院中可真熱鬧儡首,春花似錦、人聲如沸偏友。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽位他。三九已至氛濒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鹅髓,已是汗流浹背舞竿。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窿冯,地道東北人骗奖。 一個(gè)月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親执桌。 傳聞我的和親對象是個(gè)殘疾皇子鄙皇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評論 2 359

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