前言
最近給組內同學做了一次“動態換膚和換文案”的主題分享,其中的核心就是LayoutInflater類,所以把LayoutInflater原始碼梳理了一遍。巧了,這周掘金新榜和部分公眾號都發布了LayoutInflater或者換膚主題之類的文章。那隻好站在各位大佬的肩膀上,也來湊個熱鬧,分析一下LayoutInflater類。(前方長文預警,會有很多原始碼分析,原始碼基於Android 9.0)
LayoutInflater簡介
我們在載入佈局的時候都會主動或者被動的用到 LayoutInflater ,比如 Activity 的setContentView
方法和Fragment的onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
回撥等。LayoutInflater 的作用就是把佈局檔案xml例項化為相應的View元件。我們可以通過三種方法獲取 LayoutInflater:
Activity.getLayoutInflater()
;LayoutInflater.from(context)
;context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
;
每個方法都和 Context 相關聯,其中方法1和方法2最終都會通過方法3來實現。
獲取到 LayoutInflater 後,通過呼叫inflate
方法來例項化佈局。而inflate
方法由很多過載,我們常用的是inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
,所有 inflate 方法最終會呼叫到 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
。下面就從這個方法入手,開始分析 LayoutInflater 的原始碼。
原始碼分析
inflate方法
先看一下inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
的三個引數:
- XmlPullParser parser:很顯然是一個 XML 解析器,這個解析器就是 LayoutInflater 所要載入的 XML 佈局轉化來的,通過 PULL 方式解析。
- ViewGroup root:裝載要載入的 XML 佈局的根容器,比如,在 Activity 的
setContentView
方法中就是 id 為android.R.id.content
的 FrameLayout 根佈局了。 - boolean attachToRoot:是否將所解析的佈局新增到根容器中,同時也影響了所解析佈局的寬高。
被廣泛討論的是root
和attachToRoot
的不同傳參對被載入的佈局檔案的影響,下面看程式碼。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
// 將parser轉成AttributeSet介面,用來讀取xml中設定的View屬性
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root; // 此方法返回的View,預設是root
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
...
final String name = parser.getName(); // 獲取當前的標籤名
...
if (TAG_MERGE.equals(name)) { // 處理<merge>標籤
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 遞迴處理
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 建立View物件
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
...
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs); // 獲取根View的寬高
if (!attachToRoot) { // 如果attachToRoot為false,則給根View設定寬高
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true); // 遞迴處理
...
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
// 如果root不空,且attachToRoot為true,則將根View新增到容器中
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
// 如果root空或者attachToRoot為false,則將返回結果設定為根View
result = temp;
}
}
} catch (XmlPullParserException e) {
...
} catch (Exception e) {
...
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
// 要麼是root,要麼是建立的根View
return result;
}
}
複製程式碼
從程式碼中可以看出root
和attachToRoot
不同傳參的影響:
- 如果root不為null,attachToRoot設為true,則會將載入的佈局新增到一個父佈局中,即root,並且返回root;
- 如果root不為null,attachToRoot設為false,則會對佈局檔案最外層的所有layout屬性進行設定,並且返回該佈局的根View,當該view被新增到父view當中時,這些layout屬性會自動生效;
- 如果root為null,attachToRoot將失去作用,設定任何值都沒有意義,返回的也是要載入的佈局的根View;
rInflate方法
從上面的方法中可以看到處理<merge>
標籤時會呼叫rInflate
,處理子View時會呼叫rInflateChildren
方法。其實rInflateChildren
中呼叫的是rInflate
,而rInflate
也呼叫了rInflateChildren
,從而形成了遞迴呼叫,也就是遞迴處理子View。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
// 處理<requestFocus>標籤
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
// 處理<tag>標籤
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
// 處理<include>標籤
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) { // <merge>標籤異常
throw new InflateException("<merge /> must be the root element");
} else { // 建立View物件
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true); // 遞迴處理孩子節點
viewGroup.addView(view, params); // 將View新增到父佈局中
}
}
if (pendingRequestFocus) { // 父佈局處理焦點
parent.restoreDefaultFocus();
}
if (finishInflate) { // 結束載入
parent.onFinishInflate();
}
}
複製程式碼
該方法中會處理<requestFocus>
、<tag>
、<include>
、<merge>
和普通View標籤。其中:
<requestFocus>
是重新定位焦點的,呼叫的consumeChildElements
方法其實沒幹什麼事,只是簡單的把該標籤消費結束掉。<tag>
標籤一般很少用,它主要用來標記View,給View設定一個標籤值,例如:
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" >
<tag android:id="@+id/tag"
android:value="hello" />
</TextView>
findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 試一下tag標籤
Toast.makeText(MainActivity.this, (String) v.getTag(R.id.tag), Toast.LENGTH_SHORT).show();
}
});
複製程式碼
在ListView的自定義Adapter中,應該都有用到過View的setTag方法,即:使用ViewHolder來重複利用View。
parseViewTag
方法:
private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)
throws XmlPullParserException, IOException {
final Context context = view.getContext();
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
// 讀取tag的id
final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
// 讀取tag的值
final CharSequence value = ta.getText(R.styleable.ViewTag_value);
// 給View設定該tag
view.setTag(key, value);
ta.recycle();
// 結束該標籤(子View無效)
consumeChildElements(parser);
}
複製程式碼
<include>
標籤不能是根標籤,parseInclude
方法單獨分析。<merge>
標籤只能是根標籤,這裡會拋異常。
parseInclude方法
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
if (parent instanceof ViewGroup) { // 必須在ViewGroup裡才有效
// 處理theme屬性
...
// If the layout is pointing to a theme attribute, we have to
// massage the value to get a resource identifier out of it.
// 拿到layout指定的佈局
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
...
if (layout == 0) { // 必須是合法的id
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
throw new InflateException("You must specify a valid layout "
+ "reference. The layout ID " + value + " is not valid.");
} else { // 類似於inflate的處理
// 拿到layout的解析器
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
while ((type = childParser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty.
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(childParser.getPositionDescription() +
": No start tag found!");
}
// layout的根標籤
final String childName = childParser.getName();
if (TAG_MERGE.equals(childName)) { // 處理<merge>
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else { // 處理View
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
// 獲取<include>裡設定的id
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
// 獲取<include>裡設定的visibility
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
ViewGroup.LayoutParams params = null;
try { // 獲取<include>裡設定的寬高
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
// 獲取layout裡設定的寬高
params = group.generateLayoutParams(childAttrs);
}
// <include>裡設定的寬高優先於layout裡設定的
view.setLayoutParams(params);
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
if (id != View.NO_ID) {
// include裡設定的id優先順序高
view.setId(id);
}
// include裡設定的visibility優先順序高
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
LayoutInflater.consumeChildElements(parser);
}
複製程式碼
- include裡必須設定layout屬性,且layout的id必須合法;
- include裡設定的id優先順序高於layout裡設定的id,即:兩者同時設定時,後者會失效;
- include裡設定的width和height屬性優先順序高於layout裡設定的寬高;
- include裡設定的visibility屬性優先順序高於layout設定的visibility。
createViewFromTag方法
正常View標籤都是通過createViewFromTag
來建立對應的View物件的。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
// 真正的View標籤名存在class屬性中
name = attrs.getAttributeValue(null, "class");
}
...
try {
View view;
if (mFactory2 != null) { // 先使用Factory2
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) { // 再使用Factory
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
// 通過標籤名中是否包含'.'來區分是否為自定義View
if (-1 == name.indexOf('.')) {
// 處理系統View
view = onCreateView(parent, name, attrs);
} else { // 自定義View用的是全限定類名
// 處理自定義View
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
...
} catch (ClassNotFoundException e) {
...
} catch (Exception e) {
...
}
}
複製程式碼
- 優先通過Factory2和Factory來建立View,這兩個Factory等會再說;
- 通過標籤名中是否包含'.'來區分待建立的View是自定義View還是系統View;
- 系統View會在
onCreateView
方法中新增android.view.
字首,然後交由createView
處理。
createView方法
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 有快取
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
if (constructor == null) { // 第一次則通過反射建立constructor
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
// 使用的是包含Context, AttributeSet這兩個引數的建構函式
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor); // 新增到快取中
} else { // 命中快取
// If we have a filter, apply it to cached constructor
if (mFilter != null) { // 先過濾
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
// 反射建立View例項物件
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 如果是ViewStub則懶載入
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
}
...
}
複製程式碼
通過反射待建立View的建構函式(兩個引數:Context和AttributeSet的建構函式)來例項化View物件,如果是ViewStub物件還會進行懶載入。
LayoutInflater.Factory/Factory2
通過以上流程,使用LayoutInflater的infalte方法載入佈局檔案的整體流程就分析完了。但出現了Factory2
和Factory
類,它們會優先建立View,我們來看看著兩個類到底是什麼!
它們都是LayoutInflater的內部類——兩個介面:
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
複製程式碼
Factory2
繼承了Factory
,增加了一個帶View parent
引數的onCreateView
過載方法。它們是在createViewFromTag
中被呼叫的,預設為null,說明開發人員可以自定義這兩個Factory,則通過它們可以改造待載入XML佈局中的View標籤,來使用自定義規則建立View。
來看一下它們的設定方法:
public void setFactory(Factory factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
// 和setFactory2類似
...
}
}
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 { // 合併原有的Factory
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
複製程式碼
可以看到Factory
和Factory2
只能設定一次,否則會拋異常。
這兩個Factory的區別是什麼?
- Factory2 是API 11 被加進來的;
- Factory2 繼承自 Factory,也就說現在直接使用Factory2即可;
- Factory2 可以對建立 View 的 Parent 進行操作;
那如何應用呢?
Factory2/Factory的應用
AppCompatActivity中的應用
先看一張圖:
這個佈局中使用的是正常的標籤<TextView>
和<Button>
,但通過Layout Inspector工具分析頁面會發現它們被替換成了AppCompatTextView
和AppCompatButton
。
跟蹤一下AppCompatActivity
的onCreate
方法:
protected void onCreate(@Nullable Bundle savedInstanceState) {
AppCompatDelegate delegate = this.getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
...
super.onCreate(savedInstanceState);
}
複製程式碼
委託到了AppCompatDelegate
類,並且呼叫了installViewFactory
方法。找到這個類的一個實現AppCompatDelegateImpl
(不同版本的原始碼這個實現類的名字不同):
class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2
複製程式碼
看到關鍵的Factory2
了,直接看installViewFactory
方法:
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) { // 設定自身到LayoutInflater
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");
}
}
複製程式碼
通過LayoutInflaterCompat.setFactory2
將AppCompatDelegateImpl
設定到LayoutInflater中。繼續跟蹤onCreateView
的實現,會走到createView
方法:
public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (this.mAppCompatViewInflater == null) {
TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
String viewInflaterClassName = a.getString(styleable.AppCompatTheme_viewInflaterClass);
if (viewInflaterClassName != null && !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
try {
Class viewInflaterClass = Class.forName(viewInflaterClassName);
this.mAppCompatViewInflater = (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();
} catch (Throwable var8) {
Log.i("AppCompatDelegate", "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", var8);
this.mAppCompatViewInflater = new AppCompatViewInflater();
}
} else {
this.mAppCompatViewInflater = new AppCompatViewInflater();
}
}
...
return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}
複製程式碼
會建立一個AppCompatViewInflater
類,並且呼叫了它的createView
方法,看樣子找到源頭了。
來到AppCompatViewInflater
類:
public class AppCompatViewInflater {
...
final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
Context originalContext = context;
...
View view = null;
byte var12 = -1;
switch(name.hashCode()) {
...
case -938935918:
if (name.equals("TextView")) {
var12 = 0;
}
break;
...
}
switch(var12) {
case 0:
view = this.createTextView(context, attrs);
this.verifyNotNull((View)view, name);
break;
...
default:
view = this.createView(context, name, attrs);
}
...
return (View)view;
}
@NonNull
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
...
}
複製程式碼
通過name
引數拿到TextView標籤後,直接替換成了AppCompatTextView。
通過這波操作,將一些 widget 自動變成相容widget (例如將 TextView 變成 AppCompatTextView)以便於向下相容新版本中的特性。
那我們也可以仿照AppCompatActivity來自定義Factory實現自己需要的替換效果。
自定義Factory2
大多換膚功能的實現就是通過實現自定義Factory,攔截特定View,然後修改這些View的屬性值,或者直接返回自定義的View。舉個栗子:
- 替換TextView的文字顏色;
- 在不建立selector檔案前提下實現圓角按鈕;
先看XML:
<LinearLayout 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"
android:id="@+id/container"
android:orientation="vertical"
tools:ignore="MissingPrefix">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center"
android:textSize="20sp"
android:textColor="@color/third_tv_text_color"
android:text="測試設定的Factory"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:padding="20dp"
android:layout_gravity="center_horizontal"
android:background="#ffbccc"
android:text="蘋果獼猴桃牛油果榴蓮"
android:textSize="15sp"
app:cornerRadius="5dp"
app:strokeWidth="1dp"
app:strokeColor="#ccffcc"/>
</LinearLayout>
複製程式碼
注意:這裡用的是系統View的標籤,但屬性裡用到了自定義屬性。
<resources>
<declare-styleable name="MyTextView">
<attr name="android:textColor"/>
</declare-styleable>
<declare-styleable name="RoundButton">
<attr name="cornerRadius" format="dimension" />
<attr name="strokeWidth" format="dimension" />
<attr name="strokeColor" format="color" />
</declare-styleable>
</resources>
複製程式碼
注意:如果想直接替換Android自帶屬性,需要在自定義屬性里加上android:
字首。
public class MyFactory implements LayoutInflater.Factory2 {
public LayoutInflater.Factory mOriginalFactory;
// 這個模擬新資源
private Map<String, String> mColorMap;
public MyFactory(LayoutInflater.Factory factory) {
this.mOriginalFactory = factory;
mColorMap = new HashMap<>();
// 模擬新的皮膚資源——新文字顏色
mColorMap.put("third_tv_text_color", "#0000ff");
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return onCreateView(name, context, attrs);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = null;
if (mOriginalFactory != null) {
view = mOriginalFactory.onCreateView(name, context, attrs);
}
if ("TextView".equals(name)) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
// 注意這裡的屬性名:android:textColor,不用自定義名稱空間
int resourceId = ta.getResourceId(R.styleable.MyTextView_android_textColor, -1);
String resourceName = context.getResources().getResourceName(resourceId);
resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1);
view = new TextView(context, attrs); // 可以直接修改原TextView的屬性
ta.recycle();
// 這裡模擬替換原TextView的textColor屬性值
String color = mColorMap.get(resourceName);
((TextView) view).setTextColor(Color.parseColor(color));
}
if ("Button".equals(name)) {
view = new Button(context, attrs);
// 讀取自定義的屬性值
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundButton);
float radius = ta.getDimension(R.styleable.RoundButton_cornerRadius, 0);
float strokeWidth = ta.getDimension(R.styleable.RoundButton_strokeWidth, 0);
int strokeColor = ta.getColor(R.styleable.RoundButton_strokeColor, -1);
// 構造圓角按鈕
GradientDrawable drawable = new GradientDrawable();
drawable.setCornerRadius(DensityUtil.dip2px(context, radius));
drawable.setStroke(DensityUtil.dip2px(context, strokeWidth), strokeColor);
view.setBackground(drawable);
ta.recycle();
}
return view;
}
}
複製程式碼
注意:新資源可以通過其他方式儲存和獲取,從而實現動態熱換膚;如果使用的是AppCompatActivity,自定義Factory必須在呼叫super.onCreate
之前設定,因為它已經有了一個Factory;如果使用的是Activity,則必須在呼叫setContentView
方法之前設定。
效果:
可以看到TextView文字的顏色變成了藍色;在不提供自定義drawable的xml檔案以及不使用自定義View標籤的前提下,實現了圓角按鈕。
本文的相關示例程式碼都在:zjxstar的GitHub上,感興趣的同學可以看下。
總結
LayoutInflater的相關分析就這麼多,文章有點長,慢慢看吧!
- LayoutInflater的inflate的過程的核心方法是:createViewFromTag 和 createView 方法;
- LayoutInflater通過PULL解析器來解析XML佈局檔案,通過反射來建立View物件;
- LayoutInflater.Factory只能設定一次,可以用來替換View;
參考資料
- 換膚、全域性字型替換、無需編寫shape、selector 的原理Factory小結mp.weixin.qq.com/s/1ua0geFnr…
- Android系統原始碼分析--View繪製流程之-inflatejuejin.im/post/5bfa7f…
- Android LayoutInflater Factory 原始碼解析www.jianshu.com/p/9c16bbaee…
- Android LayoutInflater 原始碼解析www.jianshu.com/p/f0f3de2f6…