网易动态换肤技术

  • 作者:彭老师
  • 日期:2019-07-17
  • 类型:Android
  • 说明:本文源于 彭老师 手写摘要,如需转载请带上链接或注明出处!

前言

动态换肤 技术在app应用中,越来越普及大众化了。那么具体的实现是怎么样呢?

今天我们就来大致分析下大众版换肤技术(思路分析)

核心6点:

  • 1、 registerActivityLifecycleCallbacks注册观察者
  • 2 、Activity接收用户操作:点击换肤按钮
  • 3 、调用被观察者Observable方法:setChanged(); notifyObservers();
  • 4 、实现Fatory2接口(收集/采集所有控件),并实现观察者Observer执行update()方法
  • 5 、遍历2层(所有View控件,每个View控件所有属性),匹配换肤属性
  • 6 、有皮肤包加载皮肤包资源,找不到则加载内置资源,更改控件属性值

1、registerActivityLifecycleCallbacks注册观察者

这是什么方法?有什么作用?

监听当前应用所有Activity生命周期方法的执行,也是谷歌AOP思想

该方法的意义就是:执行在setContentView()方法之前

public class SkinActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {

private SkinFactory skinFactory;

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

LayoutInflater layoutInflater = LayoutInflater.from(activity);

// 利用反射去修改mFactorySet的值为false,防止抛出 A factory has already been set on this...
try {
Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
mFactorySet.setAccessible(true);
mFactorySet.set(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}

skinFactory = new SkinFactory(activity);
LayoutInflaterCompat.setFactory2(layoutInflater, skinFactory);

// 注册观察者(监听用户操作,点击了换肤,通知观察者更新)
SkinEngine.getInstance().addObserver(skinFactory);
}

// 代码省略

@Override
public void onActivityDestroyed(Activity activity) {
// 解除注册的观察者
SkinEngine.getInstance().deleteObserver(skinFactory);
}
}

2 、Activity接收用户操作:点击换肤按钮

布局activity_netease.xml接收到用户的换肤操作,即点击按钮事件

3 、调用被观察者Observable方法:setChanged()notifyObservers()

// 被观察者
public class SkinEngine extends Observable {

// 代码省略

/**
* 用户点击 换肤按钮
*/
public void updateSkin() {
setChanged();
notifyObservers();
}
}

4 、实现Fatory2接口(收集/采集所有控件),并实现观察者Observer执行update()方法

public class SkinFactory implements LayoutInflater.Factory2, Observer {

// 代码省略

/**
* 拦截View的创建,此View指的是 布局文件中 某个控件,例如:TextView,Button,自定义控件,...
*
* @param parent 父控件View
* @param name 控件的名字,例如:TextView
* @param context 上下文环境
* @param attrs 控件的属性,例如:TextView(定义了很多的属性 宽 高 text textColor ...)
* @return 返回创建好的View,如果返回null,系统内部就会创建View
*/
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

// 此createViewFromTag,目的是去创建系统的控件 例如:TextView,Button,ImageView
View resultView = createViewFromTag(parent, name, context, attrs);

switch (name) {
case "ImageView":
resultView = new ImageView(context, attrs);
break;
case "xxxxx":
// 省略
break;
}

// 如果为null,可认为是自定义View,所以需要传入 name + "" ---> 自定义控件包名和控件名 + ""
if (null == resultView) {
resultView = createView(name, "", context, attrs);
}

/**
* 换肤第一步:收集控件信息:
* 1、一个布局中有 N 个控件 List<ViewInfo>
* 2、一个具体控件有 N 个属性 List<AttrsForView>
* 3、一个具体的属性有键值对:key:android:textColor, value:@color/xxx
*/
widgetViewList.saveWidgetView(attrs, resultView);

return resultView;
}

/**
* 创建系统控件 例如:TextView,ImageView,Button ...
*/
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
View view = null;
for (String s : sClassPrefixList) {
view = createView(name, s, context, attrs);
if (view != null) {
break;
}
}
return view;
}

/**
*
* 真正进行反射的方式创建View
* 1.当传入 name + s --> 控件名 + 控件包名, 需要创建系统的控件。
* 2.当传入 name + "" --> 控件名 + "" 这个控件名就是完整的 自定义 包名+自定义控件名
*/
private View createView(String name, String prefix, Context context, AttributeSet attrs) {
Constructor<? extends View> constructors = sConstructorMap.get(name);

if (null == constructors) {

Log.d(TAG, "需要反射找的>>>>>>>>>> prefix + name:" + prefix + name);
// 反射找
try {
Class<? extends View> classz = context.getClassLoader().loadClass(prefix + name).asSubclass(View.class);
Constructor<? extends View> constructor = classz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor); // 缓存一份
return constructor.newInstance(context, attrs);
} catch (Exception e) {
}

} else {
try {
constructors.setAccessible(true);
return constructors.newInstance(context, attrs);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}

/**
* 换肤第二步:收到 被观察者 发出的---> 改变通知
* 当被被观察者发生改变时回调
*/
@Override
public void update(Observable o, Object arg) {
// 换肤第三步:告诉WidgetViewList去换肤,因为WidgetViewList身上有所有需要换肤的控件
widgetViewList.skinChange();
}
}

5 、遍历2层(所有View控件,每个View控件所有属性),匹配换肤属性

public class WidgetViewList {

// 代码省略

/**
* 换肤第四步:遍历所有保存的 控件(WidgetView)
* 每一个控件执行换肤
*/
public void skinChange() {
for (WidgetView widgetView : WIDGET_VIEWS) {
widgetView.skinChange();
}
}

/**
* 由于最终要保存 属性名 = 资源ID(Int型)attrValueInt
* 类似于:android:textColor=
* 所以定义JavaBean
*/
class AttributeNameAndValue {
String attrName;
int attrValueInt;

public AttributeNameAndValue(String attrName, int attrValueInt) {
this.attrName = attrName;
this.attrValueInt = attrValueInt;
}
}

/**
* 由于 一个TextView对应一个View,一个View中 有 多个AttributeNameAndValue
* 所以需要描述这个对象,这个对象可以抽象理解为 TextView == WidgetView
* 类似于:
* <TextView
* android:layout_width="wrap_content"
* android:layout_height="wrap_content"
* android:text="测试"
* android:textSize="30sp"
* android:textColor="@color/skin_my_textColor" />
*
* 所以定义成JavaBean
*/
class WidgetView {

View mView;
List<AttributeNameAndValue> attributeNameAndValues;

public WidgetView(View mView, List<AttributeNameAndValue> attributeNameAndValues) {
this.mView = mView;
this.attributeNameAndValues = attributeNameAndValues;
}

/**
* 换肤第五步:遍历当前这个控件(WidgetView==TextView) 里面的属性(AttributeNameAndValue)
* android:layout_width="wrap_content"
* android:layout_height="wrap_content"
* android:text="测试"
* android:textSize="30sp"
* android:textColor="@color/skin_my_textColor"
*/
public void skinChange() {
for (AttributeNameAndValue attributeNameAndValue : attributeNameAndValues) {
switch (attributeNameAndValue.attrName) {
case "background":
// 代码省略
if (background instanceof Integer) {
mView.setBackgroundColor((Integer) background);
} else {
mView.setBackground((Drawable) background);
}
break;

case "textColor":
// 代码省略
TextView textView = (TextView) mView;
textView.setTextColor(xxx);
break;
}
}
}
}
}

6 、有皮肤包加载皮肤包资源,找不到则加载内置资源,更改控件属性值(AssetsManager)

根据app内置资源的 resourceID 获取资源 nametype ,需要指定包名(皮肤包)资源,如果skinResourceID 等于0则说明皮肤包资源加载失败

/**
* 参考:resources.arsc资源映射表
* 通过ID值获取资源的 Name 和 Type
*
* @param resourceId 资源的ID值(app内置资源)
* @return 如有没有皮肤包则加载app内置资源ID,反之则加载皮肤包对应的资源ID
*/
private int getSkinResourceIds(int resourceId) {

String resourceName = appResources.getResourceEntryName(resourceId);// "music_bg"
String resourceType = appResources.getResourceTypeName(resourceId);// drawable

// 动态获取皮肤包内的指定资源ID
int skinResourceId = skinResources.getIdentifier(resourceName, resourceType, skinPackageName);

return skinResourceId == 0 ? resourceId : skinResourceId;
}

这个架构的缺陷和性能:

  • 1、置换setContentView()方法,容易不兼容,且自定义控件麻烦

  • 2、控件信息收集:临时集合 + 所有控件

  • 3、换肤时,双层遍历 + 改变控件

截图如下:


网易换肤技术,性能至少是上面 7 倍。随着布局中的控件递增,差距是几何倍!(支持自定义控件、字体,兼容5.0 - 9.0)

截图如下:

效果图如下:



更多技术内幕请关注:网易云课堂 - 微专业 - 安卓高级开发工程师