如何实现插件化换肤
1.拿到App中要替换的view以及该view在App中的resId
2.通过该view的resId它拿到在App中的属性名字和类型等信息,然后通过这些信息拿到皮肤中的id,暂命名为skinId
3.把skinId设置给要替换的view
来看一个例子,我们新建一个TestActivity继承自AppCompatActivity :
public class TestActivity extends AppCompatActivity {
private static final String TAG = TestActivity.class.getSimpleName();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
TextView mTvTest = findViewById(R.id.tv_test);
Log.d(TAG, "The instance is:" + mTvTest);
TextView textView = new TextView(this);
Log.d(TAG, "The instance is:" + textView);
}
}
布局文件如下:
<?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">
<TextView
android:id="@+id/tv_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
Manifest.xml里面的theme要继承Theme.AppCompat.Light.NoActionBar,运行到手机上后打印信息如下:
The instance is:androidx.appcompat.widget.AppCompatTextView
The instance is:android.widget.TextView
发现了吗?我们自己new出来的TextView与布局文件里的TextView不一样,说明sdk对布局文件里的TextView进行了改造。sdk里面具体是怎么实现的呢,我们来看看AppCompatActivity的onCreate()方法:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
其中第4行的delegate.installViewFactory()会调用AppCompatDelegateImpl的installViewFactory()方法,代码如下:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
在这里设置了setFactory2为this,通过上一篇文章LayoutInflater.inflate参数配置,我们知道inflate()方法会通过createViewFromTag()来根据节点名创建View对象,如果mFactory2 != null,会调用mFactory2.onCreateView(parent, name, context, attrs),所以这里会调用AppCompatDelegateImpl的onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,最终调用AppCompatViewInflater的createView(),在这里我们可以看到把TextView改造成AppCompatTextView的代码,这就是继承AppCompatActivity打印的布局的实例是AppCompatTextView的原因。
我们也可以模仿这种方法通过onCreateView()拿到view以及resId等信息。AppCompatActivity 的布局是通过setContentView()解析的,所以我们必须在setContentView()之前执行setFactory2()方法,同时我们还需要注意LayoutInflater中的setFactory2(Factory2 factory)代码如下:
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 {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
如果mFactorySet为true,会抛出异常,我们还需要提前把mFactorySet的值改为false,再执行setFactory2()方法。