所谓换肤技术,就是用户可以根据自己的喜好,选择自己喜欢的并且APP提供的颜色,背景图片,作为整个app的主题背景颜色,或者字体颜色等等...满足用户的需求。
APP换肤 主要分为2种:
1.内置换肤:就是将换肤所需要的图片资源,或者颜色资源,在打包的时候,打包到APK当中
2.皮肤包换肤:就是 当用户选择某一款皮肤的时候,动态从服务器下载打包好的皮肤包,然后解析出相应的图片资源,并替换出原有的图片,颜色,字体等等资源
优缺点
1.内置换肤:将所有皮肤包的资源打包到APK内部当中的话,如果皮肤包资源多,会增大APK包 的体积,所以预制在APK内部的的 皮肤包资源不能太多。
2.皮肤包换肤:可以动态改变下载或者删除皮肤包,不占用APK的大小,缺点就是第一次下载皮肤包的时候 需要下载,而且解析的时候 需要浪费时间,消耗流量
内置换肤
所谓的内置换肤,一般就是在app内部,内置几套皮肤包供用户选择,由于APK的体积有限,所以APK内部提供给用户选择的皮肤就不可能多,用户选择之后,将当前的Activity或者Fragment中各个需要替换的背景替换掉。对于内置换肤,由于内置皮肤包有限,一般开发人员会根据用户的选择,在代码中已经写好自己的switch case语句或者if else if语句来判断某个用户选择的那款内置的皮肤,根据选择修改即可。这里只做介绍不讲具体实现,因为很简单
例如 app中提供白天模式 和 夜间模式,这其实也是换肤的一种简单的实现方式。我们完全可以在app中设置某个全局变量,用户选择某个选项之后,去将背景变为白色或者黑色。
外置皮肤包换肤
外置皮肤包换肤方式主要有两种实现方式,这里主要细讲第二种实现方式,因为我觉得第二种优于第一种。
实现方式一
外置皮肤包换肤,就是当用户选择某一种皮肤之后,可以从服务器下载一个皮肤包,当皮肤包下载完成就立马更新当前Activity的背景或者相关图片,如果跳转到别的activity之后 也会展示最新皮肤包下的相关背景或者图片。
主要思想就是利用SDK提供给我们的Application.ActivityLifecycleCallbacks接口可以检测各个Activity的生命周期:
public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity activity, Bundle savedInstanceState);
void onActivityStarted(Activity activity);
void onActivityResumed(Activity activity);
void onActivityPaused(Activity activity);
void onActivityStopped(Activity activity);
void onActivitySaveInstanceState(Activity activity, Bundle outState);
void onActivityDestroyed(Activity activity);
}
然后利用 自定义LayoutInflater.Factory2 的实现类,去修改拦截系统onCreateView方法,将当前Activity的所有控件记录下来,到需要换肤的时候去遍历所有控件的对应属性,并且去修改对应的属性值,达到换肤的目的。
public void skinChange() {
for (AttributeNameAndValue attributeNameAndValue : attributeNameAndValues) {
switch (attributeNameAndValue.attrName) {
case "background":
Object background = SkinResources.getInstance().getBackground(attributeNameAndValue.attrValueInt);
if (background instanceof Integer) {
mView.setBackgroundColor((Integer) background);
} else {
// mView.setBackground((Drawable) background);
// 用兼容包的
ViewCompat.setBackground(mView, (Drawable) background);
}
break;
case "textColor":
TextView textView = (TextView) mView;
textView.setTextColor(SkinResources.getInstance().getColorStateList(attributeNameAndValue.attrValueInt));
break;
case "src":
Object src = SkinResources.getInstance().getBackground(attributeNameAndValue.attrValueInt);
if (src instanceof Integer) {
((ImageView) mView).setImageDrawable(new ColorDrawable((Integer) src));
} else {
((ImageView) mView).setImageDrawable((Drawable) src);
}
case "tint":
SkinResources skinRes = SkinResources.getInstance();
Log.d(TAG, "tint>>>>>>>>>>>" + attributeNameAndValue.attrValueInt + " -- " + skinRes.getDefaultSkin());
break;
}
}
}
上面这种方式就是每次一个新的Activity打开的时候,需要去遍历,记录当前Activity的所有控件,并且换肤的时候,有需要去遍历所有控件的对应的background,src,textColor....等属性,这里至少需要遍历2次,所以需要消耗性能,并且需要将Activity的内存都缓存起来,必然会增加内存。这种方式实现的Demo地址为 传送门 。
注意:这里,如果对于我们自定义的的View,如果我们不是系统的这些background,src属性值,这也那个这种方式就会难以实现,或者说对于这个switch ... case .... 语句会由于开发人员越来越多,或者自定义控件越来越多,这个方法很难管理或者臃肿,不太友好。
实现方式二
思路:换肤所需要的的目标皮肤包,其实就是一个Apk包。我们只需要将皮肤包中的用的图片的名称,颜色名称...与APP中所需要替换的图片资源,颜色资源的name相同就OK。正在运行的APP,通过Resources和AssetsManager可以得到当前控件中所使用到的颜色资源的name,背景图片的name,也可以得到这些资源对应的id ,例如R.drawable.***,R.color.***,,得到这些参数值之后,就可以通过插件的Resources和AssetsManager利用主app中获取到的name,id,将插件中的相同图片资源或者颜色资源取出来,最后更新目标控件的背景,颜色或者字体,就可以达到换肤的目的了。
先看换肤思想的由来,代码中涉及到的源码都是Android9.0(SDK28),所有思想都来源于源代码的阅读:
Activity.setContentView(layoutResID)-->getDelegate().setContentView(layoutResID)-->AppCompatDelegateImpl.setContentView
-->LayoutInflater.from(this.mContext).inflate(resId, contentParent)
-->PhoneLayoutInflater.inflate-->createViewFromTag(parent, name, context, attrs)-->
-->view = mFactory2.onCreateView(parent, name, context, attrs) 返回layoutResID中每一个view
-->思考如何mFactory2 是如何赋值的?
-->AppCompatActivity 的onCreate方法-->AppCompatDelegateImpl.installViewFactory();
-->LayoutInflaterCompat.setFactory2(layoutInflater, this); this就是AppCompatDelegateImpl 它实现了Factory2
-->那么上面的mFactory2 = AppCompatDelegateImpl
-->mFactory2.onCreateView = AppCompatDelegateImpl.onCreateView
-->AppCompatViewInflater.createView(...)
在AppCompatActivity中的onCreate方法中执行下面这个方法 赋值mFactory2
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
}
}
因此我们可以定义自己的Factory2 在onCreate方法之前调用,那么所有的view的创建都调用我们自己实现的factory2
而Activity实现了Factory2接口,那么我们只需要在onCreate之前调用
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);
再重写onCreateView方法,就可以拦截View的创建
上面的代码分析中我们需要定义自己的LayoutInflater来做和系统LayoutInflater加载xml,创建出不同的View。这里我们将自定义的LayoutInfalter 继承系统的 AppCompatViewInflater 这样可以不影响系统功能的情况下,实现自己的逻辑:
package com.android.skin.library.core;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatViewInflater;
import android.util.AttributeSet;
import android.view.View;
import com.android.skin.library.views.SkinnableButton;
import com.android.skin.library.views.SkinnableImageView;
import com.android.skin.library.views.SkinnableLinearLayout;
import com.android.skin.library.views.SkinnableRelativeLayout;
import com.android.skin.library.views.SkinnableTextView;
/**
* 自定义控件加载器(可以考虑该类不被继承)
*/
public final class CustomAppCompatViewInflater extends AppCompatViewInflater {
private String name; // 控件名
private Context context; // 上下文
private AttributeSet attrs; // 某控件对应所有属性
public CustomAppCompatViewInflater(@NonNull Context context) {
this.context = context;
}
public void setName(String name) {
this.name = name;
}
public void setAttrs(AttributeSet attrs) {
this.attrs = attrs;
}
/**
* @return 自动匹配控件名,并初始化控件对象
*/
public View autoMatch() {
View view = null;
switch (name) {
case "LinearLayout":
// view = super.createTextView(context, attrs); // 源码写法
view = new SkinnableLinearLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "RelativeLayout":
view = new SkinnableRelativeLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "TextView":
view = new SkinnableTextView(context, attrs);
this.verifyNotNull(view, name);
break;
case "ImageView":
view = new SkinnableImageView(context, attrs);
this.verifyNotNull(view, name);
break;
case "Button":
view = new SkinnableButton(context, attrs);
this.verifyNotNull(view, name);
break;
}
return view;
}
/**
* 校验控件不为空(源码方法,由于private修饰,只能复制过来了。为了代码健壮,可有可无)
*
* @param view 被校验控件,如:AppCompatTextView extends TextView(v7兼容包,兼容是重点!!!)
* @param name 控件名,如:"ImageView"
*/
private void verifyNotNull(View view, String name) {
if (view == null) {
throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
}
}
}
autoMatch中处理的方式和系统AppCompatViewInflater的 createView方法。
以其中一个自定义View SkinnableLinearLayout为例说明,如果想实现全部的activity或者控件换肤,我们就需要自定义对应的所有系统控件,这也是这种方法的弊端吧。
首先在attrs.xml文件中加入我们自定义的属性如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TextView控件属性 -->
<declare-styleable name="SkinnableTextView">
<attr name="android:background" />
<attr name="android:textColor" />
<attr name="custom_typeface"/>
</declare-styleable>
<!-- Button控件继承TextView,此处parent语法通过,但无效果,不像style.xml -->
<declare-styleable name="SkinnableButton">
<attr name="android:background" />
<attr name="android:textColor" />
<attr name="custom_typeface"/>
</declare-styleable>
<!-- ImageView控件特有属性 -->
<declare-styleable name="SkinnableImageView">
<attr name="android:src" />
</declare-styleable>
<!-- SkinnableLinearLayout控件 -->
<declare-styleable name="SkinnableLinearLayout">
<attr name="android:background" />
</declare-styleable>
<!-- SkinnableRelativeLayout控件 -->
<declare-styleable name="SkinnableRelativeLayout">
<attr name="android:background" />
</declare-styleable>
<!-- 后续可以自己拓展………………………………………………………………………… -->
</resources>
我们的思想就是在自定义LayoutInflator去加载xml文件创建出对应的控件的时候,去创建我们自定义的控件,自定义的控件完全继承系统控件(为了适配我们可以尽量继承Appcompact***的控件),只是实现了我们自定义的接口,这样我们就可以将关于background,src,textColor等属性解析出来,存放在自定义的实体类中保存,这样每个控件都保存着跟换肤相关的需要改变的 background,src,textColor 等属性,当换肤的时候,就直接替换这些属性的值就OK。具体的SkinnableLinearLayout的实现如下:
public class SkinnableLinearLayout extends LinearLayout implements ViewsMatch {
private AttrsBean attrsBean;
public SkinnableLinearLayout(Context context) {
this(context, null);
}
public SkinnableLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SkinnableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
attrsBean = new AttrsBean();
// 根据自定义属性,匹配控件属性的类型集合,如:background
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.SkinnableLinearLayout,
defStyleAttr, 0);
// 存储到临时JavaBean对象
attrsBean.saveViewResource(typedArray, R.styleable.SkinnableLinearLayout);
// 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
typedArray.recycle();
}
@Override
public void skinnableView() {
// 根据自定义属性,获取styleable中的background属性
int key = R.styleable.SkinnableLinearLayout[R.styleable.SkinnableLinearLayout_android_background];
// 根据styleable获取控件某属性的resourceId
int backgroundResourceId = attrsBean.getViewResource(key);
if(backgroundResourceId>0){
if(SkinManager.getInstance().getIsDefaultSkin()){
Drawable drawable = ContextCompat.getDrawable(getContext(),backgroundResourceId);
setBackground(drawable);
}else{
Object obj = SkinManager.getInstance().getBackground(backgroundResourceId);
if(obj instanceof Integer){ //说明是颜色
int color = (int)obj;
setBackgroundColor(color);
}else{
Drawable drawable = (Drawable)obj;
setBackground(drawable);
}
}
}
}
}
三个参数的构造方法中,使用attrBean.saveViewResource方法就是将上面自定义SkinnableLinearLayout的attr:background属性抽取出来存放在AttrBean中,然后在skinnableView中获取对应的属性,修改其属性值达到换肤的目的.AttrBean的结构如下:
/**
* 临时JavaBean对象,用于存储控件的key、value
* 如:key:android:textColor, value:@Color/xxx
* <p>
* 思考:动态加载的场景,键值对是否存储SharedPreferences呢?
*/
public class AttrsBean {
private SparseIntArray resourcesMap;
private static final int DEFAULT_VALUE = -1;
public AttrsBean() {
resourcesMap = new SparseIntArray();
}
/**
* 储控件的key、value
*
* @param typedArray 控件属性的类型集合,如:background / textColor
* @param styleable 自定义属性,参考value/attrs.xml
*/
public void saveViewResource(TypedArray typedArray, int[] styleable) {
for (int i = 0; i < typedArray.length(); i++) {
int key = styleable[i];
int resourceId = typedArray.getResourceId(i, DEFAULT_VALUE);
resourcesMap.put(key, resourceId);
}
}
/**
* 获取控件某属性的resourceId
*
* @param styleable 自定义属性,参考value/attrs.xml
* @return 某控件某属性的resourceId
*/
public int getViewResource(int styleable) {
return resourcesMap.get(styleable);
}
}
在resourcesMap中存放的是控件的属性和 控件属性值的资源id,例如上面的SkinnableLinearLayout中的AttrBean 中存放的key为
R.styleable.SkinnableLinearLayout[R.styleable.SkinnableLinearLayout_android_background]
而value为该background下的颜色值或者图片值,这个值必须在color.xml里面定义,或者图片放在drawable文件夹下面,这样系统就会在.R文件中生成对应的资源id。如R.color.xxx R.drawable.xxx。然后我们根据资源id,获取到资源的名称,去皮肤包中找到对应名称的图片,或者对应名称的颜色值。这样就可以达到换肤目的(必须在color.xlm文件中或者drawable文件加下,否则找不到对应的文件名或者颜色名字)
要实现换肤,并且拦截系统加载xml创建View的过程,刚刚说了,需要快系统一步将Factory2赋值,这样加载XML文件实例化View的时候,就会用我们自己的Factory2中的onCreateView方法,而我们的Activity本身就实现了Factory2接口,所以就近取材,将Activity的实例this赋值给mFactory2,重写Activity的onCreateView方法,我们就可以在Activity的onCreateView做文章,自定义的LayoutInflator来创建View.所以我们定义一个基础类SkinActivity来实现这一操作,后续需要换肤的所有类都继承自这个Activity。
/**
* 换肤Activity父类
*
* 用法:
* 1、继承此类
* 2、重写openChangeSkin()方法
*/
public class SkinActivity extends AppCompatActivity {
private CustomAppCompatViewInflater viewInflater;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (openChangeSkin()) {
if (viewInflater == null) {
viewInflater = new CustomAppCompatViewInflater(context);
}
viewInflater.setName(name);
viewInflater.setAttrs(attrs);
View view = viewInflater.autoMatch();
if(view !=null){
return view;
}
}
return super.onCreateView(parent, name, context, attrs);
}
/**
* @return 是否开启换肤,增加此开关是为了避免开发者误继承此父类,导致未知bug
*/
protected boolean openChangeSkin() {
return false;
}
protected void updateSkin(String skinPath, int themeColorId) {
SkinManager.getInstance().setSkinResource(skinPath);
if (Build.VERSION.SDK_INT >= 21) {
int themeColor = SkinManager.getInstance().getColor(themeColorId);
// 换状态栏
StatusBarUtils.forStatusBar(this,themeColor);
// 换标题栏
ActionBarUtils.forActionBar(this,themeColor);
// 换底部导航栏
NavigationUtils.forNavigation(this,themeColor);
}
View decorView = getWindow().getDecorView();
applyDayNightForView(decorView);
}
protected void defaultSkin(String skinPath, int themeColorId){
updateSkin(null,themeColorId);
}
/**
* 回调接口 给具体控件换肤操作
*/
protected void applyDayNightForView(View view) {
if (view instanceof ViewsMatch) {
ViewsMatch viewsMatch = (ViewsMatch) view;
viewsMatch.skinnableView();
}
if (view instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) view;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
applyDayNightForView(parent.getChildAt(i));
}
}
}
在super.onCreate之前就设置mFactory2的值,mFactory2 = this = SkinActivity 重写onCreateView方法,在onCreateView中处理的办法就是,我们已经自定义的View,我们的CustomLayoutInflator能够处理的,就交给CustomLayoutInflator去处理,不能够处理的就交给super.onCretateView方法去处理,这样不会导致某些系统控件我们没重写,而显示不出来。在SkinActivity中还定义了一个openChangeSkin方法,用来控制当前页面是否需要换肤,用户可以继承SkinActivity自己来控制是否换肤。
准备工作都做好了,那么就是怎么去加载解析皮肤包的资源了,上面的分析,我们目前拿到了,每个View的对应属性的值,例如background属性值R.color.xxx或者R.drawable,现在需要把这些R.xxx.xxx代表的属性值,代表的图片名称,颜色name找出来,然后去皮肤包中找对应名称的图片,对应颜色name的value值,然后赋值给当前View的background。这样就大功告成了。具体实现在下面的SkinManager中,相关注释也很清晰:
public class SkinManager {
private static final SkinManager ourInstance = new SkinManager();
public static SkinManager getInstance() {
return ourInstance;
}
private Application mContext;
private volatile boolean isDefaultSkin = true; //使用系统默认的皮肤
private Resources mAppResource;
private Resources mSkinResource;
private String skinPackageName; //皮肤包的包名
private Map<String, SkinCache> resourcesMap = new HashMap<>();
private SkinManager() {
}
public void init(Application context){
this.mContext = context;
this.mAppResource = mContext.getResources();
}
public void setSkinResource(String skinPath){
if(TextUtils.isEmpty(skinPath) || !new File(skinPath).exists()){
isDefaultSkin = true;
return;
}
if(resourcesMap.containsKey(skinPath)){
isDefaultSkin = false;
SkinCache skinCache = resourcesMap.get(skinPath);
if(null!=skinCache){
mSkinResource = skinCache.getSkinResource();
skinPackageName = skinCache.getSkinPkgName();
return;
}
}
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager,skinPath);
mSkinResource = new Resources(assetManager,mAppResource.getDisplayMetrics(),mAppResource.getConfiguration());
skinPackageName = mContext.getPackageManager().getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES).packageName;
isDefaultSkin = TextUtils.isEmpty(skinPackageName);
if(!isDefaultSkin){
resourcesMap.put(skinPath,new SkinCache(skinPackageName,mSkinResource));
}
} catch (Exception e) {
e.printStackTrace();
isDefaultSkin = true;
}
}
/**
* 根据当前包中的resourceId得到皮肤包中 对应的名称的资源文件的resourceId
* @param resourceId
* @return
*/
public int getIdentifier(int resourceId){
if(isDefaultSkin){
return resourceId;
}
String resourceEntryName = mAppResource.getResourceEntryName(resourceId); //ic_launcher
String resourceType= mAppResource.getResourceTypeName(resourceId); //mimap /drawable
return mSkinResource.getIdentifier(resourceEntryName,resourceType,skinPackageName);
}
public int getColor(int resId) {
if (isDefaultSkin) { // 如果没有皮肤,那就加载当前App运行的Apk资源
return mAppResource.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
return mAppResource.getColor(resId);
}
// skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
return mSkinResource.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) { // 如果没有皮肤,那就加载当前App运行的Apk资源
return mAppResource.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
return mAppResource.getColorStateList(resId);
}
// skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
return mSkinResource.getColorStateList(skinId);
}
public ColorStateList getColorStateList2(int resId, int attrValueInt) {
if (isDefaultSkin) { // 如果没有皮肤,那就加载当前App运行的Apk资源
return mAppResource.getColorStateList(resId);
}
int skinId = getIdentifier(attrValueInt);
if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
return mAppResource.getColorStateList(attrValueInt);
}
// skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
return mSkinResource.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
//如果有皮肤 isDefaultSkin false 没有就是true
if (isDefaultSkin) {
return mAppResource.getDrawable(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResource.getDrawable(resId);
}
// skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
return mSkinResource.getDrawable(skinId);
}
public String getString(int resId) {
try {
if (isDefaultSkin) { // 如果没有皮肤,那就加载当前App运行的Apk资源
return mAppResource.getString(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
return mAppResource.getString(skinId);
}
// skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
return mSkinResource.getString(skinId);
} catch (Resources.NotFoundException e) {
}
return null;
}
/**
* 获取background 是特殊情况,因为:
* 可能是color
* 可能是drawable
* 可能是mipmap
* 所有得到当前属性的类型Resources.getResourceTypeName(resId); 进行判断
* @return
*/
public Object getBackground(int resId) {
String resourceTypeName = mAppResource.getResourceTypeName(resId);
if (resourceTypeName.equals("color")) {
return getColor(resId);
} else if (resourceTypeName.equals("drawable") || resourceTypeName.equals("mipmap")) {
// drawable or mipmap
return getDrawable(resId);
}
return getColorStateList(resId);
}
/**
* 获得字体
* @param resId
* @return
*/
public Typeface getTypeface(int resId) {
/**
* 获取到了字符串,可能是 本地xxx.skin皮肤包资源的字符串 还是 当前App运行的Apk资源的字符串,这个不关注
* 暂停了 ...
*/
String skinTypefacePath = getString(resId);
if (TextUtils.isEmpty(skinTypefacePath)) {
return Typeface.DEFAULT;
}
try {
Typeface typeface;
if (isDefaultSkin) {
typeface = Typeface.createFromAsset(mAppResource.getAssets(), skinTypefacePath);
return typeface;
}
typeface = Typeface.createFromAsset(mSkinResource.getAssets(), skinTypefacePath);
return typeface;
} catch (RuntimeException e) {
}
return Typeface.DEFAULT;
}
public boolean getIsDefaultSkin() {
return isDefaultSkin;
}
}
SkinManager写好之后,我们只需要在用户下载完对应的皮肤包之后,通过调用setSkinResource方法将对应皮肤包相关Resource解析得到,然后就可以很快的实现换肤了。正常情况下是从服务下载下来皮肤包,解析然后唤起换肤。这里只是用本地apk包替换而已。
这种方式实现的换肤,虽然可能需要定义很多自定义控件,但是其实结构单一 只需要实现接口,开发人员自己自定义的控件,自己管理自己控件里面的换肤逻辑就OK,我们可以实现 任何控件任何自定义属性的换肤,并且不同的开发人员还相互不影响。开发的目的不是为了代码少。而是为了逻辑清晰,简单,可扩展性比较好。这是方式二优于方式一的地方。如下面的例子:
public class CustomCircleView extends View implements ViewsMatch {
private Paint mTextPain;
private AttrsBean attrsBean;
public CustomCircleView(Context context) {
this(context, null);
}
public CustomCircleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
attrsBean = new AttrsBean();
// 根据自定义属性,匹配控件属性的类型集合,如:circleColor
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.CustomCircleView,
defStyleAttr, 0);
int corcleColorResId = typedArray.getResourceId(R.styleable.CustomCircleView_circleColor, 0);
// 存储到临时JavaBean对象
attrsBean.saveViewResource(typedArray, R.styleable.CustomCircleView);
// 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
typedArray.recycle();
mTextPain = new Paint();
mTextPain.setColor(getResources().getColor(corcleColorResId));
//开启抗锯齿,平滑文字和圆弧的边缘
mTextPain.setAntiAlias(true);
//设置文本位于相对于原点的中间
mTextPain.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 获取宽度一半
int width = getWidth() / 2;
// 获取高度一半
int height = getHeight() / 2;
// 设置半径为宽或者高的最小值(半径)
int radius = Math.min(width, height);
// 利用canvas画一个圆
canvas.drawCircle(width, height, radius, mTextPain);
}
@Override
public void skinnableView() {
// 根据自定义属性,获取styleable中的circleColor属性
int key = R.styleable.CustomCircleView[0]; // = R.styleable.CustomCircleView_circleColor
int resourceId = attrsBean.getViewResource(key);
if (resourceId > 0) {
if (SkinManager.getInstance().getIsDefaultSkin()) {
int color = ContextCompat.getColor(getContext(), resourceId);
mTextPain.setColor(color);
} else {
int color = SkinManager.getInstance().getColor(resourceId);
mTextPain.setColor(color);
}
}
invalidate();
}
}
所有的自定义控件需要实现 ViewsMatch 接口,重写skinnableView()方法之后,才能实现换肤,上面CustomCircleView也不例外,这里添加了一个自定义属性:
<!-- 自定义控件属性 -->
<declare-styleable name="CustomCircleView">
<attr name="circleColor" format="color|integer" />
</declare-styleable>
我们只需要自己去解析 自己需要的属性,并且解析存放在AttrBean中,然后实现自己的换肤逻辑。