好久没写文章了,因此打算水一篇文章,毕竟这方面知识的文章有不少不少。前端
前段时间流行起来了忽然不肯意写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;
......
......
}
复制代码
是否是这么看,你们基本就懂了原理,这样你再去看它的库,或者要加上什么本身特定的属性,都有能力本身去进行修改了。
固然还有不少奇思妙想的用处,只要你们想象力够多,就能够在这中间作各类骚操做。
不当心把文章就水完了.......有错误欢迎你们指出。