赞
踩
此文来自阿钟的投稿,阅读大约15分钟
实现的效果图
动态换肤一般实现的原理
对页面需要换肤的View进行标记
在Activity#setContentView()
加载view时获取到标记的view(后面会说是要怎么获取到)
创建一个Library项目制作我们的皮肤包(res下的资源名称需要与app使用的一致,换肤就是通过使用的资源名称去皮肤包
里加载相同名字的资源)
创建皮肤包对应的Resources
对象(用于加载皮肤包内的资源)
点击换肤将我们标记的View的一些属性上设置的值修改为皮肤包里的值,这样就达到换肤的效果
这一步是相对简单的,只要自定义一个属性即可;在获取View的时候判断有无这个属性 有就将这个view存起来
- 1<?xml version="1.0" encoding="utf-8"?>
- 2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- 3 xmlns:skin="http://schemas.android.com/apk/azhon-skin"
- 4 xmlns:tools="http://schemas.android.com/tools"
- 5 android:layout_width="match_parent"
- 6 android:layout_height="match_parent"
- 7 android:orientation="vertical"
- 8 android:padding="16dp"
- 9 tools:context=".MainActivity">
- 10 <TextView
- 11 android:layout_width="wrap_content"
- 12 android:layout_height="wrap_content"
- 13 android:background="@color/bg_1"
- 14 android:text="我是一个TextView"
- 15 android:textColor="@color/title_1"
- 16 android:textSize="16sp"
- 17 skin:enable="true" />
- 18 <Button
- 19 android:id="@+id/btn_dark"
- 20 android:layout_width="match_parent"
- 21 android:layout_height="wrap_content"
- 22 android:layout_marginTop="20dp"
- 23 android:background="@color/bg_2"
- 24 android:text="@string/btn_text"
- 25 android:textColor="@color/title_2"
- 26 skin:enable="true" />
- 27 <Button
- 28 android:id="@+id/btn_default"
- 29 android:layout_width="match_parent"
- 30 android:layout_height="wrap_content"
- 31 android:layout_marginTop="20dp"
- 32 android:background="@color/bg_3"
- 33 android:text="@string/btn_reset_text"
- 34 android:textColor="@color/title_3" />
- 35</LinearLayout>

skin:enable="true"
这个就是自定的一个属性取值为
boolean,如果为
true就表示在换肤的时候需要去皮肤包加载对应的资源 ¨K15K **这里使用的是自定义布局加载器
LayoutInflater的
LayoutInflater.Factory2 `来监听View的创建;下面我们来通过阅读源码来具体说一下为什么使用的这个:**
查看AppCompatActivity的setContentView()方法
- 1public class MainActivity extends AppCompatActivity {
- 2 @Override
- 3 protected void onCreate(Bundle savedInstanceState) {
- 4 super.onCreate(savedInstanceState);
- 5 setContentView(R.layout.activity_main);
- 6}
接着继续调用了getDelegate()的setContentView()方法
- 1// AppCompatActivity.java
- 2@Override
- 3public void setContentView(@LayoutRes int layoutResID) {
- 4 getDelegate().setContentView(layoutResID);
- 5}
getDelegate()获取到的是AppCompatDelegate这个抽象类的实现类,而他的实现类就只有一个AppCompatDelegateImpl
接着调用了AppCompatDelegateImpl的setContentView()
- 1// AppCompatDelegateImpl.java
- 2@Override
- 3public void setContentView(int resId) {
- 4 ensureSubDecor();
- 5 ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
- 6 contentParent.removeAllViews();
- 7 //重点就是这样代码,通过布局加载器加载xml文件
- 8 LayoutInflater.from(mContext).inflate(resId, contentParent);
- 9 mAppCompatWindowCallback.getWrapped().onContentChanged();
- 10}
阅读到这里就可以看到有用的代码了
LayoutInflater.from(mContext).inflate(resId, contentParent)加载我们的xml布局文件,他传入了我们的布局资源
id`和android.R.id.content这个ViewGroup;有了解过Activity的布局层次结构的同学肯定就知道是什么了。
接着往下看LayoutInflater的inflate()方法
- 1// LayoutInflater.java
- 2//No.1
- 3public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
- 4 return inflate(resource, root, root != null);
- 5}
- 6//No.2 接着调用了
- 7public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
- 8 final Resources res = getContext().getResources();
- 9 if (DEBUG) {
- 10 Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
- 11 + Integer.toHexString(resource) + ")");
- 12 }
- 13 final XmlResourceParser parser = res.getLayout(resource);
- 14 try {
- 15 return inflate(parser, root, attachToRoot);
- 16 } finally {
- 17 parser.close();
- 18 }
- 19}
- 20//No.3 接着调用了
- 21public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
- 22 synchronized (mConstructorArgs) {
- 23 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
- 24 // 省略若干源代码....
- 25 if (TAG_MERGE.equals(name)) {
- 26 if (root == null || !attachToRoot) {
- 27 throw new InflateException("<merge /> can be used only with a valid "
- 28 + "ViewGroup root and attachToRoot=true");
- 29 }
- 30 rInflate(parser, root, inflaterContext, attrs, false);
- 31 } else {
- 32 // Temp is the root view that was found in the xml
- 33 final View temp = createViewFromTag(root, name, inflaterContext, attrs);
- 34 ViewGroup.LayoutParams params = null;
- 35
- 36 // 省略若干源代码....
- 37
- 38 }
- 39 }
- 40}

调用inflate()最终调用了
createViewFromTag()
这个方法根据布局写的代码开始创建对应的View实体,继续向下查看createViewFromTag()
的代码
- 1// LayoutInflater.java
- 2// No.1
- 3private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
- 4 return createViewFromTag(parent, name, context, attrs, false);
- 5}
- 6// No.2
- 7View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
- 8 boolean ignoreThemeAttr) {
- 9
- 10 // 省略若干源代码....
- 11
- 12 try {
- 13 View view;
- 14 if (mFactory2 != null) {
- 15 view = mFactory2.onCreateView(parent, name, context, attrs);
- 16 } else if (mFactory != null) {
- 17 view = mFactory.onCreateView(name, context, attrs);
- 18 } else {
- 19 view = null;
- 20 }
- 21 if (view == null && mPrivateFactory != null) {
- 22 view = mPrivateFactory.onCreateView(parent, name, context, attrs);
- 23 }
- 24 if (view == null) {
- 25 final Object lastContext = mConstructorArgs[0];
- 26 mConstructorArgs[0] = context;
- 27 try {
- 28 if (-1 == name.indexOf('.')) {
- 29 view = onCreateView(parent, name, attrs);
- 30 } else {
- 31 view = createView(name, null, attrs);
- 32 }
- 33 } finally {
- 34 mConstructorArgs[0] = lastContext;
- 35 }
- 36 }
- 37 return view;
- 38 } catch (InflateException e) {
- 39 throw e;
- 40 }
- 41 // 省略若干源代码....
- 42}

代码查看到这里终于看到了开头所说的Factory
这个东西,上面代码最终通过调用onCreateView()来创建view;所以我们只需要对LayoutInflater
设置一个Factory
即可。
先来看看设置setFactory()的方法
- 1// LayoutInflater.java
- 2public void setFactory(Factory factory) {
- 3 if (mFactorySet) {
- 4 throw new IllegalStateException("A factory has already been set on this LayoutInflater");
- 5 }
- 6 if (factory == null) {
- 7 throw new NullPointerException("Given factory can not be null");
- 8 }
- 9 mFactorySet = true;
- 10 if (mFactory == null) {
- 11 mFactory = factory;
- 12 } else {
- 13 mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
- 14 }
- 15}
可以很清楚的看到,如果我们调用了这个方法那么肯定会抛出一个异常IllegalStateException ,A factory has already been set on this LayoutInflater
,所以设置之前我们需要通过反射将mFactorySet
这个变量置为false
需要注意的一点:
既然是干预View的加载创建,那肯定设置Factory
需要在LayoutInflater
实例创建之后,在加载创建View之前;而Activity是通过setContentView()
加载View所以设置Factory
需要在setContentView()
之前;这里可以通过Application
设置Activity的生命周期监听器,即
registerActivityLifecycleCallbacks()
上面bb了一堆现在来上代码了
- 1public class App extends Application {
- 2 @Override
- 3 public void onCreate() {
- 4 super.onCreate();
- 5 registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
- 6 @Override
- 7 public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
- 8 setFactory(activity);
- 9 }
- 10 @Override
- 11 public void onActivityStarted(Activity activity) {
- 12 }
- 13 @Override
- 14 public void onActivityResumed(Activity activity) {
- 15 }
- 16 @Override
- 17 public void onActivityPaused(Activity activity) {
- 18 }
- 19 @Override
- 20 public void onActivityStopped(Activity activity) {
- 21 }
- 22 @Override
- 23 public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
- 24 }
- 25 @Override
- 26 public void onActivityDestroyed(Activity activity) {
- 27 }
- 28 });
- 29 }
- 30}

创建SkinFactory.java
- 1public final class SkinFactory implements LayoutInflater.Factory2 {
- 2 private static final String TAG = "SkinFactory";
- 3 private static final String[] classPrefixList = {"android.view.", "android.widget.", "android.webkit."};
- 4 private static final String NAME_SPACE = "http://schemas.android.com/apk/azhon-skin";
- 5 private static final String ATTRIBUTE = "enable";
- 6 @Override
- 7 public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
- 8
- 9 //找到布局使用属性(skin:enable="true")标记需要换肤的view
- 10 boolean isSkinView = attrs.getAttributeBooleanValue(NAME_SPACE, ATTRIBUTE, false);
- 11 //如果不是换肤的View就直接不处理
- 12 if (!isSkinView) return null;
- 13 View view = null;
- 14 //name不包含.的说明是系统的控件
- 15 if (-1 == name.indexOf('.')) {
- 16 for (String prefix : classPrefixList) {
- 17 view = createView(name, prefix, context, attrs);
- 18 if (view != null) break;
- 19 }
- 20 } else {
- 21 view = createView(name, null, context, attrs);
- 22 }
- 23 LogUtil.d(TAG, "onCreateView: 加载换肤View成功..." + view);
- 24 return view;
- 25 }
- 26
- 27 /**
- 28 * 创建系统自带View
- 29 */
- 30 private View createView(String name, String prefix, Context context, AttributeSet attrs) {
- 31 View view = null;
- 32 try {
- 33 view = LayoutInflater.from(context).createView(name, prefix, attrs);
- 34 } catch (ClassNotFoundException e) {
- 35 //
- 36 }
- 37 return view;
- 38 }
- 39 @Override
- 40 public View onCreateView(String name, Context context, AttributeSet attrs) {
- 41 return null;
- 42 }
- 43}

设置Factory
- 1/**
- 2 * 设置布局解析Factory
- 3 * 需要将LayoutInflater的mFactorySet变量设置为false
- 4 */
- 5private void setFactory(Activity activity) {
- 6 try {
- 7 LayoutInflater inflater = activity.getLayoutInflater();
- 8 Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
- 9 field.setAccessible(true);
- 10 field.setBoolean(inflater, false);
- 11 //设置自己的Factory
- 12 LayoutInflaterCompat.setFactory2(inflater, new SkinFactory());
- 13 } catch (Exception e) {
- 14 e.printStackTrace();
- 15 }
- 16}

在SkinFactory#onCreateView()中就可以获取到我们标记的View了,这里需要保存换肤的View,需要替换的属性和属性的值
app默认的颜色资源
对应的皮肤包如下:
作为皮肤包只需要res
目录可以将java的目录代码全部删除
皮肤包中定义的资源名称必须与主app定义的一模一样
然后通过AS的菜单——>Build——>Build Bundle(s) / APK(s)——> Build APK(s)
就可以打包出来了
创建Resources对象
- 1/**
- 2 * 创建皮肤包的Resources
- 3 *
- 4 * @param path 皮肤包路径
- 5 */
- 6public void createResources(Context context, String path) {
- 7 try {
- 8 AssetManager assetManager = AssetManager.class.newInstance();
- 9 Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
- 10 addAssetPath.invoke(assetManager, path);
- 11 Resources resources = context.getResources();
- 12 //创建对象
- 13 Resources skinResources = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
- 14 //获取皮肤包(也就是apk)的包名
- 15 PackageManager packageManager = context.getPackageManager();
- 16 PackageInfo packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
- 17 String skinPackageName = packageInfo.packageName;
- 18 } catch (Exception e) {
- 19 e.printStackTrace();
- 20 }
- 21}

path 就是皮肤包路径了
/sdcard/Android/data/com.azhon.dynamicskin/cache/dark.skin
通过PackageManager
获取皮肤包的包名,包名在获取皮肤包内的资源时会用到
我们需要替换这个TextView的background
,textColor
这两个属性
- 1<TextView
- 2 android:layout_width="wrap_content"
- 3 android:layout_height="wrap_content"
- 4 android:background="@color/bg_1"
- 5 android:text="我是一个TextView"
- 6 android:textColor="@color/title_1"
- 7 android:textSize="16sp"
- 8 skin:enable="true" />
在自定义的SkinFactory
中就可以获取每一个属性和属性对应的值,如下:
这里的@开头值后面的数字就是res下的资源对应的Id(也是就是R文件的Id)
先介绍一个重要的(api)方法
int resId = resources.getIdentifier(String name, String defType, String defPackage);
第一个参数:资源的名字,例如:bg_1、titile_1
第二个参数:资源类型,例如:drawable、color、string
第三个参数:resources资源对应的包名
根据资源id加载皮肤包内对应的资源
封装的方法
- 1/**
- 2 * 根据资源Id获取资源的名称
- 3 * @param resources app自身的资源对象
- 4 * @param skinResources 皮肤包创建的资源对象
- 5 * @param id 当前使用的资源id
- 6 */
- 7public static int getResourcesIdByName(Resources resources,Resources skinResources, String packageName, int id) {
- 8 String[] res = getResourcesById(resources, id);
- 9 //使用皮肤包创建的Resources加载资源
- 10 return skinResources.getIdentifier(res[0], res[1], packageName);
- 11}
- 12
- 13/**
- 14 * 根据资源Id获取资源的名称
- 15 *
- 16 * @param id 资源id
- 17 * @return 资源名称
- 18 */
- 19public static String[] getResourcesById(Resources resources, int id) {
- 20 String entryName = resources.getResourceEntryName(id);
- 21 String typeName = resources.getResourceTypeName(id);
- 22 return new String[]{entryName, typeName};
- 23}

获取对应皮肤包内的资源id(2130968664就是获取到的资源id)
1int skinResId = getResourcesIdByName(context.getResources(),skinResources,skinPackageName,2130968664);
获取到了资源的id,但是这个值是不能直接使用的需要在进一步操作
上面通过getResourcesById()
这个方法知道了这个资源id是属于color
类型的了,所以只要在调用一次getColor
即可
1int color = skinResources.getColor(skinResId);
通过上面几步就成功的拿到了皮肤包内对应的资源,最后就只要调用TextView的setTextColor(color)就可以成功的替换文字的颜色了,同理替换background也是一样的。
Resources也还提供了许多其它的方法:
Demo下载:
https://gitee.com/azhon/Resources/blob/master/DynamicSkin.zip
需要将项目根目录的 dark.skin 文件拷贝至
/sdcard/Android/data/com.azhon.dynamicskin/cache/目录下 ¨K32K <ul> <li>干预View的加载创建,
Factory的原理和使用</li> <li>对一个apk包创建对应的
Resources对象,
AssetManager、
PackageManager的使用</li> <li>加载apk包内的资源,
Resources`的使用
我创建了一个关于Android的交流群,有兴趣可以加我微信我拉你
如果感觉现在的网络技术文章质量不高,苦于自己的Android技术无法得到明显的提升,感叹没有一帮好的学习伙伴及道友,那么我的知识星球可能就是一片净土,好的学习气氛,更好的技术资源与文章,自由且高效率,快来吧。
点击阅读原文,获得更多精彩内容
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。