对于app换肤,这是一个常见而又常用的功能。虽然我做的项目中还没涉及到换肤,但是还是想研究下。
于是,下载了鸿洋大神的换肤demo来研究。
先看效果图:(尊重鸿洋大神的代码,效果图上原创)
鸿洋大神的换肤有两种:
1,ChangeSkin;侵入式换肤
2,AndroidChangeSkin;tag换肤
这两种我都分析过。各有各的好处。不过我还是推荐使用第一种。
博文后面我会比较这两种换肤的差异,以及为什么推荐使用第一种tag换肤的原因。
这两种换肤的核心方法其实是一样的,只不过怎么获取需要换肤的资源的方法不同而已。
先来分析关键代码:
先看项目目录:
我们可以从资源的命名看出来,资源的差异在于后缀名的不同,这是app内部换肤的关键,根据后缀名不同来区分。
而插件式换肤,也就是获得sd卡里的apk文件,然后获得其中的资源文件进行换肤的资源命名也是遵循这个规则。
1,接口
ISkinCallback
/**
* 设置皮肤状态回调
*/
public interface ISkinCallback {
void onStart();//开始
void onError(Exception e);//错误
void onComplete();//完成
}
ISkinListener
/**
* 通知皮肤设置的接口
*/
public interface ISkinListener {
//这个接口用于通知应该改变皮肤资源了
void onSkinChanged();
}
执行换肤的时候其实就是调用的ISkinListener接口而已。所有继承它的view都能得到换肤的通知。
2,保存用户的皮肤喜好设置
SkinSharedPreferencesUtils 类
/**
* 保存用户对该app的皮肤设置
*/
public class SkinSharedPreferencesUtils {
private SharedPreferences sharedPreferences;//键值对实例
public SkinSharedPreferencesUtils(Context context) {
sharedPreferences = context.getSharedPreferences(SkinContacts.PREF_NAME, Context.MODE_PRIVATE);
}
//获得插件路径
public String getPluginPath() {
if (sharedPreferences == null) return "";
return sharedPreferences.getString(SkinContacts.KEY_PLUGIN_PATH, "");
}
//添加插件路径
public void setPluginPath(String pluginPath) {
if (sharedPreferences == null) return;
sharedPreferences.edit().putString(SkinContacts.KEY_PLUGIN_PATH, pluginPath).apply();
}
//获得资源后缀
public String getAttrSuffix() {
if (sharedPreferences == null) return "";
return sharedPreferences.getString(SkinContacts.KEY_PLUGIN_SUFFIX, "");
}
//添加资源后缀
public void setAttrSuffix(String attrSuffix) {
if (sharedPreferences == null) return;
sharedPreferences.edit().putString(SkinContacts.KEY_PLUGIN_SUFFIX, attrSuffix).apply();
}
//获得插件的包名
public String getPluginPackage() {
if (sharedPreferences == null) return "";
return sharedPreferences.getString(SkinContacts.KEY_PLUGIN_PACKAGE, "");
}
//添加插件的包名
public void setPluginPackage(String pluginPackage) {
if (sharedPreferences == null) return;
sharedPreferences.edit().putString(SkinContacts.KEY_PLUGIN_PACKAGE, pluginPackage).apply();
}
//清理当前的sharedPreferences
public boolean clear() {
if (sharedPreferences == null) return false;
return sharedPreferences.edit().clear().commit();
}
}
这没啥好说的,键值对保存在app中。
3,定义换肤常量
SkinContacts类
/**
* 皮肤常量
*/
public class SkinContacts {
public static final String PREF_NAME = "skin_plugin_name";//插件工厂名
public static final String KEY_PLUGIN_PATH = "key_plugin_path";//插件路径
public static final String KEY_PLUGIN_PACKAGE = "key_plugin_package";//插件包名
public static final String KEY_PLUGIN_SUFFIX = "key_plugin_suffix";//插件后缀
public static final String ATTR_PREFIX = "skin:";//资源验证的前缀,只有带有skin的前缀才是要被修改的前缀
public static final int SKIN_TAG = ;//表填验证,获得当前view的所有名字
}
4,重点方法1:皮肤资源管理器
ResourceManager类
/**
* 皮肤资源管理器
*/
public class ResourceManager {
private Resources mResources;//资源对象
private String mPluginPackageName;//插件包名
private String mSuffix;//皮肤区别的后缀名
//默认图片和颜色类型
private static final String DEFTYPE_DRAWABLE = "drawable";
private static final String DEFTYPE_COLOR = "color";
public ResourceManager(Resources mResources, String mPluginPackageName, String mSuffix) {
this.mResources = mResources;
this.mPluginPackageName = mPluginPackageName;
this.mSuffix = TextUtils.isEmpty(mSuffix) ? "" : mSuffix;
}
//将资源名添加后缀,用于app内部的皮肤修改
private String appendSuffix(String name) {
//如果设置了皮肤的后缀名,则在资源名称的后面添加后缀名
//例如:默认皮肤的资源名是:skin_index_drawable
//如果添加了后缀名,则说明使用了另外一套皮肤:skin_index_drawable_red
return TextUtils.isEmpty(mSuffix) ? name : name + "_" + mSuffix;
}
//传入资源名称,根据包名和资源后缀名来确定返回的资源
public Drawable getDrawableByName(String name) {
try {
name = appendSuffix(name);
//这段代码的意思相当于在指定的包名下找到指定的资源文件夹名字然后找到指定的资源名称。参数是相反的。
//参数:1,资源名;2,资源类型;3,包名
return mResources.getDrawable(mResources.getIdentifier(name, DEFTYPE_DRAWABLE, mPluginPackageName));
} catch (Resources.NotFoundException e) {
try {
//如果在图片中没有找到资源就在颜色资源里找
return mResources.getDrawable(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));
} catch (Resources.NotFoundException e2) {
e.printStackTrace();
return null;
}
}
}
//根据资源名获得颜色
public int getColorByName(String name) {
try {
name = appendSuffix(name);
return mResources.getColor(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));
} catch (Resources.NotFoundException e) {
e.printStackTrace();
return -;
}
}
//根据资源名获得颜色集合
public ColorStateList getColorStateList(String name) {
try {
name = appendSuffix(name);
return mResources.getColorStateList(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));
} catch (Resources.NotFoundException e) {
e.printStackTrace();
return null;
}
}
//根据资源名获得图片集合
public ColorStateList getColorStateListtDrawable(String name) {
try {
name = appendSuffix(name);
return mResources.getColorStateList(mResources.getIdentifier(name, DEFTYPE_DRAWABLE, mPluginPackageName));
} catch (Resources.NotFoundException e) {
e.printStackTrace();
return null;
}
}
}
ResourceManager类用于保存当前app的资源对象或者从插件中获取的资源对象,用于得到资源对象中的资源。实际的换皮肤的资源和方法都由它管理。
5,重点方法2:skin
从项目结构图可以看出,skin目录下有四个java文件。
他们创建的顺序是:
1,SkinAttrType 具体修改皮肤的枚举
2,SkinAttr 保存资源名和资源类型,一一对应
3,SkinView 保存需要更改皮肤的view和view下的所有资源,一一对应
4,SkinAttrSupport 获得一个view下所有资源集合的工具类
1,SkinAttrType
/**
* 3,资源类型枚举
*/
public enum SkinAttrType {
BACKGROUD("background")//背景,将给传入的view设定新的背景
{
@Override
public void apply(View view, String resName) {
//背景可能是图片也可能只是颜色
Drawable drawable = getResourceManager().getDrawableByName(resName);
if (drawable == null) return;
view.setBackgroundDrawable(drawable);
LogUtils.i("背景:" + resName + "view的id:" + view.getId());
}
},
TEXT_COLOR("textColor")//字体颜色,将给传入的view设定新的字体颜色
{
@Override
public void apply(View view, String resName) {
if (view instanceof TextView) {
ColorStateList colorlist = getResourceManager().getColorStateList(resName);
if (colorlist == null) return;
((TextView) view).setTextColor(colorlist);
LogUtils.i("字体颜色:" + resName + "view的id:" + view.getId());
}
}
},
SRC("src")//src图片,将给传入的view指定新的图片
{
@Override
public void apply(View view, String resName) {
if (view instanceof ImageView) {
Drawable drawable = getResourceManager().getDrawableByName(resName);
if (drawable == null) return;
((ImageView) view).setImageDrawable(drawable);
LogUtils.i("src图片:" + resName + "view的id:" + view.getId());
}
}
};
private String attrType;//资源类型
//提供外部调用方法,获得当前资源的类型
public String getAttrType() {
return attrType;
}
//构造方法
SkinAttrType(String attrType) {
this.attrType = attrType;
}
//抽象方法传入需要改变的view和资源名
protected abstract void apply(View view, String resName);
//获得资源管理器
public ResourceManager getResourceManager() {
ResourceManager resourceManager = SkinManagerOutdated.getInstance().getResourceManager();
return resourceManager;
}
}
2,SkinAttr
/**
* 2,该类保存了一个view下的资源名和资源类型,对应关系
*/
public class SkinAttr {
private String resName;//资源名
private SkinAttrType attrType;//资源类型
//构造方法中传入资源类型实例和资源名
public SkinAttr(SkinAttrType attrType, String resName) {
this.resName = resName;
this.attrType = attrType;
}
//执行换肤,对于传进来的view换肤成相对应传进来的资源名
protected void apply(View view) {
//枚举中有具体的换肤操作
attrType.apply(view, resName);
}
}
3,SkinView
/**
* 1,该类提供了一个activity所有view的资源替换方法
*/
public class SkinView {
private View view;//需要改变皮肤的view
private List<SkinAttr> attrs;//这个view下所有的资源实例
//传入view和所有资源
public SkinView(View view, List<SkinAttr> skinAttrs) {
this.view = view;
this.attrs = skinAttrs;
}
//执行换肤,将该view下的所有资源都进行换肤
public void apply() {
if (view == null) return;
for (SkinAttr attr : attrs) {
//skinattr中有针对单个资源的换肤
attr.apply(view);
}
}
}
4,SkinAttrSupprot
注意:这个SkinAttrSupprot类我将两种换肤方式的方法都写在其中了,以便于更好理解。真正实际用到的,只有通过资源截取或者tag样式获取。两者二选一。因为这两个方法属于不同的换肤方式。
/**
* 4,皮肤属性兼容类
* 因为每个activity都会使用到该方法所以直接将它设置成静态的,以免多次实例化和释放造成内存压力过大
*/
public class SkinAttrSupport {
/**
* 通过资源截取
*
* @param attrs
* @param context
* @return
*/
public static List<SkinAttr> getSkinAttrs(AttributeSet attrs, Context context) {
List<SkinAttr> skinAttrs = new ArrayList<>();
SkinAttr skinAttr;
//在这里循环遍历出这个activity中所包含的所有资源
for (int i = ; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);//属性名
String attrValue = attrs.getAttributeValue(i);//属性值
//通过属性名获得到属性类型,这个属性类型枚举中已经包含了被查找到的枚举属性:例如包含了backage
SkinAttrType attrType = getSupprotAttrType(attrName);
if (attrType == null) continue;
if (attrValue.startsWith("@")) {
//通过属性值获得属性id,以@开头验证
int id = Integer.parseInt(attrValue.substring());
//通过属性id获得这个属性的名称,例如R文件id是0x7f050000;可以通过这个id得到它的命名:例如:skin_index_bg
String entryName = context.getResources().getResourceEntryName(id);
if (entryName.startsWith(SkinContacts.ATTR_PREFIX)) {
//我们验证skin以后的资源名称加上带有资源类型的枚举,例如:color类型的枚举
skinAttr = new SkinAttr(attrType, entryName);
skinAttrs.add(skinAttr);
//LogUtils.i("添加资源SkinAttr:" + entryName);
}
}
}
return skinAttrs;
}
/**
* tag的样式,我们可以根据tag来截取
*
* @param tagStr 样式:skin_left_menu_icon:src|skin_color_red:textColor
* @return
*/
public static List<SkinAttr> getSkinTags(String tagStr) {
List<SkinAttr> skinAttrs = new ArrayList<>();
if (TextUtils.isEmpty(tagStr)) return skinAttrs;
//将string截取成一|分隔符的字符串数组
String[] items = tagStr.split("\\|");
for (String item : items) {
//如果不包含标识换肤的前缀,则表示它不是需要换肤的
if (!tagStr.startsWith(SkinContacts.ATTR_PREFIX))
return skinAttrs;
//截取出资源名和资源类型
String[] resItems = item.split(":");
String resName = resItems[];
String resType = resItems[];
//通过属性名获得到属性类型,这个属性类型枚举中已经包含了被查找到的枚举属性:例如包含了backage
SkinAttrType attrType = getSupprotAttrType(resType);
if (attrType == null) continue;
SkinAttr attr = new SkinAttr(attrType, resName);
skinAttrs.add(attr);
}
return skinAttrs;
}
//传入资源名得到资源类型实例
private static SkinAttrType getSupprotAttrType(String attrName) {
for (SkinAttrType attrType : SkinAttrType.values()) {
//如果枚举中有一个资源类型匹配了,则返回这个枚举所带有的资源类型实例
if (attrType.getAttrType().equals(attrName))
return attrType;
}
return null;
}
}
6,重点方法3:SkinManager
这个类决定了你如何选择皮肤,是调用换肤的核心。以上代码是换肤的核心。
我们先来看看tag式换肤的SkinManager。
先将SkinManager设置成单例,然后定义属性
public class SkinManager {
private Context mContext;//这里的上下文其实是appliaction的上下文,因为换肤全局有效
private Resources mResources;//换肤的资源对象
private ResourceManager mResourceManager;//换肤的资源管理器
private SkinSharedPreferencesUtils mPrefUtils;//用户偏好设置
private boolean usePlugin;//是否可以换肤标识
private String mSuffix = "";//应用内换肤的标识后缀
private String mCurPluginPath;//插件换肤的插件文件路径
private String mCurPluginPkg;//插件的内置包名
private Map<ISkinListener, List<SkinView>> mSkinViewMaps = new HashMap<>();//键值对指向哪一个view对应的它之下所有的需要更换的皮肤资源
private List<ISkinListener> mActivities = new ArrayList<>();//保存整个app被标记的要被换肤的对象
private SkinManager() {
}
private static class SingletonHolder {
static SkinManager sInstance = new SkinManager();
}
public static SkinManager getInstance() {
return SingletonHolder.sInstance;
}
public ResourceManager getResourceManager() {
if (!usePlugin) {
mResourceManager = new ResourceManager(mContext.getResources(), mContext.getPackageName(), mSuffix);
}
return mResourceManager;
}
init是在appliaction中调用的方法,为app第一次进来的时候,验证用户是否使用了插件皮肤,如果使用了则加载皮肤。
//用户保存的皮肤,初始化的时候就可以直接加载进去
public void init(Context context) {
mContext = context.getApplicationContext();
mPrefUtils = new SkinSharedPreferencesUtils(context);
String skinPluginPath = mPrefUtils.getPluginPath();
String skinPluginPkg = mPrefUtils.getPluginPackage();
mSuffix = mPrefUtils.getAttrSuffix();
if (TextUtils.isEmpty(skinPluginPath))//如果没有插件路径则返回
return;
File file = new File(skinPluginPath);
if (!file.exists()) return;//如果插件文件不存在,则返回
try {
loadPlugin(skinPluginPath, skinPluginPkg, mSuffix);
mCurPluginPath = skinPluginPath;
mCurPluginPackage = skinPluginPkg;
} catch (Exception e) {
mPrefUtils.clear();
e.printStackTrace();
}
}
loadPlugin 加载插件资源,这个方法没得说。要想加载sd卡中的皮肤资源就这个方法。
//加载插件资源,建议在非ui线程工作;该方法是通用的,必须的方法
private void loadPlugin(String skinPath, String skinPkgName, String suffix) throws Exception {
//获得系统资源管理类实例
AssetManager assetManager = AssetManager.class.newInstance();
//获得系统资源管理类下的添加资源路径方法
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//调用该方法,并且传入皮肤所在的路径
addAssetPath.invoke(assetManager, skinPath);
//获得resources对象,现在获得的还是当前app内的
Resources superRes = mContext.getResources();
//获得一个资源管理,显示指标,获取配置
//创建一个新的资源对象的一组现有的资产管理公司的资产。
mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
mResourceManager = new ResourceManager(mResources, skinPkgName, suffix);
isUsePlugin = true;
}
clearPluginInfo清理插件信息,即回复默认皮肤
//清理插件信息
private void clearPluginInfo() {
mCurPluginPath = null;
mCurPluginPackage = null;
isUsePlugin = false;
mSuffix = null;
mPrefUtils.clear();
}
//清除所有插件
public void removeAnySkin() {
clearPluginInfo();
notifyChangedListeners();//通知所有的view,修改皮肤
}
//更新插件信息
private void updatePluginInfo(String skinPluginPath, String pkgName, String suffix) {
mPrefUtils.setPluginPath(skinPluginPath);
mPrefUtils.setPluginPackage(pkgName);
mPrefUtils.setAttrSuffix(suffix);
mCurPluginPackage = pkgName;
mCurPluginPath = skinPluginPath;
mSuffix = suffix;
}
getSkinViews/putSkinViews;存取放在map中的对应view资源集合
//传入一个继承了换肤接口的activity,并且返回这个activity中所有的可以换肤的SkinView对象,获得一个需要换肤的view
//该map里面存入的是整个app被记录的需要换肤的view,传入接口,返回对应的实现了接口的view的所有资源对象
public List<SkinView> getSkinViews(ISkinListener listener) {
return mSkinViewMaps.get(listener);
}
//将每个皮肤接口的所有view的资源都保存在map中,然后方便存取对iang
public void putSkinViews(ISkinListener listener, List<SkinView> skinViews) {
mSkinViewMaps.put(listener, skinViews);
}
注册/反注册;传入接口信息,并保存,用于全局换肤;传入view,用于更换资源。
/**
* 注册换肤
*/
public void register(final ISkinListener listener, final View view) {
mActivities.add(listener);
//这里将注入皮肤的操作添加到队列中执行,要不然会获取不到
view.post(new Runnable() {
@Override
public void run() {
injectSkin(listener, view);
}
});
}
//反注册
public void unregister(ISkinListener listener) {
mActivities.remove(listener);
mSkinViewMaps.remove(listener);
}
injectSkin注入皮肤;该方法为初始化的时候所调用的
//注入皮肤,在activity初始化的时候进行
public void injectSkin(ISkinListener iSkinListener, View view) {
List<SkinView> skinViews = new ArrayList<>();
addSkinViews(view, skinViews);
putSkinViews(iSkinListener, skinViews);
for (SkinView skinView : skinViews) {
skinView.apply();
}
}
递归传入view的资源到skinview集合中
//递归添加view的资源到skinview结合中
public void addSkinViews(View view, List<SkinView> skinViews) {
SkinView skinView = getSkinView(view);
if (skinView != null) skinViews.add(skinView);
if (view instanceof ViewGroup) {
ViewGroup container = (ViewGroup) view;
int n = container.getChildCount();
for (int i = ; i < n; i++) {
View child = container.getChildAt(i);
addSkinViews(child, skinViews);
}
}
}
//获得该view下所有的资源属性
public SkinView getSkinView(View view) {
//获得每个view所附带的标签,这是关键所在,后面会讲到如何给view赋值tag和拿到view的tag
Object tag = view.getTag(SkinContacts.SKIN_TAG);
if (tag == null)
tag = view.getTag();
if (tag == null) return null;
if (!(tag instanceof String)) return null;
List<SkinAttr> skinAttrs = SkinAttrSupport.getSkinTags(tag.toString());
if (!skinAttrs.isEmpty()) {
changeViewTag(view);
return new SkinView(view, skinAttrs);
}
return null;
}
//修改view所持有的tag标签
private void changeViewTag(View view) {
Object tag = view.getTag(SkinContacts.SKIN_TAG);
if (tag == null) {
tag = view.getTag();
view.setTag(SkinContacts.SKIN_TAG, tag);
//view.setTag(null);//我不知道为什么鸿洋大神最后要在添加了tag后又置空,我把它注释了,不影响运行。
}
}
改变皮肤的方法changeskin
/**
* 应用内换肤,传入资源区别的后缀
*/
public void changeSkin(String suffix) {
clearPluginInfo();
mSuffix = suffix;
mPrefUtils.setAttrSuffix(suffix);
notifyChangedListeners();
}
/**
* 更换插件皮肤,默认为""
*
* @param skinPluginPath
* @param skinPluginPkg
* @param callback
*/
public void changeSkin(final String skinPluginPath, final String skinPluginPkg, ISkinCallback callback) {
changeSkin(skinPluginPath, skinPluginPkg, "", callback);
}
/**
* 根据suffix选择插件内某套皮肤
*
* @param skinPluginPath
* @param skinPluginPkg
* @param suffix
* @param callback
*/
public void changeSkin(final String skinPluginPath, final String skinPluginPkg, final String suffix, final ISkinCallback callback) {
if (callback == null) return;
callback.onStart();
try {
checkPluginParamsThrow(skinPluginPath, skinPluginPkg);
} catch (IllegalArgumentException e) {
callback.onError(new RuntimeException("checkPlugin occur error"));
return;
}
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... params) {
try {
loadPlugin(skinPluginPath, skinPluginPkg, suffix);
return ;
} catch (Exception e) {
e.printStackTrace();
return ;
}
}
@Override
protected void onPostExecute(Integer res) {
if (res == ) {
callback.onError(new RuntimeException("loadPlugin occur error"));
return;
}
try {
updatePluginInfo(skinPluginPath, skinPluginPkg, suffix);
notifyChangedListeners();
callback.onComplete();
} catch (Exception e) {
e.printStackTrace();
callback.onError(e);
}
}
}.execute();
}
notifyChangedListeners发出换肤的通知
//通知所有注册过的view进行换肤
public void notifyChangedListeners() {
for (ISkinListener listener : mActivities) {
apply(listener);
}
}
ok。SkinManager的方法就这些。它主要是实例化了资源管理器,得到资源实例,并且去调用skin文件夹下的更换皮肤的流程。
接下来我们写一个抽象activity,实现ISkinListener接口,然后供外部去调用。
public abstract class BaseSkinActivity extends AppCompatActivity implements ISkinListener {
//抽象方法传入当前activity 的资源id
protected abstract int setContentView();
protected abstract void initview();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(setContentView());
//获得当前activity的最外层view
ViewGroup content = (ViewGroup) findViewById(android.R.id.content);
//在activity创建的时候添加换肤的监听
//鸿洋大神写的是直接传入一个activity实例,我这里稍微的修改了一下,并不算是优化,只能说方便约束和统一管理
//我这里不再传入activity对象,而是传入实现了这个接口的activity对象和它的view
//目的就是为了当我们继承这个抽象类的时候,其他地方如果用到了ViewGroup的话,我们也不用再次获取了
SkinManagerOutdated.getInstance().register(this, content);
initview();
}
/**
* 当程序要求app修改皮肤的时候就会调用这个方法
* 该方法里面写实时的修改皮肤的方法
*/
@Override
public void onSkinChanged() {
//因为在onCreateView方法中已经将参数声明了,所以这里直接调用
//如果皮肤管理器向所有注册过的view发出通知,改变皮肤;则就会出发该方法
//而该方法中写的就是单个的修改view的皮肤
SkinManagerOutdated.getInstance().apply(this);
}
/**
* 当activity被finish的时候我们从换肤集合中移除这个activity,下一次换肤就不会对该activity产生作用了
*/
@Override
protected void onDestroy() {
super.onDestroy();
//反注册,从皮肤注册集合中移除,以免造成内存泄漏
SkinManagerOutdated.getInstance().unregister(this);
}
}
这样的话,只要我们的activity继承了这个baseskinactivity的话就可以实现换肤了,并且是无缝换肤和支持插件或者app内部换肤。
这里有几点需要注意:
1:插件换肤要声明读写sd卡权限,要不然获取不到插件。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
2:每个需要换肤的资源需要在xml中指明tag。例如:
<TextView
android:id="@+id/id_tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/id_iv_icon"
android:layout_toRightOf="@id/id_iv_icon"
android:text="鸿洋"
android:tag="skin:skin_item_text_color:textColor"
android:textColor="@color/skin_item_text_color" />
其中tag就是用于换肤的标识;有严格的格式规定。
以skin:开头 +资源名+:+资源类型
skin:skin:skin_item_text_color:textColor
这是鸿洋大神写的关于tag方式的换肤。
因为篇幅太长,我会在第二篇博文中继续解析。这一片主要分析了tag方式的换肤。下一篇就分析一下侵入式换肤和tag式的区别以及不好之处。