今天介绍一下Android 中的常用的换肤策略,同时动手实现一个动态换肤的框架
先上效果图:
换肤概念
换肤: 在android中是指 对 文字、 颜色、 图片 等的资源的更换。
人 : 对应于现实生活中,就是我们的 肤色 、 衣服 等的更换。
有什么好处或者说 目的是什么 ??
对应于我们android 中呢,就是 可以 满足用户的新鲜感,可以提高用户的留存率,当然 皮肤也可以进行收费。 这就好比我们生活中 去旅行时,有普通的酒店, 也有很多主题酒店,有各种各样的…, 你会去哪个呢?
换肤实现方式
在我们的开发中, 一般常用的有两种换肤的方式:
第一种: 静态换肤:
这种换肤的方式,也就是我们所说的内置换肤,这种方式我也用过很多次,就是在APP内部放置多套相同的资源。进行资源的切换(这种方式,我想你也用过,当然,我们今天并不是要说这种). 这种换肤的方式有很多缺点,比如, 灵活性差,只能更换内置的资源、apk体积太大,在我们的应用Apk中等一般图片文件能占到apk大小的一半左右。
当然了,这种方式也并不是一无是处, 任何事物的存在,必然有其理由。比如我们的应用内,只是普通的 日夜间模式 的切换,并不需要图片等的更换,只是更换颜色,那这样的方式就很实用。
啰嗦了半天… 是时候开始正式进入我们今天的主题.
~
~
~
Ok .
第二种:动态换肤
怎么个动态法呢? 即在运行时动态的加载皮肤包
这么说可能有点不是很好理解,大白话就是,当控件初始化完毕,需要设置内容(图片
、颜色
等)时,动态的去我们对应的皮肤包去找。
~ 我这么说你可能觉的这跟静态换肤没啥不同都是 需要去 set
。
我们这里呢,强调两点 :
1.动态
,即灵活性比较高,可以随意的切换 , 当然这是基于第二点上 .
2.皮肤包
,这个和apk
是分离开来的,我们可以去网络,去服务器获取到皮肤包,想怎么换就怎么换。
是时候开始表演真正的技术了, 哈哈~~~
在开始之前,我们先重申一点: 我们今天要做的是框架。 所以我们要站在框架开发者的角度, 怎么让使用这更加方便、更加简洁的使用。
Ok, 开始 !
先总结一下我们的整体步骤:
1.采集控件 获取需要换肤的控件。(要换肤,你总得拿到或者知道要换哪些控件吧)
2.加载皮肤包 加载我们的外置皮肤包。
3.换肤 这一步呢,我们需要控件与皮肤包资源的匹配,进行换肤.
采集控件
我们需要去拿到每一个页面的 所有需要换肤的控件。你可能会想到在每个Activity
的OnCreate()
中去获取,然后保存到集合中。 stop! ~~~ ok 我们要做的是一个框架,要做到尽可能少的去 侵入用户的代码。 下面这个 api, 可以拿到Activity
的回调。
application.registerActivityLifecycleCallbacks(参数)
这个方法可以获取到Activity
的生命周期回调 ActivityLifecycleCallbacks。
ok,下面我们来点实际的。
我们新建一个类,用来做初始化,application
中初始化。
public class SkinChangeManager {
private static final String TAG = SkinChangeManager.class.getSimpleName();
private static SkinChangeManager _Instance = null;
private static Application mContext;
private ActivityCallbacks mActivityCallBacks;
public static void init(Application context) {
mContext = context;
synchronized (SkinChangeManager.class) {
if (_Instance == null) {
_Instance = new SkinChangeManager();
}
}
}
public SkinChangeManager() {
SkinPreference.init(mContext); // sp 保存
SkinResources.init(mContext); // 资源的获取
//注册activity的回调 此方法运行在 super.xxx 之前
mActivityCallBacks = new ActivityCallbacks();
mContext.registerActivityLifecycleCallbacks(mActivityCallBacks);
}
public static SkinChangeManager getInstance() {
return _Instance;
}
ok, 在构造方法 的初始化中,我们 创建了 ActivityLifecycleCallbacks
的实例,并进行了注册 registerActivityLifecycleCallbacks
.
下面是 ActivityCallbacks
的代码:
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
...
你或许已经猜到了, 我们要搞点事的地方,没错,就是她 onActivityCreated
ok~~~,接下来我们要拿到所有的View
。这下才是重点。
需要拿到View,我们就要知道setContentView
是干了什么事。
简单描述下流程:
setContentView -> window.setContentView()(实现类是PhoneWindow)->mLayoutInflater.inflate() -> inflate .. ->createViewFromTag().
下面这段是 系统 createViewFromTag()
中的部分代码:
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
ok,非常简单,首先判断了mFactory2
、mFactory
是否为null。然后,判断是否是系统View if (-1 ==
那么 拼接前缀进行反射创建实例,如果是自定义的View ,则直接去创建了实例。
name.indexOf('.'))
这里我们的思路是 View部分的创建我们自己来完成.
即给LayoutInflater 设置一个 Factory
我们模仿系统的实现来给系统返回一个View,当然我们也可以在中间做一些我们想做的操作。
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
LayoutInflater inflater = LayoutInflater.from(activity);
SkinViewFactory skinFactory = new SkinViewFactory(activity,mSkinTypeface);
inflater.setFactory2(skinFactory);
}
ok, 我们来看 SkinFactory的实现:
public class SkinViewFactory implements LayoutInflater.Factory2{
private Activity mActivity;
private SkinAttribute mSkinAttrs; // 此View 用来存储 所有我们需要换肤的view
private static final Class<?>[] mConstructorSignature = new Class[]{
Context.class, AttributeSet.class}; // 两个参数的签名
private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
new HashMap<String, Constructor<? extends View>>();
private static final String[] mClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
public SkinViewFactory(Activity activity, Typeface mSkinTypeface) {
this.mActivity = activity;
mSkinAttrs = new SkinAttribute(mSkinTypeface);
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createViewFromTag(name, context, attrs);
//自定义view的情况 这里是全类名
if (view == null) {
view = createView(name, context, attrs);
}
//view 创建完毕了, 我们要从其中晒选出我们需要 换肤的view
mSkinAttrs.load(view,attrs);
return view;
}
private View createViewFromTag(String name, Context context, AttributeSet attrs) {
View view = null;
if (-1 != name.indexOf('.')) { // 包含了. 自定义view
return null;
}
for (int i = 0; i < mClassPrefixList.length; i++) { // 拼接 然后 生成view
view = createView(mClassPrefixList[i] + name, context, attrs);
if (view != null) {
break;
}
}
return view;
}
private View createView(String name, Context context, AttributeSet attrs) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor == null) {
try {
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = aClass.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} catch (Exception e) {
e.printStackTrace();
}
}
if (null != constructor) {
try {
return constructor.newInstance(context, attrs);
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
}
SkinFactory
中我们做了两件事:
- View的创建并返回,由系统来进行后续的操作
- 我们创建了一个实体
SkinAttribute
并调用了mSkinAttrs.load(view,attrs);
来进行View的筛选。
我们接着来看筛选的逻辑部分, 即mSkinAttrs.load(view,attrs);
/**
* 获取view 中的属性, 过滤出 需要换肤的View 以及 相关的属性 保存起来
*
* @param view
* @param set
*/
public void load(View view, AttributeSet set) {
List<SkinPair> skinPairs = new ArrayList<>();
int attributeCount = set.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attrName = set.getAttributeName(i);
if (mAttributes.contains(attrName)) {
int resId;
String attributeValue = set.getAttributeValue(i);
Log.e(TAG, "attribute name :" + attrName + " attribute value " + attributeValue);
if (attributeValue.startsWith("#")) {
// 如果写固定了,比如#111111 我们则不做操作
continue;
} else if (attributeValue.startsWith("?")) {
// 主题中的资源
int attrId = Integer.parseInt(attributeValue.substring(1));
// 获取到attrId
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0]; } else {
//@1213432 格式
resId = Integer.parseInt(attributeValue.substring(1));
// 剪切之前 类似 @2131165298
}
if (resId != 0) {
// 存储对应的 name - id 比如. drawable - 121321423 SkinPair skinPair = new SkinPair(attrName, resId); skinPairs.add(skinPair);
}
}
}
if (!skinPairs.isEmpty()) {
SkinView skinView = new SkinView(view, skinPairs); skinView.applySkin(mSkinTypeface);
mSkinViews.add(skinView);
}
}
在下面我们定义了一个 所要过滤View 属性的集合。 即满足该属性的View,都会被保存起来。当然,你也可以添加你想过滤的属性到里面。
//用来记录所有需要更换皮肤的 属性
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");
}
以上,我们就把 采集控件 的步骤完成了, 把需要换肤的控件 都 保存了起来,具体的代码我会在 文章放上链接。 大家可以下载下来详细的看流程。
加载皮肤包
即把皮肤包(外置的apk或者zip文件)加载进来。 下面是SkinChangeManager.java
中的代码。
/**
* 加载皮肤包
*
* @param path
*/
public void loadSkin(String path) {
if (TextUtils.isEmpty(path)) {
SkinPreference.getInstance().setSkin("");
SkinResources.getInstance().reset();
} else {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, path);
Resources mAppResource = mContext.getResources();
//获取外部Apk(皮肤包) 包名
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager
.GET_ACTIVITIES);
String packageName = info.packageName;
Resources mSkinResources = new Resources(assetManager, mAppResource.getDisplayMetrics(), mAppResource.getConfiguration());
Log.e(TAG,"package Name : "+packageName);
SkinResources.getInstance().applySkin(mSkinResources, packageName);
SkinPreference.getInstance().setSkin(path);
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上的代码完成 加载皮肤包,并 得到皮肤包 对应的 Resources
。
然后交给了 SkinResources
来处理。就这么简单?
没错,皮肤包加载完了 ~~~
我们需要稍微提一下Android 加载资源的方式:
ok, 你理解了上面的图中的图片加载方式,就能理解SkinResources
的代码操作了.下面是部分 SkinResources
的代码:
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮肤包中不一定就是 当前程序的 id
//获取对应id 在当前的名称 colorPrimary
//R.drawable.ic_launcher
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
String resType = mAppResources.getResourceTypeName(resId);//drawable
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName); 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);
}
稍微解释一下: 当你现在使用的是apk的默认皮肤,会直接 mAppResources.getColor(resId);
如果不是 默认皮肤,就会通过 resId
获取到对应的资源名 和 资源类型 ,然后去皮肤包去取。
说了半天了,终于该到最后一步了.
换肤
在我们的 SkinAttribute
这个类中,有一个类 SkinView
用来记录换肤的View 和 View对应的属性 key(比如 textColor
属性) 。
面向对象的思想,谁知道你该穿什么衣服? 当然是你自己咯 ~ 。
所以我们的 换肤方法 就在SkinView
中 :
/**
* 应用皮肤, 换肤 是View的 方法 所以我们在 View包装类 的内部进行 皮肤的切换
* @param mSkinTypeface
*/
public void applySkin(Typeface mSkinTypeface) {
for (SkinPair skinPair : skinPairs) {
Drawable left = null, top = null, right = null, bottom = null;
switch (skinPair.attributesName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPair.resId);
// Color
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
view.setBackgroundDrawable((Drawable) background);
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPair
.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
(skinPair.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
bottom);
}
}
}
总结:
你知道动态的换肤需要几步么?那你知道把大象装进冰箱需要几步么?
没错,三步就够了。
第一步:采集所有需要换肤的控件,保存起来。
第二步:加载皮肤包。
第三步:换肤(让控件对应上指定的资源)
到这里,我们的换肤的整体思路与代码逻辑就结束,还有一些关于 字体、状态栏 、自定义View 等的换肤,整体的思想是一致的,大家可以去参看完整的源码。
代码地址
https://download.csdn.net/download/mike_cui_ls/10311537