在进行换肤框架讲解之前,我先把View创建过程说一下:
调用Context.getSystemService()方法
LayoutInflater inflater = LayoutInflater.from(context);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View rootView = inflater.inflate(R.layout.view_layout, null);
直接使用LayoutInflater.from()方法
View rootView = inflater.inflate(R.layout.view_layout, null);
在Activity下直接调用getLayoutInflater()方法
LayoutInflater inflater = getLayoutInflater();
View rootView = inflater.inflate(R.layout.view_layout, null);
使用View的静态方法View.inflate()
rootView = View.inflate(context, R.layout.view_layout, null);
这四种方式他们的源代码实质都是一样的,过程都是先获取LayoutInflater 对象context.getSystemService(Context.LAYOUT_INFLATER_SERVICE),然后调用LayoutInflater 对象的函数inflate(int resource, ViewGroup root, boolean attachToRoot),生成对应的View,我们来看看inflate函数:
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
if (DEBUG) System.out.println("INFLATING from resource: " + resource);
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
可以看到上面的函数是先通过resourceId获取对应的XmlResourceParser对象,这个对象用于解析XML文件的。
然后在调用inflate()函数,接着往下看
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
mConstructorArgs[0] = mContext;
View result = root;
try {
// 寻找root节点
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
....
....
if (TAG_MERGE.equals(name)) {
....
....
} else {
// Temp is the root view that was found in the xml
View temp;
if (TAG_1995.equals(name)) {
temp = new BlinkLayout(mContext, attrs);
} else {
temp = createViewFromTag(root, name, attrs);
}
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 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
rInflate(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
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) {
result = temp;
}
}
} catch (XmlPullParserException e) {
....
....
}
return result;
}
}
关键代码就是那两句红色代码,首先找到开始节点,然后调用CreateViewFromTag创建root布局,然后调用rinflate函数创建root布局的所有子View。
上面两个函数,我们先看CreateViewFromTag函数
View createViewFromTag(View parent, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
.....
try {
View view;
if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
else view = null;
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
}
if (view == null) {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
if (DEBUG) System.out.println("Created view is: " + view);
return view;
} catch (InflateException e) {
....
....
}
}
可以看到mFactory,Factory两个对象用户创建view视图,只有当两个对象都没有创建成功时才会依次调用mPrivateFactory.onCreateView,onCreateView,createView方法,直达view创建成功为止。
其中mFactory,Factory是LayoutInflater的两个可实现的接口,他们的作用是用于创建View,我们在进行皮肤替换的时候,就是对Factory接口进行实现然后替换系统自带的Factory接口,LayoutInflater的boolean类型的标志位mFactorySet代表着是否设置过factory接口。其中AppCompatActivity,在setContentView之前factory接口就设置过,mFactorySet就被设置成true,因此用户自己设置factory就会报错,提示已经被设置过的错误,因此在AppCompatActivity中,就需要通过反射将mFactorySet设置成false,然后把自己实现的factory设置给LayoutInflater,这些操作都必须在setContentView之前完成。
在生成view视图的时候,如果我们已经设置了factory,那么我们的view就一定是根据factory来生成的,我们可以在实现了的factory中来收集需要皮肤更新的view。
换肤框架的思路就是如下:
这里面最主要的两个部分就是,factory的实现类,异步加载其他apk资源的类,其他的类就是容器,接口,回调函数之类的。容器的关系是,一个view容器内部对应一个属性容器表,因为每一个view有多个属性,而属性也需要一个容器类来保存,因为不同的属性设置函数不一样,所以需要多个容器来保存不同的属性。下面来看看factory的onCreate函数(都有注释)
public class SkinFactory implements Factory {
private static final String DEFAULT_SCHEMA_NAME = "http://schemas.android.com/apk/res-auto";
private static final String DEFAULT_ATTR_NAME = "enable";
private List<SkinView> mSkinViews = new ArrayList<SkinView>();
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = null;
//判断view有没有skin:enable属性,自定义属性,设置了就表示该view需要换肤
final boolean skinEnable = attrs.getAttributeBooleanValue(DEFAULT_SCHEMA_NAME, DEFAULT_ATTR_NAME, false);
if(skinEnable) { //有这个属性,那么就使用自定义函数来创建view
view = createView(name, context, attrs);
if(null != view) {
//收集所有需要换肤的view加入到list中
parseAttrs(name, context, attrs, view);
}
}
//如果返回的是null,那么就会调用系统的创建方法,前面已经说过了
return view;
}
public final View createView(String name, Context context, AttributeSet attrs) {
View view = null;
//给view设置前缀,这样系统才能识别是什么控件
if(-1 == name.indexOf('.')) {
if("View".equalsIgnoreCase(name)) {
view = createView(name, context, attrs, "android.view.");
}
if(null == view) {
view = createView(name, context, attrs, "android.widget.");
}
if(null == view) {
view = createView(name, context, attrs, "android.webkit.");
}
} else {
view = createView(name, context, attrs, null);
}
return view;
}
View createView(String name, Context context, AttributeSet attrs, String prefix) {
View view = null;
try {
view = LayoutInflater.from(context).createView(name, prefix, attrs);
} catch (Exception e) {
}
return view;
}
private void parseAttrs(String name, Context context, AttributeSet attrs, View view) {
int attrCount = attrs.getAttributeCount();
final Resources temp = context.getResources();
List<BaseAttr> viewAttrs = new ArrayList<BaseAttr>();
for(int i = 0; i < attrCount; i++) {
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if(isSupportedAttr(attrName)) {
//由此可见,属性只有设置了@引用资源的才能换肤
if(attrValue.startsWith("@")) {
int id = Integer.parseInt(attrValue.substring(1));
String entryName = temp.getResourceEntryName(id);
String entryType = temp.getResourceTypeName(id);
BaseAttr viewAttr = createAttr(attrName, attrValue, id, entryName, entryType);
if(null != viewAttr) {
viewAttrs.add(viewAttr);
}
}
}
}
if(viewAttrs.size() > 0) {
SkinView skinView = new SkinView();
skinView.view = view;
skinView.viewAttrs = viewAttrs;
mSkinViews.add(skinView);
}
}
// attrName:textColor attrValue:2130968576 entryName:common_bg_color entryType:color
private BaseAttr createAttr(String attrName, String attrValue, int id, String entryName, String entryType) {
BaseAttr viewAttr = null;
//目前只实现了background和textcolor属性设置
if("background".equalsIgnoreCase(attrName)) {
viewAttr = new BackgroundAttr(); //背景属性容器
} else if("textColor".equalsIgnoreCase(attrName)) {
viewAttr = new TextColorAttr(); //文字颜色属性容器
}
if(null != viewAttr) {
viewAttr.attrName = attrName;
viewAttr.attrValue = id;
viewAttr.entryName = entryName;
viewAttr.entryType = entryType;
}
return viewAttr;
}
//是否支持该属性
private boolean isSupportedAttr(String attrName) {
if("background".equalsIgnoreCase(attrName)) {
return true;
} else if("textColor".equalsIgnoreCase(attrName)) {
return true;
}
return false;
}
//应用皮肤
public void applaySkin() {
if(null != mSkinViews) {
for(SkinView skinView : mSkinViews) {
if(null != skinView.view) {
skinView.apply();
}
}
}
}
}
public final class SkinManager {
private static final Object mClock = new Object();
private static SkinManager mInstance;
private Context mContext; //当前上下文
private Resources mResources; //该变量是保存对应皮肤资源的Resources 对象,不同的Resources 代表着不同的皮肤
private String mSkinPkgName; //Resources 对应的包名
private SkinManager() {
}
//懒汉加载单例
public static SkinManager getInstance() {
if(null == mInstance) {
synchronized (mClock) {
if(null == mInstance) {
mInstance = new SkinManager();
}
}
}
return mInstance;
}
public void init(Context context) {
enableContext(context);
mContext = context.getApplicationContext();
}
public void loadSkin(String skinPath) {
loadSkin(skinPath, null);
}
public void loadSkin(final String skinPath, final ILoadListener listener) {
enableContext(mContext);
if(TextUtils.isEmpty(skinPath)) {
return;
}
//异步加载其他apk的资源 ,给定一个apk包路径,返回一个资源Resources
new AsyncTask<String, Void, Resources>() {
@Override
protected void onPreExecute() {
if(null != listener) {
listener.onStart();
}
}
@Override
protected Resources doInBackground(String... params) {
if(null != params && params.length == 1) {
String skinPath = params[0];
File file = new File(skinPath);
if(null != file && file.exists()) {
PackageManager packageManager = mContext.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, 1);
if(null != packageInfo) {
mSkinPkgName = packageInfo.packageName;
}
return getResources(mContext, skinPath);
}
}
return null;
}
@Override
protected void onPostExecute(Resources result) {
if(null != result) {
mResources = result;
if(null != listener) {
// 如果返回的Resources 不是空的话,那么就回调listener,这里就回到了activity中
// 根据之前说factory是在setContentView()之前设置的,所以Activity中就可以使用factory
// 然后这个回调就调用 factory.applyskin()函数,这样收集的view就会设置Resources 对应的属性
// 下面的getColor(),getDrawer(),函数就是根据ID返回Resources中的属性值, 那么就可以达到换肤的效果了。
listener.onSuccess();
}
} else {
if(null != listener) {
listener.onFailure();
}
}
}
}.execute(skinPath);
}
public Resources getResources(Context context, String apkPath) {
try {
//下面四行是固定写法,反射加载静态资源。
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, apkPath);
//生成Resources
Resources r = context.getResources();
Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
return skinResources;
} catch (Exception e) {
}
return null;
}
public void restoreDefaultSkin() {
if(null != mResources) {
mResources = null;
mSkinPkgName = null;
}
}
public int getColor(int id) {
enableContext(mContext);
Resources originResources = mContext.getResources();
int originColor = originResources.getColor(id);
if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {
return originColor;
}
String entryName = mResources.getResourceEntryName(id);
int resourceId = mResources.getIdentifier(entryName, "color", mSkinPkgName);
try {
return mResources.getColor(resourceId);
} catch (Exception e) {
}
return originColor;
}
public Drawable getDrawable(int id) {
enableContext(mContext);
Resources originResources = mContext.getResources();
Drawable originDrawable = originResources.getDrawable(id);
if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {
return originDrawable;
}
String entryName = mResources.getResourceEntryName(id);
int resourceId = mResources.getIdentifier(entryName, "drawable", mSkinPkgName);
try {
return mResources.getDrawable(resourceId);
} catch (Exception e) {
}
return originDrawable;
}
private void enableContext(Context context) {
if(null == context) {
throw new NullPointerException();
}
}
}
最后 附上源码地址:http://download.csdn.net/detail/llew2011/9518833
参考的博客地址:http://blog.csdn.net/llew2011/article/details/51252401