Android 内置的换肤
Android 中可以动态切换主题,但是只能切换主题的颜色,并且主题的切换会对view进行重新加载并不连贯.
Android 中内置的换肤需要在apk中存在需要换肤的图片等资源,导致apk包的增大,并且不能进行更多皮肤的扩展,需要重新打包上线.
如何实现动态换肤
类似网易云音乐的动态换肤,是通过下载皮肤包,加载皮肤包来实现动态换肤的功能,那么皮肤包是啥呢? 其实就是apk包,读取apk中的资源文件进行替换就可以实现动态的换肤功能.
实现代码地址:https://github.com/JakePrim/PrimSkinCore
那么动态换肤的实现原理是什么? 我们知道在Activity和Fragment通过 setContentView
来加载布局文件,setContentView
中如何加载布局呢? 内部通过 LayoutInflater
布局加载器来加载布局. LayoutInflater
如何加载布局呢? 我们看如下系统的源代码:
LayoutInflater.from(mContext).inflate(resId, contentParent);
//inflate 做了什么呢? 点击看下inflate 方法做了什么
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
.....
//这里是获取返回的view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
........
}
//在createViewFromTag返回了一个view
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
.....
View view;
if (mFactory2 != null) {//如果mFactory2 != null 则通过mFactory2加载
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {//如果mFactory != null 则通过mFactory加载
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
//默认通过一下方式返回view
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;
......
}
上述代码中首先判断mFactory2是否为空,如果不为空就交给了mFactory2去处理返回view,很明显google给了我们支持自定义的方法.setFactory2方法还是public的公有的方法,我们只需要实现Factory2,对view进行响应的处理然后返回就可以实现动态换肤. 在上述代码中还有一个 mFactory
其实是和mFactory2 一样的道理,实现mFactory也是一样的
只不过 mFactory2处于第一个,防止有其他的地方实现了mFactory2,所以我们就需要去实现mFactory2,确保mFactory2被正确运行.
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
我们看了上述的系统源代码,知道了系统是支持自定义mFactory2,来自己实现对view对加载,返回view.加载view只需要参考系统的代码即可,核心的代码就是筛选属性,然后修改属性的值,来实现颜色和图片的改变.
那么系统是如何加载view的呢? 我们从系统的源代码中来查找,在 createViewFromTag
方法中name.indexOf(‘.’)如果布局中的view带. 这说明是自定义view,否则是基本的view,我们先来看一下基本的view,来看一下 onCreateView
方法
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;
}
}
onCreateView其实是有AsyncLayoutInlater调用的onCreateView
private static class BasicInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
BasicInflater(Context context) {
super(context);
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new BasicInflater(newContext);
}
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
}
最核心的代码如下: 本质上是通过反射来实现返回View对象即可.是不是很简单,系统的源码就是这样加载布局的.
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
我们在得到view对象之后,获取view的属性将view的属性资源修改为皮肤包的资源.需要采集需要换肤的需要替换的属性.
采集如下基本属性,替换为皮肤包中的资源ID
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
动态换肤的实现
在上述中我们知道通过布局加载器LayoutInflater来加载布局,所以我们需要每进入一个Activity拿到 LayoutInflater
设置自定义布局加载工厂,如果建立一个BaseActivity对代码对入侵性很强,Application中提供监听App的所有Activity的生命周期回调.
代码如下
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
private HashMap<Activity, SkinLayoutFactory> mLayoutFactoryMap = new HashMap<>();
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//拿到每个activity的布局加载器
LayoutInflater layoutInflater = LayoutInflater.from(activity);
//mFactorySet 如果为true 抛出异常,有可能会出现这种情况,通过反射将mFactorySet设置为false
try {
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//自定义布局处理工厂
SkinLayoutFactory factory = new SkinLayoutFactory();
//设置工厂
LayoutInflaterCompat.setFactory2(layoutInflater, factory);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
}
接下来就是自定义 LayoutInflater.Factory2
,Factory2的实现其实就是参考系统的实现,代码原理也非常简单,代码如下
public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
new HashMap<String, Constructor<? extends View>>();
private static final Class<?>[] mConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private SkinAttribute skinAttribute;
public SkinLayoutFactory() {
skinAttribute = new SkinAttribute();
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//参考系统的实现
View view = createViewFromTag(context, name, attrs);
if (view == null) {
view = createView(name, context, attrs);
}
//采集view的属性
skinAttribute.load(view,attrs);
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.contains(".")) {//如果布局xml中的view 名包含.表示为自定义view 此处先不做处理
return null;
}
View view = null;
for (String prefix : sClassPrefixList) {//遍历view属于哪个包中 然后反射创建view对象
try {
view = createView(prefix + name, context, attrs);
if (view != null) {
break;
}
} catch (Exception e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return view;
}
private View createView(String name, Context context, AttributeSet attrs) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
} catch (Exception e) {
}
if (null != constructor) {
try {
return constructor.newInstance(context, attrs);
} catch (Exception e) {
}
}
return null;
}
@Override
public void update(Observable o, Object arg) {
skinAttribute.applySkin();
}
}
真正实现换肤的就是,对Attribute的操作, 接下来看一下SkinAttribute的实现很非常简单,其实就是遍历所有view的属性拿到自己需要的属性,然后存储在集合中如下:
//采集的属性集合
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
public void load(View view, AttributeSet attrs) {
List<SkinPair> skinPairs = new ArrayList<>();
//获取属性
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获取属性名
String attributeName = attrs.getAttributeName(i);
//匹配要修改的属性名
if (mAttributes.contains(attributeName)) {
//获取属性值
String attributeValue = attrs.getAttributeValue(i);
if (attributeValue.startsWith("#")) {//写死的先不去管 #000000
continue;
}
int resId;
if (attributeValue.startsWith("?")) {
//attrId
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = utils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
//@1232311
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
//将匹配的都存储下来
SkinPair skinPair = new SkinPair(attributeName, resId);
skinPairs.add(skinPair);
}
}
}
//将布局中每个view与之对应的属性集合放入到对应的view集合中
if (!skinPairs.isEmpty()) {
SkinView skinView = new SkinView(view, skinPairs);
//每次新加载activity就要进行换肤
applySkin();
skinViews.add(skinView);
}
}
至于SkinView类存储了属性名和资源ID和当前的view
/**
* 保存每个view要修改的属性值列表
*/
static class SkinView {
View view;
List<SkinPair> skinPairs;
public SkinView(View view, List<SkinPair> skinPairs) {
this.view = view;
this.skinPairs = skinPairs;
}
}
static class SkinPair {
String attrName;
int resId;
public SkinPair(String attrName, int resId) {
this.attrName = attrName;
this.resId = resId;
}
}
上述代码就是核心的实现,最后我们需要实现初始化和资源的切换,代码如下 通过皮肤包的路径,Application获取到资源管理器AssetManager获取皮肤包的资源,然后通过SkinResources来实现资源的加载,然后使SkinLayoutFactory做为观察者,当更换皮肤包时更新SkinLayoutFactory. 然后通过SkinAttribute将属性的资源更改为皮肤包的资源
/**
* 加载皮肤包
*
* @param path
*/
public void loadSkin(String path) {
if (path == null || path.isEmpty()) {
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 resources = application.getResources();
//皮肤包的资源
Resources skinResources = new Resources(assetManager,
resources.getDisplayMetrics(), resources.getConfiguration());
//加载皮肤包资源
//皮肤包的包名
PackageManager packageManager = application.getPackageManager();
//获取一个apk的包名
PackageInfo info = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = info.packageName;
Log.e(TAG, "loadSkin: " + packageName);
//更换为皮肤包资源
SkinResources.getInstance().applySkin(skinResources, packageName);
//记录使用的皮肤包
SkinPreference.getInstance().setSkin(path);
} catch (Exception e) {
e.printStackTrace();
}
}
//通知观察者 //应用皮肤包
setChanged();
notifyObservers();
}
SkinResources的实现,其实就是根据app的资源的资源名和资源类型,通过皮肤包的资源返回皮肤包的资源ID,如果没有就返回当前app的资源ID.
public class SkinResources {
//皮肤的资源
private Resources mSkinResources;
//当前应用的资源
private Resources mAppResources;
//皮肤包名
private String mSkinName;
//是否使用默认皮肤
private boolean isDefalueSkin = true;
public static SkinResources instance;
public static void init(Context context) {
if (null == instance) {
synchronized (SkinResources.class) {
if (null == instance) {
instance = new SkinResources(context);
}
}
}
}
public static SkinResources getInstance() {
return instance;
}
public SkinResources(Context context) {
mAppResources = context.getResources();
}
public void reset() {
mSkinResources = null;
mSkinName = "";
isDefalueSkin = true;
}
public void applySkin(Resources resources, String name) {
mSkinResources = resources;
mSkinName = name;
isDefalueSkin = false;
}
/**
* 通过资源ID 获取皮肤包中对应的资源ID 然后进行设置
*
* @param resId
* @return
*/
public int getIdentfier(int resId) {
if (isDefalueSkin) {
return resId;
}
//通过当前应用资源获取到 资源名 和 资源类型,然后通过资源名 资源类型 资源ID 获取到皮肤包中的资源ID
String resourceName = mAppResources.getResourceEntryName(resId);
String resourceTypeName = mAppResources.getResourceTypeName(resId);
//getIdentifier ?? 获取到皮肤包对应到资源ID
int skinId = mSkinResources.getIdentifier(resourceName, resourceTypeName, mSkinName);
return skinId;
}
private static final String TAG = "SkinResources";
public int getColor(int resId) {
if (isDefalueSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentfier(resId);
Log.e(TAG, "getColor: " + skinId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefalueSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentfier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
//如果有皮肤 isDefaultSkin false 没有就是true
if (isDefalueSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentfier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
/**
* 可能是Color 也可能是drawable
*
* @return
*/
public Object getBackground(int resId) {
String resourceTypeName = mAppResources.getResourceTypeName(resId);
if (resourceTypeName.equals("color")) {
return getColor(resId);
} else {
// drawable
return getDrawable(resId);
}
}
public String getString(int resId) {
try {
if (isDefalueSkin) {
return mAppResources.getString(resId);
}
int skinId = getIdentfier(resId);
if (skinId == 0) {
return mAppResources.getString(skinId);
}
return mSkinResources.getString(skinId);
} catch (Resources.NotFoundException e) {
}
return null;
}
public Typeface getTypeface(int resId) {
String skinTypefacePath = getString(resId);
if (TextUtils.isEmpty(skinTypefacePath)) {
return Typeface.DEFAULT;
}
try {
Typeface typeface;
if (isDefalueSkin) {
typeface = Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
return typeface;
}
typeface = Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
return typeface;
} catch (RuntimeException e) {
}
return Typeface.DEFAULT;
}
}
当切换资源通知观察者SkinLayoutFactory的SkinAttribute 来采集属性和更换资源ID.代码的实现都非常简单
/**
* 更换皮肤
*/
public void applySkin() {
//遍历保存的表然后换皮肤
for (SkinView skinView : skinViews) {
skinView.applySkin();
}
}
/**
* 保存每个view要修改的属性值列表
*/
static class SkinView {
View view;
List<SkinPair> skinPairs;
public SkinView(View view, List<SkinPair> skinPairs) {
this.view = view;
this.skinPairs = skinPairs;
}
public void applySkin() {
for (SkinPair skinPair : skinPairs) {
Drawable left = null, right = null, top = null, bottom = null;
switch (skinPair.attrName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPair.resId);
//可能是一个图片或者颜色
if (background instanceof Integer) {
//color 选择器
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
Object background1 = SkinResources.getInstance().getBackground(skinPair.resId);
if (background1 instanceof Integer) {
imageView.setImageDrawable(new ColorDrawable((Integer) background1));
} else {
imageView.setImageDrawable((Drawable) background1);
}
}
break;
case "textColor":
int color = SkinResources.getInstance().getColor(skinPair.resId);
if (view instanceof TextView) {
TextView textView = (TextView) view;
textView.setTextColor(color);
}
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;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
bottom);
}
}
}
}
OK,现在我们实现了基本的动态换肤功能,目前我们实现的换肤功能还没有实现自定义view和状态栏的换肤功能.下一节来讲解如何实现自定义view的换肤和状态栏的换肤,其实代码写到这里大家都知道了实现的原理可以先试着自己实现.