LayoutInflaterFactory app内全局更换字体
效果图.png-
LayoutInflater 大家肯定很熟悉,布局填充器。但是LayoutInflaterFactory我们缺很少用,今天就来学习一下,LayoutInflaterFactory~
-
LayoutInfater,我们平时最常用就是inflate方法,其实它还有2个方法
- setFactory()
- setFactory2()
-
2个方法,作用其实是一样的,只是setFactory2是SDK11后加入的,所以要兼容以前的版本的话,我们可以用v4包中的LayoutInflaterCompat。
LayoutInflaterFactory,它的作用是什么?简单来讲就是,我们在布局中写的控件,在反射构造完后在设置到View树之前,先过一把我们写的工厂类。我们可以对它一些操作,甚至替换掉~
- 先来写一段代码~
Activity...
@Override
protected void onCreate(Bundle savedInstanceState) {
createTextTypeface();
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//动态替换成带字体的TextView
if (name.equals("TextView")) {
return new Button(MainActivity.this, attrs);
}
return view;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
-
在Activity的onCreate调用时,调用父类super.onCreate()之前,安装我们的工厂类,重写onCreateView方法,在这个方法回传了name,attrs等信息,这个name是什么呢,其实就是控件在布局上的名称,例如我们在布局上写了一个TextView,这个的name就是TextView。其实我们在布局上面写官方控件的时候,为什么我们的自定义控件要写全包名,而系统控件时却不用呢?其实在Inflate内部去Xml解析时,会先加上一个“android.view.”的前缀拼接后尝试去反射,所以我们就不用全类名了,而像RecyclerView,自定义控件这些就需要我们去写全类名了,因为系统里面没有内置他们的前缀~
-
所以这里的name,如果是系统控件,则直接使用布局中的名字,而其他控件则判断使用全类名。而attrs,就是使用的属性和对应的值。
-
上述代码,判断如果是TextView我们就偷偷返回了一个Button,将属性也传递进去,本来要显示的TextView就变了一个Button了。是不是很简单呢。
其实还有没有问题呢?
- 其实在5.0开始,谷歌为了让低版本支持MD,就占用了这个接口,像TextView就有AppCompatTextView,Button就有AppCompatButton,依次类推,那么多控件,如果要更换,还补得换死人喔,但是有了这个工厂,就能一键式全更换啦。其实在AppCompateActivity就已经做了这一步了。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
//创建代理
final AppCompatDelegate delegate = getDelegate();
//这里开始替换
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
@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");
}
}
}
-
我们看到,在onCreate()方法,调用了installViewFactory()方法,其实就是调用了LayoutInflater.setFactory(),这里有个非空判断,如果之前已经安装了,则不安装了,并打印一句log,如果是这样的话,AppCompatActivity的替换Compat系列控件就无法替换了,那怎么兼容这一块呢?
-
上述的操作都是由一个AppCompatDelegate来进行的,一开始先getDelegate()。
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
- 没有创建就创建一个AppCompatDelegate
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
- 这里就是根据当前运行的版本号去创建不同版本的代理实现,并且高版本的代理是继承低版本的,通过复写来达到兼容,例如5.0的阴影,也是一样的做法,低版本的实现类直接是空实现。
class AppCompatDelegateImplV11 extends AppCompatDelegateImplV9 {
}
- 创建完代理后,调用delegate.installViewFactory();在installViewFactory()方法中,安装了工厂给LayoutInflate,其实就是自身,回调View的操作是onCreateView,所以我们来看它的onCreateView()
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
//替换并返回
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
- 最终返回的是mAppCompatViewInflater.createView(),继续跟踪
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
...省略部分代码
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
省略部分代码...
return view;
}
- 原来是在这里判断系统控件,去替换AppCompat系列的组件,那么我们只要手动调用这个onCreateView去替换掉,不需要使用AppCompatActivity的install()。这个方法在哪呢?在AppCompatViewInflater这个类,这个类可以由AppCompatActivity的getDelegate().createView()来调用,所以,我们写的时候就可以这样:
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//这里先做自己的替换操作
if (name.equals("TextView")) {
return new xxx();
}
//为了让AppCompat的控件替换到,所以调用AppCompat的替换进行替换
View view = getDelegate().createView(parent, name, context, attrs);
return view;
}
});
用途
- 既然可以使用工厂来进行替换原生控件,那么我们的给全局的TextView都加上字体就很容易啦
-
先将我们的字体放在src-main-assets文件夹,没有则需要自己新建,这个大家都懂
-
创建Typeface对象,给TextView设置setTypeface
public class MainActivity extends AppCompatActivity {
private Typeface mTypeface;
@Override
protected void onCreate(Bundle savedInstanceState) {
createTextTypeface();
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//为了让AppCompat的控件替换到,所以调用AppCompat的替换进行替换
View view = getDelegate().createView(parent, name, context, attrs);
if (view != null && view instanceof TextView) {
//这里给每个TextView都设置上字体
((TextView) view).setTypeface(mTypeface);
}
return view;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//创建字体
private void createTextTypeface() {
mTypeface = Typeface.createFromAsset(getAssets(), "QingXinKaiTi.ttf");
}
}
- 也可以自定义TextView去调用字体设置,替换TextView喔
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//动态替换成带字体的TextView
if (name.equals("TextView")) {
return new FontTextView(MainActivity.this, attrs);
}
//为了让AppCompat的控件替换到,所以调用AppCompat的替换进行替换
View view = getDelegate().createView(parent, name, context, attrs);
if (view != null && view instanceof TextView) {
((TextView) view).setTypeface(mTypeface);
}
return view;
}
});
- 最后将这句话放到Activity的基类的onCreate(),这样就大功告成啦~
网友评论