1. 换肤效果
先看效果,此demo比较简陋,主要实现了颜色、图片、自定义View、字体样式、状态栏换肤等模块
2. 换肤思路
先说插件化换肤主要思路:一般应用换肤主要都是更换颜色、图片等资源,所以我们首先需要拿到要换肤的资源ID,然后在皮肤包中设置该属性值为想改变的颜色或图片资源,原应用内下载皮肤包,通过代码即可实现换肤。
例如:一个TextView的颜色需要改变,那么我们需要得到该TextView的textColor 属性对应的颜色ID值,假设为android:textColor="@color/colorAccent" ,原应用中colorAccent的值为<color name="colorAccent">#ffffff</color> 在皮肤包中,我们将colorAccent的值修改为任意想改变的值<color name="colorAccent">#569847</color> ,打包成APK,通过代码即可实现TextView的颜色的改变。
一个成熟的项目一般都是批量化换肤,我们来一步步实现。
我们知道layout资源加载都是在setContentView中,在资源文件加载之前替换资源实现换肤,阅读源码,重点在框起来的这行代码 LayoutInflater也就是布局填充器,负责将xml布局加载到页面上,继续深入,进入inflate方法,最终定位到我们的目标方法createViewFromTag 中,重点在框起来的部分,factory工厂 其中,Factory2继承自Factory,比Factory多了一个parent参数。区别在于:如果需要提供将创建视图的父级,则需要使用Factory2 。但如果应用的定位API级别为11+,则通常使用Factory2 ,否则,只需使用Factory
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
Factory2提供了一个接口onCreateView ,我们可以通过实现这个接口,介入到创建view的这个过程中去,记录所有的view,同时拿到view所有需要换肤的属性,记录下来,然后根据属性替换,以上就是我们换肤的大致思路。
3. 代码实现
创建一个library,专门用于处理换肤的SDK,先看项目目录如下
其实SkinManager是换肤库的管理类,单利模式实现,在项目的MyApplication 中初始化,传递application
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SkinManager.init(this);
}
}
通过自定义SkinActivityLifeCycle继承自Application.ActivityLifecycleCallbacks,实现对应用中所有Activity的生命周期的管理 SkinActivityLifeCycle主要在onActivityCreated方法中创建自定义工厂SkinFactory,先只看框起来的部分 SkinFactory继承自LayoutInflater.Factory2,在onCreateView方法中遍历所有的View,得到可以换肤的View的集合 createViewFromTag 和createView 实现如下,代码注释也写得比较清楚,主要思路是用全类名,通过反射获取其class得到该View的构造器,存储到自定义的构造器集合里,便于下次再遇到不用再通过反射创建,然后返回该View的构造器(这段其实和源码一样,不想写可以直接复制源码,这里写出来主要是为了理解思路)
private View createViewFromTag(String name, Context context, AttributeSet attributeSet) {
if (name.contains(".")) {
return null;
}
View view = null;
for (String aMClassPrefixlist : mClassPrefixlist) {
view = createView(aMClassPrefixlist + name, context, attributeSet);
if (view != null) {
break;
}
}
return view;
}
private View createView(String name, Context context, AttributeSet attributeSet) {
Constructor<? extends View> constructor = constructorHashMap.get(name);
if (constructor == null) {
try {
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = aClass.getConstructor(mConstructorSignature);
constructorHashMap.put(name, constructor);
return constructor.newInstance(context, attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
} else {
try {
return constructor.newInstance(context, attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
继续回到我们实现的onCreateView方法中,既然得到了View,我们就可以通过SkinAttr的load方法,遍历该View种的属性集合,筛选需要换肤的属性,存储起来。 SkinAttr实现如下,主要思路是自定义了一个mAttributes集合,集合中包含了需要换肤的属性合集,在load方法中,通过遍历传进来View的AttributeSet集合,与我们需要换肤的集合比较,如果有就获取该属性对应的属性值,判断属性值开头是# ,? ,@ ,等。 如果是# 说明是固定颜色值,可以修改也可以不改,具体看项目需求,此处未修改。 ? 代表是系统属性,需要特殊处理,其他就是类似@ 开头的值,依次得到resId后,创建自定义的SkinPain,包括属性名和属性id,添加到SkinPain的集合中。 遍历完成后,判断SkinPain集合是否为空(代码可以看到,这里的条件还有view instanceof TextView || view instanceof SkinViewSupport ,这两个是更换字体和自定义view的判断条件,后面再讲),不为空则创建SkinView(属性包含View和SkinPain),并将其添加到SkinView的集合中,至此我们得到了所有需要换肤的View的集合。
public class SkinAttr {
private Typeface typeface;
private String tag = SkinAttr.class.getSimpleName();
private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
mAttributes.add("skinTypeface");
}
private List<SkinView> skinViews = new ArrayList<>();
public SkinAttr(Typeface typeface) {
this.typeface = typeface;
}
public void load(View view, AttributeSet attributeSet) {
List<SkinPain> skinPains = new ArrayList<>();
for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
String attributeName = attributeSet.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
String attributeValue = attributeSet.getAttributeValue(i);
Log.e(tag, "attributeValue == " + attributeValue);
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
if (attributeValue.startsWith("?")) {
int attrId = Integer.parseInt(attributeValue.substring(1));
Log.e(tag, "attrId == " + attrId);
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
Log.e(tag, "resId? == " + resId);
} else {
resId = Integer.parseInt(attributeValue.substring(1));
Log.e(tag, "resId@ == " + resId);
}
if (resId != 0) {
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
}
}
}
if (!skinPains.isEmpty() || view instanceof TextView || view instanceof SkinViewSupport) {
SkinView skinView = new SkinView(view, skinPains);
skinView.applySkin(typeface);
skinViews.add(skinView);
}
}
public void setTypeface(Typeface typeface) {
this.typeface = typeface;
}
private class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
public void applySkin(Typeface typeface) {
applyTypeFace(typeface);
applySkinSupport();
for (SkinPain skinPain : skinPains) {
Drawable left = null, top = null, right = null, bottom = null;
Log.e(tag, "skinPain == " + skinPain.attributeName);
switch (skinPain.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer) background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPain.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "skinTypeface":
applyTypeFace(SkinResources.getInstance().getTypeface(skinPain.resId));
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
}
}
}
private void applyTypeFace(Typeface typeface) {
if (view instanceof TextView) {
Log.e(tag, "typeface == " + typeface.getStyle());
((TextView) view).setTypeface(typeface);
}
}
private void applySkinSupport() {
if (view instanceof SkinViewSupport) {
Log.e(tag,"applySkinSupport === ");
((SkinViewSupport) view).applySkinView();
}
}
}
private class SkinPain {
String attributeName;
int resId;
public SkinPain(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
public void applySkin() {
for (SkinView skinView : skinViews) {
skinView.applySkin(typeface);
}
}
}
通过实现Factory的onCreateView方法,我们在xml加载之前得到了所有需要换肤的View集合,那么怎么实现换肤呢?
SkinManager中的loadSkin 方法,判断传过来的皮肤包地址是否为空,空就加载默认皮肤,否则加载给定路径的皮肤,这里我换肤路径写死了,给模拟器对应的路径传了自定义皮肤包的apk进去,一般线上是先下载,然后换肤。
注意:这里因为读取了sd卡,所以需要添加读写权限,否则会空指针异常
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
通过反射得到PackageInfo,重点在SkinResources.getInstance().applySkin(skinResource, packageName); 这行代码,初始化皮肤包资源和包名。然后通过观察者模式通知更新皮肤。
public void loadSkin(String skinPath) {
Log.e(tag,"skinPath == " + skinPath);
if (TextUtils.isEmpty(skinPath)) {
SkinResources.getInstance().reset();
SkinPreference.getInstance().setSkin("");
} else {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPath);
Resources appResources = application.getResources();
Resources skinResource = new Resources(assetManager, appResources.getDisplayMetrics(), appResources.getConfiguration());
PackageInfo packageArchiveInfo = application.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
Log.e(tag,"packageArchiveInfo == "+packageArchiveInfo);
if(packageArchiveInfo!=null) {
String packageName = packageArchiveInfo.packageName;
Log.e(tag,"skinResource == " + skinResource);
Log.e(tag,"packageName == " + packageName);
SkinResources.getInstance().applySkin(skinResource, packageName);
SkinPreference.getInstance().setSkin(skinPath);
} else {
Toast.makeText(application,"包信息为null",Toast.LENGTH_SHORT).show();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
setChanged();
notifyObservers(null);
}
SkinFactory的update 方法中收到更新消息,skinAttr.applySkin()方法实现换肤
@Override
public void update(Observable observable, Object o) {
SkinThemeUtils.updateStatusBarColor(activity);
Typeface typeface = SkinThemeUtils.updateTypeFace(activity);
skinAttr.setTypeface(typeface);
skinAttr.applySkin();
}
SkinAttr的applySkin 方法,遍历之前得到的需要换肤的skinViews集合,通过applySkin方法,遍历属性合集
public void applySkin() {
for (SkinView skinView : skinViews) {
skinView.applySkin(typeface);
}
}
SkinAttr的SkinView 内部类实现
private class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
public void applySkin(Typeface typeface) {
applyTypeFace(typeface);
applySkinSupport();
for (SkinPain skinPain : skinPains) {
Drawable left = null, top = null, right = null, bottom = null;
Log.e(tag, "skinPain == " + skinPain.attributeName);
switch (skinPain.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer) background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPain.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "skinTypeface":
applyTypeFace(SkinResources.getInstance().getTypeface(skinPain.resId));
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
}
}
}
private void applyTypeFace(Typeface typeface) {
if (view instanceof TextView) {
Log.e(tag, "typeface == " + typeface.getStyle());
((TextView) view).setTypeface(typeface);
}
}
private void applySkinSupport() {
if (view instanceof SkinViewSupport) {
Log.e(tag,"applySkinSupport === ");
((SkinViewSupport) view).applySkinView();
}
}
}
SkinResources是实现换肤的类,大致思路是,如果是默认皮肤,就返回原始包中对应的id值,如果需要换肤就返回mSkinResources也就是通过皮肤包得到的id,然后在SkinAttr的SkinView的applySkin中设置给对应的View即可实现换肤。
public class SkinResources {
private static SkinResources instance;
private Resources mSkinResources;
private String mSkinPkgName;
private boolean isDefaultSkin = true;
private Resources mAppResources;
private String tag = SkinResources.class.getSimpleName();
private SkinResources(Context context) {
mAppResources = context.getResources();
}
public static void init(Context context) {
if (instance == null) {
synchronized (SkinResources.class) {
if (instance == null) {
instance = new SkinResources(context);
}
}
}
}
public static SkinResources getInstance() {
if (instance == null) {
throw new IllegalStateException("SkinResources 未初始化");
}
return instance;
}
public void reset() {
mSkinResources = null;
mSkinPkgName = "";
isDefaultSkin = true;
}
public void applySkin(Resources resources, String pkgName) {
mSkinResources = resources;
mSkinPkgName = pkgName;
isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
}
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
String resName = mAppResources.getResourceEntryName(resId);
Log.e(tag, " resName == " + resName);
String resType = mAppResources.getResourceTypeName(resId);
Log.e(tag, " resType == " + resType);
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
Log.e(tag, " skinId == " + skinId);
return skinId;
}
public int getColor(int resId) {
if (isDefaultSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
public Object getBackground(int resId) {
String resourceTypeName = mAppResources.getResourceTypeName(resId);
if (resourceTypeName.equals("color")) {
return getColor(resId);
} else {
return getDrawable(resId);
}
}
public String getString(int resId) {
try {
if (isDefaultSkin) {
Log.e("SkinResources", "mAppResources.getString(resId) == " + mAppResources.getString(resId));
return mAppResources.getString(resId);
}
int skinId = getIdentifier(resId);
Log.e("SkinResources", "skinId == " + skinId);
if (skinId == 0) {
Log.e("SkinResources", "mAppResources.getString(resId) == " + mAppResources.getString(resId));
return mAppResources.getString(resId);
}
Log.e("SkinResources", "mSkinResources.getString(resId) == " + mSkinResources.getString(skinId));
return mSkinResources.getString(skinId);
} catch (Resources.NotFoundException e) {
}
return null;
}
public Typeface getTypeface(int resId) {
String skinTypefacePath = getString(resId);
Log.e("tag", "typefacepath == " + skinTypefacePath);
if (TextUtils.isEmpty(skinTypefacePath)) {
return Typeface.DEFAULT;
}
try {
Typeface typeface;
if (isDefaultSkin) {
typeface = Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
return typeface;
}
typeface = Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
return typeface;
} catch (RuntimeException e) {
}
return Typeface.DEFAULT;
}
}
最后在项目中添加点击事件实现换肤即可
写得有点啰嗦,但是大致思路和实现方法基本就是这些,不是很难,就是具体项目中皮肤包的实现比较繁琐,需要细心细心再细心。
4. 生成皮肤包
说到这里,说一下怎么实现皮肤包吧,新建一个项目,不需要activity这些,只保留value下的资源,设置需要换肤的属性值,color,图片等,然后Build——Build Bundle(s)/APK(s)——Build APK(s),打包成一个apk就行了
5. 代码下载地址
https://download.csdn.net/download/mr_hmgo/21351930
|