好久沒寫文章了,因此打算水一篇文章,畢竟這方面知識的文章有不少不少。前端
前段時間流行起來了忽然不肯意寫Shape,Selector文件的文章,而後各類方案,編寫自定義View等。那時候你們應該都看到了一篇: 無需自定義View,完全解放shape,selector吧。我發現這個想法挺好的,因此今天就一步步來說解下跟這個方案有關的相關基礎知識點,看完後你們基本就會懂了,而後能夠本身編寫。android
因此咱們本文主要學習:bash
1. LayoutInflater相關知識(⭐️科普爲主)app
2. setFactory相關知識(⭐️⭐️⭐️本文主要知識點)框架
3. 實際項目中的用處(⭐️⭐️科普爲主️)ide
估計不少人都會使用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查看:post
你們有沒有看到有沒有什麼特別的地方:學習
咱們在佈局中寫的是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
其實大部分人使用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建立過程
咱們在前言中的例子中能夠看到咱們的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方法
因此咱們這邊要記住其實就是調用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
。
咱們如今來具體看下Factory2
的onCreateView
方法,咱們本身來實現一個自定義的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);
}
複製代碼
咱們能夠看到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() {
@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;
}
});
複製代碼
咱們能夠看下效果:
咱們知道了在onCreateView中,能夠看到遍歷的全部View的名字及屬性參數,也能夠在這裏把return的值更改作替換。
可是咱們知道系統替換了的AppCompatXXX控件作了不少兼容,若是咱們像上面同樣把TextView和ImageView直接換成了Button,那麼系統也由於咱們設置過了Factory2,就不會再去設置了,也就不會幫咱們自動變成AppCompatButton,而是變成了三個Button。
因此咱們不能單純盲目的直接使用咱們的Factory2
,因此咱們仍是用的系統最終構建View的方法,只不過在它構建前,更改參數而已,這樣最終仍是會跑系統的代碼。
咱們前面代碼提過最終設置的Factory2
以後調用的onCreateView
方法,其實就是調用AppCompatDelegateImpl
的createView
方法(就是前面講的,重要的事情說三遍那個地方,忘記的能夠回頭再看下)
因此咱們能夠修改相應的控件的參數,最後再把修改過的內容從新還給AppCompatDelegateImpl
的createView
方法去生成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);
}
複製代碼
咱們最終能夠看到:
按鈕也的確都變成了AppCompatButton。
總結:設置Factory2更像是在系統填充View以前,先跑了一下onCreateView方法,而後咱們能夠在這個方法裏面,在View被填充前,對它進行修改。
其實之前在一些文章中也看到過,說什麼忽然你想全局要替換Button
到TextView
,這樣更方便什麼的,可是單純這種直接整個控件替換我我的更喜歡去xml文件裏面改,由於通常一個app是團隊一塊兒開發,而後你這麼處理,後期別人維護時候,看了xml,反而很詫異,後期維護我我的感受不方便。
因此我這個列舉了幾個經常使用的功能:
由於字體等是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);
}
複製代碼
這塊動態換膚功能,網上的文章也不少,可是基本的原理都同樣,也是用了咱們本文的知識,和上面的更換字體相似,咱們能夠對作了標記的View進行識別,而後在onCreateView遍歷到它的時候,更改它的一些屬性,好比背景色等,而後再交給系統去生成View。
具體能夠參考下:Android動態換膚原理解析及實踐
估計前端時間你們在掘金都看到過這篇文章:
裏面講到咱們若是要設置控件的角度等屬性值,不須要再去寫特定的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;
......
......
}
複製代碼
是否是這麼看,你們基本就懂了原理,這樣你再去看它的庫,或者要加上什麼本身特定的屬性,都有能力本身去進行修改了。
固然還有不少奇思妙想的用處,只要你們想象力夠多,就能夠在這中間作各類騷操做。
不當心把文章就水完了.......有錯誤歡迎你們指出。