本人所写博客都是一句话带过,只说重点,博客只针对自己所写,怕忘了,如果想要学习,直接看代码,尽量别看我的说明。会把你带沟里
-
换肤分为两种:
内置换肤:适用日间模式、夜间模式这种皮肤需求极少,直接把资源打包到APK中。
动态换肤:适用于大量皮肤,用户选择下载、QQ、网易云音乐这种。它是将皮肤包下载到本地,皮肤包其实是个APK,网易云的皮肤包是skin结尾的,手动改成压缩包,即可看到项目资源。
(此处提一下 高德地图的换肤是动态换肤的,虽然看起来像是静态的,因为他的换肤是换路线颜色,高德地图的所有地图都是瓦片图片生成的,然后使用OpenGL画路线 点等)
-原理:
- 采集需要换肤的所有控件
- 加载皮肤包(resoure)
- 应用皮肤包
第一步采集
- 问: 首先明白setContView做了什么?
- 答: 我们写的Activity都会对应一个XML,而setContentView的作用就是将XML和Activity绑定一起的,也是通过布局加载器LayoutInflater加载
阅读源码理解部分
- 第一步 xml绑定activity先调用setContentView()方法,然后方法里执行了
LayoutInflater.from(mContext).inflate(resId, contentParent);
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
- inflate方法是获取Resoure, 看这句 final XmlResourceParser parser = res.getLayout(resource); 返回XML解析器解析布局文件。后来又调用一个inflate方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
- 第二个inflate方法,会将解析后的View再new出一个对象,此处调用了createViewFromTag(...)方法。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
- createViewFromTag(View parent, String name, Context context, AttributeSet attrs) 此处的name是XML节点类型名称,比如是Imageview,或者Button,大家再看创建new对象的,如果工厂不为null,创建一个View对象,如果view为null,并且名字包含'.'(常用的imageView不包含. 自定义的才包含. 比如自定义View com.XXX.XXX.ImageView )。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
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;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
- 这里知道自定义和系统的View,接下来看创建View,上面的方法说,系统View调用oncreateView,其实oncreateView是createView拼接了系统的android.view的名字,拿到全类名,通过mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) ,反射一下,获取View的Class对象,然后获得构造函数。 构造函数里有 static final Class>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};上下文,属性,调用构造函数, final View view = constructor.newInstance(args);创建一个View对象返回出去。
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
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);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
attrs.getPositionDescription() + ": Error inflating class "
+ (clazz == null ? "" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
(看懂源码,咱就动手撸)
首先说一下用户调用的最终效果:
// 初始化:
SkinManager.init(this);
//换肤调用:
SkinManager.getInstance().loadSkin("/sdcard/app-skin-debug.skin");
- 定义一个类,让用户初始化我们,拿到布局加载器。
- 拿到布局加载器的前提,需要获取Activity,(就是LayoutInflater.from(Activity())),此处使用 application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());获取Activity的活动,实现SkinActivityLifecycle()类,
- 那么拿到Activity,我们需要设置工厂让他帮我们生成View对象,并且加载到布局上面(就是源码Factory2的用法)。LayoutInflaterCompat.setFactory2(layoutInflater,new SkinLayoutFactory());
/**
* Created by LiChangXing
* on 2018/3/20.
*/
public class SkinManager {
private static SkinManager instance;
private Application application;
public static SkinManager init(Application application) {
synchronized (SkinManager.class) {
if (instance != null) {
instance = new SkinManager(application);
}
return instance;
}
}
private SkinManager getInstance() {
return instance;
}
public SkinManager(Application application) {
//注册
application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
}
}
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
/**
* 更新布局视图
*/
//获得Activity的布局加载器
LayoutInflater layoutInflater = LayoutInflater.from(activity);
try {
//Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
//如设置过抛出一次
//设置 mFactorySet 标签为false
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
LayoutInflaterCompat.setFactory2(layoutInflater,new SkinLayoutFactory());
} catch (Exception e) {
e.printStackTrace();
}
}
@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) {
}
工厂实现类SkinLayoutFactory
- 此处写的所有方法都是系统源码的逻辑
- 根据传过来的name(com.xxxx或者Imageview) 先判断是自定义View还是系统View
- 如果是系统的View(此处传过来的View就是XML解析后的数据)通过mClassPrefixList集合,拼接完整包名,再反射类,反射构造函数,最后实例化View对象,返回对象。自定义同样道理,只是不需要拼接系统包名了。
- 因为反射影响性能,此处写了一个键值对,缓存了构造方法
public class SkinLayoutFactory implements LayoutInflater.Factory2 {
private static final HashMap> sConstructorMap =
new HashMap>();
private static final String[] mClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
static final Class>[] mConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//每次渲染一个View 此方法都会执行
//反射classLoader 和系统一样
View view = createViewFromTag(name, context, attrs);
//自定义view
if (view == null) {
createView(name, context, attrs);
}
return null;
}
private View createViewFromTag(String name, Context context, AttributeSet attrs) {
//自定义组件
if (-1 == name.indexOf('.')) {
return null;
}
//拼接View的全包名 用于反射 获取View实例对象
View view = null;
for (int i = 0; i < mClassPrefixList.length; i++) {
view = createView(mClassPrefixList[i] + name, context, attrs);
if (null != view) {
break;
}
}
return view;
}
/**
* 创建View对象
*
* @param name
* @param context
* @param attrs
* @return
*/
private View createView(String name, Context context, AttributeSet attrs) {
Constructor extends View> constructor = sConstructorMap.get(name);
if (null == constructor) {
try {
Class extends View> aClass = context.getClassLoader().loadClass(name).asSubclass
(View.class);
constructor = aClass.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} catch (Exception e) {
}
}
if (null != constructor) {
try {
return constructor.newInstance(context, attrs);
} catch (Exception e) {
}
}
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
}
- 一个View中,不可能所有属性都需要更改,为了加载速度,我们需要筛选一下,只更改需要更改的属性。
public class SkinAttribute {
//定义需筛选出的参数
private static final List 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");
}
List mSkinViews = new ArrayList<>();
public void load(View view, AttributeSet attributeSet) {
List skinPairs = new ArrayList<>();
for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
//获取属性名
String name = attributeSet.getAttributeName(i);
//是否符合 需要筛选的属性名
if (mAttributes.equals(name)) {
//获取属性值
String attributeValue = attributeSet.getAttributeValue(i);
//写死了 不管了
if (attributeValue.startsWith("#")) {
continue;
}
//资源ID
int resId;
if (attributeValue.startsWith("?")) {
//attr Id 这个是从Theme里拿 获取的Id 是主题的ItemID 所以需要再从itemId转换为colorId
int attrId = Integer.parseInt(attributeValue.substring(1));
//获得 主题 style 中的 对应 attr 的资源id值
resId = SkinThemeUtils.getResId(view.getContext(), new int[attrId])[0];
} else {
//attr Id 这个就是颜色ID
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
SkinPair skinPair = new SkinPair(name, resId);
skinPairs.add(skinPair);
}
}
}
if (!skinPairs.isEmpty()) {
SkinView skinView = new SkinView(view, skinPairs);
mSkinViews.add(skinView);
}
}
//这就View的javaBean 存的属性集合 和对应的View
static class SkinView {
View view;
List skinPairs;
public SkinView(View view, List skinPairs) {
this.view = view;
this.skinPairs = skinPairs;
}
}
//这是View的属性javaBean 比如说TextView的 width=100do;
static class SkinPair {
String attributeName;
int resId;
public SkinPair(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
到第一部分,采集已经结束,接下来是加载皮肤包。
访问 外部资源,需要Assest,访问resId需要Resource,获取资源,缓存皮肤包地址,监听改变等
/**
* 加载皮肤包并更新
*
* @param path 皮肤包路径
*/
public void loadSkin(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
//记录使用默认皮肤
SkinPreference.getInstance().setSkin("");
//清空资源管理器 皮肤资源属性
SkinResources.getInstance().reset();
} else {
try {
//反射创建AssetManager 与 Resource
AssetManager assetManager = AssetManager.class.newInstance();
//资源路径设置 目录或压缩包
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, skinPath);
Resources appResource = mContext.getResources();
//根据当前的显示与配置(横竖屏、语言等)创建Resources
Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics
(), appResource.getConfiguration());
//记录
SkinPreference.getInstance().setSkin(skinPath);
//获取外部Apk(皮肤包) 包名
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager
.GET_ACTIVITIES);
String packageName = info.packageName;
SkinResources.getInstance().applySkin(skinResource, packageName);
} catch (Exception e) {
e.printStackTrace();
}
}
//通知采集的View 更新皮肤
//被观察者改变 通知所有观察者
setChanged();
notifyObservers(null);
加载皮肤包后,我们需要通知改变属性。注册监听。当用户调用则
public void applySkin() {
for (SkinPair skinPair : skinPairs) {
Drawable left = null, top = null, right = null, bottom = null;
switch (skinPair.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPair
.resId);
//Color
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (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;
}
}
}
}
切换主题颜色配置:
SkinThemeUtils:
private static int[] APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS = {
android.support.v7.appcompat.R.attr.colorPrimaryDark
};
private static int[] STATUSBAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr
.navigationBarColor};
public static void updateStatusBarColor(Activity activity) {
//5.0以上才能修改
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
//获得 statusBarColor 与 nanavigationBarColor (状态栏颜色)
//当与 colorPrimaryDark 不同时 以statusBarColor为准
int[] statusBarColorResId = getResId(activity, STATUSBAR_COLOR_ATTRS);
//如果直接在style中写入固定颜色值(而不是 @color/XXX ) 获得0
if (statusBarColorResId[0] != 0) {
activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor
(statusBarColorResId[0]));
} else {
//获得 colorPrimaryDark
int colorPrimaryDarkResId = getResId(activity, APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS)[0];
if (colorPrimaryDarkResId != 0) {
activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor
(colorPrimaryDarkResId));
}
}
if (statusBarColorResId[1] != 0) {
activity.getWindow().setNavigationBarColor(SkinResources.getInstance().getColor
(statusBarColorResId[1]));
}
}
配置字体
依赖库:
attrs.xml
主工程:
styles.xml
- @string/typeface
strings.xml
DNSkin
皮肤包:
assets/font/global.tff
Strings.xml:
font/global.ttf
注:要改的View(不管Textview Button还是其他控件),都是继承View,换肤其实就是换属性,比如说View的TextColor,backGround,src(文字 图片 颜色)等。