android换肤:主要是将资源文件,包括R.drawable.xx,R.mipamap.xx,R.color.xx更换为皮肤包中的资源。
- Android中对控件的资源设置,如下一个简单例子:
btnSettings.setImageDrawable(SkinResources.getInstance().getDrawable(R.drawable.btn_setting_bg));
mTabLayout.setBackgroundColor(SkinResources.getInstance().getColor(R.color.home_tab_bar_bg_clr));
<ImageView
android:layout_width="wrap_content"
android:src="@drawable/app_icon"
android:layout_height="wrap_content" />
// ImageView.java 解析属性
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
// 通过TypedArray获取图片
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
// TypedArray.getDrawable() 方法
public Drawable getDrawable(@StyleableRes int index) {
// 省略部分代码....
// 加载资源其实是通过mResources去获取的
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
换肤要解决的问题:
(1)怎样把皮肤包资源替换掉系统的资源?
利用反射设置
private Resources getSkinResources(String skinPath) {
try {
AssetManager assets = AssetManager.class.newInstance();
Method addAssetPath = assets.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, skinPath);
Resources superRes = mAppContext.getResources();
Resources temp = new Resources(assets, superRes.getDisplayMetrics(), superRes.getConfiguration());
return temp;
} catch (Exception e) {
Log.i("getSkinResources",e.getStackTrace()+"================="+e.getMessage());
return null;
}
}
如果是apk皮肤包
final Context context = createPackageContext(skinPackName, Context.CONTEXT_IGNORE_SECURITY);
//layout.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.activity_bg));
Resources resources = context.getResources();
int indentify = resources.getIdentifier(context.getPackageName()+":drawable/activity_bg", null, null);
if(indentify>0){
layout.setBackgroundDrawable(context.getResources().getDrawable(indentify));
}
(2)怎样去找到xml中需要换肤的view?
利用LayoutInflateFactory.onCreateView(View parent, String name, Context context, AttributeSet attrs) 方法
阅读源码setContentView(),
LayoutInflaterFactory在系统填充view前会回调该接口,你可以去自定义布局的填充;
LayoutInflater根据xml布局文件来渲染View视图的主要流程:
先是通过布局文件的资源ID创建一个XmlResourceParser解析器对象parser,
再是利用parser递归解析xml布局文件,然后根据解析出的标签名来创建相关View,最终返回层级视图View。
如果LayoutInflater中设置了Factory,那么在创建每一个View时都会调用该Factory的onCreateView()方法,
这个方法就是我们的入口点,如果想在每一个View创建之前做点处理,只需要在Factory的onCreateView()方法中做相关逻辑操作…
已经找到了创建View的切入口。
String name:是控件的名称,如TextView,Button等
AttributeSet attrs 保存了view的一些属性;
String attrName = attrs.getAttributeName(i); 如layout_width,textColor,background
String attrValue = attrs.getAttributeValue(i);是资源的id,如:@17170443
// 注意需在调用super.onCreate(savedInstanceState);之前设置LayoutInflaterFactory
//查看源码部分: super.onCreate(savedInstanceState);----installViewFactory----AppCompatDelegateImplV9,
LayoutInflaterCompat.setFactory(getLayoutInflater(), getSkinCompatDelegate());
super.onCreate(savedInstanceState);
关联关系:
LayoutInflaterFactory的onCreateView()方法与LayoutInflater.Factory的onCreateView()之间的关联?如果相通则问题解决。
第一层关系:LayoutInflater.Factory的onCreateView()方法实际上是由LayoutInflaterFactory的onCreateView()实现的。
第二层关系:LayoutInflaterCompatBase .setFactory(LayoutInflater inflater, LayoutInflaterFactory factory)
此处传给的LayoutInflaterFactory对象,通过对FactoryWrapper封装,而FactoryWrapper implements LayoutInflater.Factory,从而实现了,表面上看是设置LayoutInflaterFactory,本质上是对LayoutInflater.Factory设置,因此二者的联系建立。
class LayoutInflaterCompatBase {
*static class FactoryWrapper implements LayoutInflater.Factory*{
final LayoutInflaterFactory mDelegateFactory;
FactoryWrapper(LayoutInflaterFactory delegateFactory) {
mDelegateFactory = delegateFactory;
}
*@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return mDelegateFactory.onCreateView(null, name, context, attrs);
}*
public String toString() {
return getClass().getName() + "{" + mDelegateFactory + "}";
}
}
static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
inflater.setFactory(factory != null ? new FactoryWrapper(factory) : null);
}
static LayoutInflaterFactory getFactory(LayoutInflater inflater) {
*LayoutInflater.Factory factory = inflater.getFactory();
if (factory instanceof FactoryWrapper) {
return ((FactoryWrapper) factory).mDelegateFactory;*
}
return null;
}
}
至此明白:如果想要对LayoutInflater.Factory的onCreateView()方法里做一些操作,只需在外界通过setLayoutInflaterFactory,并覆写其onCreateView()方法即可。
LayoutInflaterCompat.setFactory(getLayoutInflater(), getSkinCompatDelegate());
public class SkinCompatDelegate implements LayoutInflaterFactory{
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createView(parent, name, context, attrs);
if (view == null) return null;
/*mSkinViews搜集需要换肤的view集合*/
if (view instanceof Skinnable) {
mSkinViews.add(new WeakReference<>((Skinnable) view));
}
parseSkinAttrs(context, attrs, view);
return view;
}
}
(3)将xml中的view换肤。
a . 哪些是需要换肤的view?
private static final String[] SKIN_ATTRS = {"background", "textColor", "src", "divider", "listSelector","textColorHint", "progressDrawable", "thumb", "drawableLeft", "drawableTop", "drawableRight","drawableBottom"};
public static boolean isSupportAttrType(String attrName) {
for (String name : SKIN_ATTRS) {
if (name.equalsIgnoreCase(attrName)) return true;
}
return false;
}
b. view中某一个属性的如何替换掉?
像R.drawable.text_color_selector
* typeName是text_color_selector;
* entryName是drawable;
* 然后通过这个名称获取该资源在皮肤包中的具体id
Resources res = context.getResources();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
SkinAttr skinAttr = null;
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
/*isSupportAttrType(attrName) 用来判断需要换肤的view的attr值*/
if (SkinAttrSupport.isSupportAttrType(attrName)) {
if (attrValue.startsWith("@")) {
try {
int id = Integer.parseInt(attrValue.substring(1));
/*R.drawable.text_color_selector
* typeName是text_color_selector;
* entryName是drawable;
* 然后通过这个名称获取该资源在皮肤包中的具体id
* /然后就是获取皮肤包中的资源id了
int drawavleId = apk.getResources().getIdentifier(resEntryName, resTypeName,apk.getPackageName());
* */
String typeName = res.getResourceTypeName(id);
String entryName = res.getResourceEntryName(id);
// 然后就是获取皮肤包中的资源id了
int drawavleId = apk.getResources().getIdentifier(resEntryName, resTypeName,apk.getPackageName());
//通过资源拿到图片(特指apk皮肤包)
Drawable drawable = SkinResources.getInstance().getDrawable(resId);
//给指定view设置
view.setBackgroundDrawable(drawable);
} catch (Exception e) {
skinAttr = null;
}
}
}