Android技能树 — LayoutInflater Factory小结
前言
今天早上地鐵上在洋神的公眾號上看到了一篇干貨,就給轉過來了。
前段時間流行起來了突然不愿意寫Shape,Selector文件的文章,然后各種方案,編寫自定義View等。那時候大家應該都看到了一篇:
無需自定義View,徹底解放shape,selector吧。我發現這個想法挺好的,所以今天就一步步來講解下跟這個方案有關的相關基礎知識點,看完后大家基本就會懂了,然后可以自己編寫。
所以我們本文主要學習:
1. LayoutInflater相關知識(??科普為主)
2. setFactory相關知識(??????本文主要知識點)
3. 實際項目中的用處(????科普為主?)
估計很多人都會使用AS的Tools — Layout Inspector功能來查看自己寫的界面結構及控件的相應元素。
比如我們寫了很簡單的例子:
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查看:
大家有沒有看到有沒有什么特別的地方:
我們在布局中寫的是Button,TextView,ImageView,但是在AS的Layout Inspector功能查看下,變成了AppCompatButton,AppCompatTextView,AppComaptImageView,那到底是我們的按鈕真的已經在編譯的時候自動變成了AppCompatXXX系列,還是只是單純的在這個工具里面看的時候我們的控件只是顯示給我們看到的名字是AppCompatXXX系列而已。
我們把我們的Activity的父類做下修改,改為:
public class TestActivity extends AppCompatActivity{...... } 變為 public class TestActivity extends Activity{...... }我們再來查看下Layout Inspector界面:
我們可以看到,控件就自動變成了我們布局里面寫的控件名稱了, 那就說明,我們繼承的AppCompatActivity對我們xml里面寫的控件做了替換。
而AppCompatActivity的替換主要是通過LayoutInflater setFactory
正文
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資源管理框架(三)應用程序資源的查找過程
2. Factory相關知識
2.1 源碼中默認設置的Factory2相關代碼
我們在前言中的例子中可以看到我們的Activity繼承了AppCompatActivity,我們來查看AppCompatActivity的onCreate方法:
protected void onCreate(@Nullable Bundle savedInstanceState) {//"1.獲取代理類對象"AppCompatDelegate delegate = this.getDelegate();//"2.調用代理類的installViewFactory方法"delegate.installViewFactory();............super.onCreate(savedInstanceState); }我們可以看到和Activity的onCreate方法最大的不同就是AppCompatActivity把onCreate種的操作都放在了代理類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以后引入的:
所以我們就直接可以簡單理解為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方法</br>
所以我們這邊要記住其實就是調用AppCompatDelegateImpl的createView方法</br>
所以我們這邊要記住其實就是調用AppCompatDelegateImpl的createView方法</br>
重要的事情說三遍,因為后面會用到這塊
我們繼續來分析源碼,我們跟蹤到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
我們現在來具體看下Factory2的onCreateView方法,我們自己來實現一個自定義的Factory2類,而不是用系統自己設置的:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {//'這個方法是Factory接口里面的,因為Factory2是繼承Factory的'@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}//'這個方法是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對象,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() {@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}@Overridepublic 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;}});我們可以看下效果:
我們知道了在onCreateView中,可以看到遍歷的所有View的名字及屬性參數,也可以在這里把return的值更改做替換。
但是我們知道系統替換了的AppCompatXXX控件做了很多兼容,如果我們像上面一樣把TextView和ImageView直接換成了Button,那么系統也因為我們設置過了Factory2,就不會再去設置了,也就不會幫我們自動變成AppCompatButton,而是變成了三個Button。
所以我們不能單純盲目的直接使用我們的Factory2,所以我們還是用的系統最終構建View的方法,只不過在它構建前,更改參數而已,這樣最終還是會跑系統的代碼。
我們前面代碼提過<font color = "red">最終設置的Factory2之后調用的onCreateView方法,其實就是調用AppCompatDelegateImpl的createView方法</font>(就是前面講的,重要的事情說三遍那個地方,忘記的可以回頭再看下)
所以我們可以修改相應的控件的參數,最后再把修改過的內容重新還給AppCompatDelegateImpl的createView方法去生成View即可,這樣系統原本幫我們做的兼容性也都還在。
所以我們這里要修改代碼為:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {//'這個方法是Factory接口里面的,因為Factory2是繼承Factory的'@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}//'這個方法是Factory2里面定義的方法'@Overridepublic 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); }我們最終可以看到:
按鈕也的確都變成了AppCompatButton。
總結:設置Factory2更像是在系統填充View之前,先跑了一下onCreateView方法,然后我們可以在這個方法里面,在View被填充前,對它進行修改。
3. 實際項目中的用處
其實以前在一些文章中也看到過,說什么突然你想全局要替換Button到TextView,這樣更方便什么的,但是單純這種直接整個控件替換我個人更喜歡去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() {@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 動態換膚功能
這塊動態換膚功能,網上的文章也很多,但是基本的原理都一樣,也是用了我們本文的知識,和上面的更換字體類似,我們可以對做了標記的View進行識別,然后在onCreateView遍歷到它的時候,更改它的一些屬性,比如背景色等,然后再交給系統去生成View。
具體可以參考下:Android動態換膚原理解析及實踐
3.3 無需編寫shape、selector,直接在xml設置值
估計前端時間大家在掘金都看到過這篇文章:
無需自定義View,徹底解放shape,selector吧
里面講到我們如果要設置控件的角度等屬性值,不需要再去寫特定的shape或者selector文件,直接在xml中寫入:
初步一看是不是感覺很神奇?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
當然還有很多奇思妙想的用處,只要大家想象力夠多,就可以在這中間做各種騷操作。
原文鏈接:https://www.jianshu.com/p/8d8ada21ab82
總結
以上是生活随笔為你收集整理的Android技能树 — LayoutInflater Factory小结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 某些情况下安卓引入so冲突的解决
- 下一篇: 如何线程安全的使用HashMap