Android技能树 — LayoutInflater Factory小结
前言
今天早上地鐵上在洋神的公眾號(hào)上看到了一篇干貨,就給轉(zhuǎn)過(guò)來(lái)了。
前段時(shí)間流行起來(lái)了突然不愿意寫Shape,Selector文件的文章,然后各種方案,編寫自定義View等。那時(shí)候大家應(yīng)該都看到了一篇:
無(wú)需自定義View,徹底解放shape,selector吧。我發(fā)現(xiàn)這個(gè)想法挺好的,所以今天就一步步來(lái)講解下跟這個(gè)方案有關(guān)的相關(guān)基礎(chǔ)知識(shí)點(diǎn),看完后大家基本就會(huì)懂了,然后可以自己編寫。
所以我們本文主要學(xué)習(xí):
1. LayoutInflater相關(guān)知識(shí)(??科普為主)
2. setFactory相關(guān)知識(shí)(??????本文主要知識(shí)點(diǎn))
3. 實(shí)際項(xiàng)目中的用處(????科普為主?)
估計(jì)很多人都會(huì)使用AS的Tools — Layout Inspector功能來(lái)查看自己寫的界面結(jié)構(gòu)及控件的相應(yīng)元素。
比如我們寫了很簡(jiǎn)單的例子:
public class TestActivity extends AppCompatActivity {@Overrideprotected 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"><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="button"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="textview"/><ImageViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/ic_launcher"/></LinearLayout>然后用AS查看:
大家有沒(méi)有看到有沒(méi)有什么特別的地方:
我們?cè)诓季种袑懙氖荁utton,TextView,ImageView,但是在AS的Layout Inspector功能查看下,變成了AppCompatButton,AppCompatTextView,AppComaptImageView,那到底是我們的按鈕真的已經(jīng)在編譯的時(shí)候自動(dòng)變成了AppCompatXXX系列,還是只是單純的在這個(gè)工具里面看的時(shí)候我們的控件只是顯示給我們看到的名字是AppCompatXXX系列而已。
我們把我們的Activity的父類做下修改,改為:
public class TestActivity extends AppCompatActivity{...... } 變?yōu)?public class TestActivity extends Activity{...... }我們?cè)賮?lái)查看下Layout Inspector界面:
我們可以看到,控件就自動(dòng)變成了我們布局里面寫的控件名稱了, 那就說(shuō)明,我們繼承的AppCompatActivity對(duì)我們xml里面寫的控件做了替換。
而AppCompatActivity的替換主要是通過(guò)LayoutInflater setFactory
正文
1.LayoutInflater相關(guān)知識(shí)
其實(shí)大部分人使用LayoutInflater的話,更多的是使用了inflate方法,用來(lái)對(duì)Layout文件變成View:
View view = LayoutInflater.from(this).inflate(R.layout.activity_test,null);甚至于我們平常在Activity里面經(jīng)常寫的setContentView(R.layout.xxx);方法的內(nèi)部也是通過(guò)inflate方法實(shí)現(xiàn)的。
有沒(méi)有想過(guò)為什么調(diào)用了這個(gè)方法后,我們就可以拿到了相關(guān)的View對(duì)象了呢?
其實(shí)很簡(jiǎn)單,就是我們傳入的是一個(gè)xml文件,里面通過(guò)xml格式寫了我們的布局,而這個(gè)方法會(huì)幫我們?nèi)ソ馕鯴ML的格式,然后幫我們實(shí)例化具體的View對(duì)象即可,我們具體一步步來(lái)看源碼:
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步"//"第一步:通過(guò)res.getLayout方法拿到XmlResourceParser對(duì)象"final XmlResourceParser parser = res.getLayout(resource);try {//"第二步:通過(guò)inflate方法最終把XmlResourceParser轉(zhuǎn)為View實(shí)例對(duì)象"return inflate(parser, root, attachToRoot);} finally {parser.close();} }本來(lái)我想大片的源碼拷貝上來(lái),然后一步步寫上內(nèi)容,但是后來(lái)發(fā)現(xiàn)一個(gè)講解資源獲取過(guò)程的不錯(cuò)的系列文章,所以我就直接借鑒大佬的,直接貼上鏈接了:
(關(guān)于本文的內(nèi)容相關(guān)的,可以著重看下第一篇和第三篇,inflate的源碼在第三篇)
Android資源管理框架(Asset Manager)(一)簡(jiǎn)介
Android資源管理框架(二)AssetManager創(chuàng)建過(guò)程
Android資源管理框架(三)應(yīng)用程序資源的查找過(guò)程
2. Factory相關(guān)知識(shí)
2.1 源碼中默認(rèn)設(shè)置的Factory2相關(guān)代碼
我們?cè)谇把灾械睦又锌梢钥吹轿覀兊腁ctivity繼承了AppCompatActivity,我們來(lái)查看AppCompatActivity的onCreate方法:
protected void onCreate(@Nullable Bundle savedInstanceState) {//"1.獲取代理類對(duì)象"AppCompatDelegate delegate = this.getDelegate();//"2.調(diào)用代理類的installViewFactory方法"delegate.installViewFactory();............super.onCreate(savedInstanceState); }我們可以看到和Activity的onCreate方法最大的不同就是AppCompatActivity把onCreate種的操作都放在了代理類AppCompatDelegate中的onCreate方法中處理了,而AppCompatDelegate是抽象類,具體的實(shí)現(xiàn)類是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); }我們?cè)賮?lái)看代理類的installViewFactory方法具體實(shí)現(xiàn):
public void installViewFactory() {//'獲取了LayoutInflater對(duì)象'LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);if (layoutInflater.getFactory() == null) {//'如果layoutInflater的factory2為null,對(duì)LayoutInflater對(duì)象設(shè)置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自己實(shí)現(xiàn)了Fatory2接口,所以就直接setFactory2(xx,this)即可,我們來(lái)看下Factory2到底是啥:
public interface Factory2 extends Factory {public View onCreateView(View parent, String name, Context context, AttributeSet attrs); }可能很多人在以前看過(guò)相關(guān)文章,都是Factory接口及方法是setFactory,對(duì)于Factory2是一臉懵逼,我們可以看到上面的Factory2代碼,Factory2其實(shí)就是繼承了Factory接口,其實(shí)setFactory方法已經(jīng)被棄用了,而且你調(diào)用setFactory方法,內(nèi)部其實(shí)還是調(diào)用了setFactory2方法,setFactory2是在SDK>=11以后引入的:
所以我們就直接可以簡(jiǎn)單理解為Factory2類和setFactory2方法是用來(lái)替代Factory類和setFactory方法
所以也就執(zhí)行了AppCompatDelegateImpl里面的onCreateView方法:
//'調(diào)用方法1' public View onCreateView(String name, Context context, AttributeSet attrs) {return this.onCreateView((View)null, name, context, attrs); }//'調(diào)用方法2' public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {this.createView(parent, name, context, attrs); }//'調(diào)用方法3' public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {//先實(shí)例化mAppCpatViewInflater對(duì)象代碼............//'直接看這里,最后調(diào)用了mAppCompatViewInflater.createView方法返回相應(yīng)的View'return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed()); }所以通過(guò)上面我們可以看到,最終設(shè)置的Factory2之后調(diào)用的onCreateView方法,其實(shí)就是調(diào)用AppCompatDelegateImpl的createView方法(最終調(diào)用了AppCompatViewInflater類中的createView方法)
所以我們這邊要記住其實(shí)就是調(diào)用AppCompatDelegateImpl的createView方法</br>
所以我們這邊要記住其實(shí)就是調(diào)用AppCompatDelegateImpl的createView方法</br>
所以我們這邊要記住其實(shí)就是調(diào)用AppCompatDelegateImpl的createView方法</br>
重要的事情說(shuō)三遍,因?yàn)楹竺鏁?huì)用到這塊
我們繼續(xù)來(lái)分析源碼,我們跟蹤到AppCompatViewInflater類中的createView方法(這里以Button為例,其他的代碼暫時(shí)去除):
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; }我們來(lái)看createButton方法:
@NonNull protected AppCompatButton createButton(Context context, AttributeSet attrs) {return new AppCompatButton(context, attrs); }所以我們看到了,最終我們的Button替換成了AppCompatButton。
2.2 自己實(shí)現(xiàn)自定義Factory2
我們現(xiàn)在來(lái)具體看下Factory2的onCreateView方法,我們自己來(lái)實(shí)現(xiàn)一個(gè)自定義的Factory2類,而不是用系統(tǒng)自己設(shè)置的:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {//'這個(gè)方法是Factory接口里面的,因?yàn)镕actory2是繼承Factory的'@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}//'這個(gè)方法是Factory2里面定義的方法'@Overridepublic 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); }我們可以看到Factory2的onCreateView方法里面的屬性parent指的是父View對(duì)象,name是當(dāng)前這個(gè)View的xml里面的名字,attrs 包含了View的屬性名字及屬性值。
打印后我們可以看到打印出來(lái)了我們的demo中的Layout布局中寫的三個(gè)控件了。
...... ...... ......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中設(shè)置的控件的值。我們知道了在這個(gè)onCreateView方法中,我們可以拿到當(dāng)前View的內(nèi)容,我們學(xué)著系統(tǒng)替換AppCompatXXX控件的方式更換我們demo中的控件,加上這段代碼:
LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {//'我們?cè)谶@里對(duì)傳遞過(guò)來(lái)的View做了替換'//'把TextView和ImageView 都換成了Button'if(name.equals("TextView") || name.equals("ImageView")){Button button = new Button(context, attrs);return button;}return null;}});我們可以看下效果:
我們知道了在onCreateView中,可以看到遍歷的所有View的名字及屬性參數(shù),也可以在這里把return的值更改做替換。
但是我們知道系統(tǒng)替換了的AppCompatXXX控件做了很多兼容,如果我們像上面一樣把TextView和ImageView直接換成了Button,那么系統(tǒng)也因?yàn)槲覀冊(cè)O(shè)置過(guò)了Factory2,就不會(huì)再去設(shè)置了,也就不會(huì)幫我們自動(dòng)變成AppCompatButton,而是變成了三個(gè)Button。
所以我們不能單純盲目的直接使用我們的Factory2,所以我們還是用的系統(tǒng)最終構(gòu)建View的方法,只不過(guò)在它構(gòu)建前,更改參數(shù)而已,這樣最終還是會(huì)跑系統(tǒng)的代碼。
我們前面代碼提過(guò)<font color = "red">最終設(shè)置的Factory2之后調(diào)用的onCreateView方法,其實(shí)就是調(diào)用AppCompatDelegateImpl的createView方法</font>(就是前面講的,重要的事情說(shuō)三遍那個(gè)地方,忘記的可以回頭再看下)
所以我們可以修改相應(yīng)的控件的參數(shù),最后再把修改過(guò)的內(nèi)容重新還給AppCompatDelegateImpl的createView方法去生成View即可,這樣系統(tǒng)原本幫我們做的兼容性也都還在。
所以我們這里要修改代碼為:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {//'這個(gè)方法是Factory接口里面的,因?yàn)镕actory2是繼承Factory的'@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}//'這個(gè)方法是Factory2里面定義的方法'@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {if(name.equals("TextView") || name.equals("ImageView")){name = "Button";}//'我們只是更換了參數(shù),但最終實(shí)例化View的邏輯還是交給了AppCompatDelegateImpl'AppCompatDelegate delegate = getDelegate();View view = delegate.createView(parent, name, context, attrs);return view;}});super.onCreate(savedInstanceState);setContentView(R.layout.activity_test1); }我們最終可以看到:
按鈕也的確都變成了AppCompatButton。
總結(jié):設(shè)置Factory2更像是在系統(tǒng)填充View之前,先跑了一下onCreateView方法,然后我們可以在這個(gè)方法里面,在View被填充前,對(duì)它進(jìn)行修改。
3. 實(shí)際項(xiàng)目中的用處
其實(shí)以前在一些文章中也看到過(guò),說(shuō)什么突然你想全局要替換Button到TextView,這樣更方便什么的,但是單純這種直接整個(gè)控件替換我個(gè)人更喜歡去xml文件里面改,因?yàn)橐话阋粋€(gè)app是團(tuán)隊(duì)一起開發(fā),然后你這么處理,后期別人維護(hù)時(shí)候,看了xml,反而很詫異,后期維護(hù)我個(gè)人感覺(jué)不方便。
所以我這個(gè)列舉了幾個(gè)常用的功能:
3.1. 全局替換字體等屬性
因?yàn)樽煮w等是TextView的一個(gè)屬性,為了加一個(gè)屬性,我們就沒(méi)必要去全部的布局中進(jìn)行更改,只需要上我們的onCreateView中,發(fā)現(xiàn)是TextView,就去設(shè)置我們對(duì)應(yīng)的字體。
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() {@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}@Overridepublic 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 動(dòng)態(tài)換膚功能
這塊動(dòng)態(tài)換膚功能,網(wǎng)上的文章也很多,但是基本的原理都一樣,也是用了我們本文的知識(shí),和上面的更換字體類似,我們可以對(duì)做了標(biāo)記的View進(jìn)行識(shí)別,然后在onCreateView遍歷到它的時(shí)候,更改它的一些屬性,比如背景色等,然后再交給系統(tǒng)去生成View。
具體可以參考下:Android動(dòng)態(tài)換膚原理解析及實(shí)踐
3.3 無(wú)需編寫shape、selector,直接在xml設(shè)置值
估計(jì)前端時(shí)間大家在掘金都看到過(guò)這篇文章:
無(wú)需自定義View,徹底解放shape,selector吧
里面講到我們?nèi)绻O(shè)置控件的角度等屬性值,不需要再去寫特定的shape或者selector文件,直接在xml中寫入:
初步一看是不是感覺(jué)很神奇?what amazing !!
其實(shí)核心也是使用了我們今天講到的知識(shí)點(diǎn),自定義Factory類,只需要在onCreateView方法里面,判斷attrs的參數(shù)名字,比如發(fā)現(xiàn)名字是我們制定的stroke_color屬性,就去通過(guò)代碼手動(dòng)幫他去設(shè)置這個(gè)值,我們來(lái)查看下它的部分代碼,我們直接看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;............}是不是這么看,大家基本就懂了原理,這樣你再去看它的庫(kù),或者要加上什么自己特定的屬性,都有能力自己去進(jìn)行修改了。
3.4 XXXXX
當(dāng)然還有很多奇思妙想的用處,只要大家想象力夠多,就可以在這中間做各種騷操作。
原文鏈接:https://www.jianshu.com/p/8d8ada21ab82
總結(jié)
以上是生活随笔為你收集整理的Android技能树 — LayoutInflater Factory小结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 某些情况下安卓引入so冲突的解决
- 下一篇: 如何线程安全的使用HashMap