Android架构师之动态换肤实现原理详解(从源码分析层层深入)

移动开发 Android
所有的都替换完成后,直接rebuild,拷贝出生成的apk包,可以将名称改为任何你想要的,比如这里我修改为了theme.skin,这就是皮肤包了

[[418584]]

前言

今天我们来聊聊app里动态换肤实现原理

换肤分为动态换肤和静态换肤

一、静态换肤原理

这种换肤的方式,也就是我们所说的内置换肤,就是在APP内部放置多套相同的资源。进行资源的切换。

这种换肤的方式有很多缺点,比如, 灵活性差,只能更换内置的资源、apk体积太大,在我们的应用Apk中等一般图片文件能占到apk大小的一半左右。

当然了,这种方式也并不是一无是处, 比如我们的应用内,只是普通的 日夜间模式 的切换,并不需要图片等的更换,只是更换颜色,那这样的方式就很实用。

二、动态换肤实现原理探讨

动态换肤包括替换图片资源、布局颜色、字体、文字颜色、状态栏和导航栏颜色;

动态换肤步骤包括:

采集需要换肤的控件

加载皮肤包

替换资源

1、皮肤包是什么样的文件

通过解析网易云音乐的皮肤包来理解

通过模拟器下载网易云音乐并更换皮肤。

在设备/data/data/com.netease.cloudmusic/files/theme目录下可以找到我们的皮肤包并cp到电脑上。

修改文件格式为zip,并解压。

经过上述步骤我们得到以下文件

我们可以看到,他的文件内容和我们平时apk的内容格式完全一致;

2、探讨实现原理

从sdk源码中寻找答案,这里只看主要流程

首先Activity的onCreate()方法里面我们都要去调用setContentView(int id) 来指定当前Activity的布局文件:

  1. public void setContentView(@LayoutRes int layoutResID) { 
  2.         // 调用window的setContentView 
  3.         getWindow().setContentView(layoutResID); 
  4.         initWindowDecorActionBar(); 
  5.     } 

Window - PhoneWindow

  1. @Override 
  2.     public void setContentView(int layoutResID) { 
  3.           // 调用 LayoutInflater的inflate 
  4.           mLayoutInflater.inflate(layoutResID, mContentParent); 
  5.     } 

LayoutInflater

  1. @Override 
  2. public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { 
  3.          // Temp is the root view that was found in the xml  创建根布局 
  4.          final View temp = createViewFromTag(root, name, inflaterContext, attrs); 
  5.         // Inflate all children under temp against its context. 创建子布局 最后也是调用createViewFromTag 
  6.         rInflateChildren(parser, temp, attrs, true); 
  7. void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { 
  8.       // 循环调用createViewFromTag创建子布局 
  9.        while (((type = parser.next()) != XmlPullParser.END_TAG ||parser.getDepth() >depth) && type != XmlPullParser.END_DOCUMENT) { 
  10.             final View view = createViewFromTag(parent, name, context, attrs); 
  11.         } 
  12. // 从这个方法中我们看到 尝试通过各种Factory来创建View 
  13. public final View tryCreateView(@Nullable View parent, @NonNull String name,@NonNull Context context,@NonNull AttributeSet attrs) { 
  14.         View view
  15.         if (mFactory2 != null) { 
  16.             view = mFactory2.onCreateView(parent, name, context, attrs); 
  17.         } else if (mFactory != null) { 
  18.             view = mFactory.onCreateView(name, context, attrs); 
  19.         } else { 
  20.             view = null
  21.         } 
  22.         if (view == null && mPrivateFactory != null) { 
  23.             view = mPrivateFactory.onCreateView(parent, name, context, attrs); 
  24.         } 
  25.         return view
  26.     } 
  27. View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) { 
  28.       // 这里是比较重要的地方 
  29.         // 尝试通过Factory来创建View 
  30.         View view = tryCreateView(parent, name, context, attrs); 
  31.         // 如果没有Factory来创建,那么就调用下面方法创建View 
  32.          if (view == null) { 
  33.                if (-1 == name.indexOf('.')) { 
  34.                         // 系统提供的View 不带.的 比如View ,ImageView,TextView 
  35.                         view = onCreateView(context, parent, name, attrs); 
  36.                } else { 
  37.                         // 第三方View或者自定义view 比如com.cbb.xxxView 
  38.                         view = createView(context, namenull, attrs); 
  39.                } 
  40.          }   
  41.     } 
  42. // 看到这里应该比较疑惑 为什么这里只传了android.view. ,很多view明明都不在这个包下 
  43. protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { 
  44.         // 最终还是调用createView , 传入了系统view的全名 
  45.         return createView(name"android.view.", attrs); 
  46.     } 
  47. public final View createView(@NonNull Context viewContext, @NonNull String name,@Nullable String prefix, @Nullable AttributeSet attrs)throws ClassNotFoundException, InflateException { 
  48.           // 缓存中获取View的构造方法 
  49.           Constructor<? extends View> constructor = sConstructorMap.get(name); 
  50.           // 没有缓存则反射获得View的构造方法 并缓存  
  51.           // 需要注意的是 这里使用的view两个参数的构造方法 
  52.           if (constructor == null) { 
  53.                 clazz = Class.forName(prefix != null ? (prefix + name) : namefalse
  54.                         mContext.getClassLoader()).asSubclass(View.class); 
  55.                 constructor = clazz.getConstructor(mConstructorSignature); 
  56.                 constructor.setAccessible(true); 
  57.                 sConstructorMap.put(name, constructor); 
  58.             }     
  59.           // 使用构造方法创建view 
  60.           final View view = constructor.newInstance(args); 
  61.     } 
  62. // sdk提供了设置Factory 的方法 
  63. public void setFactory2(Factory2 factory) { 
  64.   // 这里需要注意mFactorySet 会被如果设置过会被设为true,所以后面我们在设置Factory前需要将其置为false 
  65.    if (mFactorySet) { 
  66.             throw new IllegalStateException("A factory has already been set on this LayoutInflater"); 
  67.    } 
  68.    mFactorySet = true

上述流程分析,我们了解到我们在setContentView开始到创建出view的过程,我们可看到系统在创建view之前会尝试用Factory来创建view,那么我们也可以通过设置自定义Factory来代替系统自带的创建。

上面分析中有个疑问,为什么这里只传了android.view. ,很多view明明都不在这个包下却可以成功创建?这里简单分析一下这个过程

  1. public static LayoutInflater from(Context context) { 
  2.         LayoutInflater LayoutInflater = 
  3.                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 
  4.         return LayoutInflater; 
  5.     } 

上面是LayoutInflater的实例化,我们看到实际返回的是context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

我们从源码中追溯上去

传入的为Activity的context ,Activity的getSystemService(String name)

ContextThemeWrapper的getSystemService(String name)

ContextWrapper中getBaseContext().getSystemService(name)

getBaseContext()返回的mBase

ContextWrapper中的attachBaseContext(Context base)赋值

Activity中的attachBaseContext(context);

Activity中的attach()方法中attachBaseContext(context)

在ActivityThread中performLaunchActivity方法中调用Activity的attach()方法

在ActivityThread中performLaunchActivity方法中ContextImpl appContext = createBaseContextForActivity(r)实例化了Context

ContextImpl中getSystemService(String name)调用SystemServiceRegistry.getSystemService(this, name)返回;

拿着LAYOUT_INFLATER_SERVICE去SystemServiceRegistry中寻找发现返回的是PhoneLayoutInflater类

经过上述步骤我们看到了实际返回的是PhoneLayoutInflater类

  1. public class PhoneLayoutInflater extends LayoutInflater { 
  2.   private static final String[] sClassPrefixList = { 
  3.         "android.widget."
  4.         "android.webkit."
  5.         "android.app." 
  6.     }; 
  7.  @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { 
  8.         for (String prefix : sClassPrefixList) { 
  9.          View view = createView(name, prefix, attrs); 
  10.              if (view != null) { 
  11.                     return view
  12.              } 
  13.         } 
  14.         return super.onCreateView(name, attrs); 
  15.     } 

从上述PhoneLayoutInflater源码中可以看到PhoneLayoutInflater是LayoutInflater的子类,所以实际是拼接的这三个包下的,如果没有则就是原来的view包下。

上述疑问就得到了解决

3、实现view布局的拦截

拦截系统view的创建

  1. public class SkinLayoutFactory implements LayoutInflater.Factory2 { 
  2.     // 包目录列表 
  3.     private static final String[] sClassPrefixList = { 
  4.             "android.widget."
  5.             "android.webkit."
  6.             "android.app."
  7.             "android.view." 
  8.     }; 
  9.     // view构造方法的两个参数 
  10.     private static final Class<?>[] mConstructorSignature = new Class[]{ 
  11.             Context.class, AttributeSet.class}; 
  12.     // 用户缓存已经反射获得的构造方法,防止后续同一个类型的view重复反射 
  13.     private static final HashMap<String, Constructor<? extends View>> sConstructorMap = 
  14.             new HashMap<String, Constructor<? extends View>>(); 
  15.     @Nullable 
  16.     @Override 
  17.     public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { 
  18.         // 创建view 
  19.         View view = createViewFromTag(context, name, attrs); 
  20.         Log.e("Skin""name = " + name + " , view = " + view); 
  21.         return view
  22.     } 
  23.     /** 
  24.      * 创建view 
  25.      * 通过判断是否包含.来确定是否区分两种view类型 
  26.      * 
  27.      * @param name 可能为TextView , 也可能为xxx.xxx.xxxView 
  28.      */ 
  29.     private View createViewFromTag(Context context, String name, AttributeSet attrs) { 
  30.         View view
  31.         if (-1 == name.indexOf('.')) { 
  32.             view = createViewByPkgList(context, name, attrs); 
  33.         } else { 
  34.             view = createView(context, name, attrs); 
  35.         } 
  36.         return view
  37.     } 
  38.     /** 
  39.      * 通过遍历系统包来尝试创建view,如果上个没有创建成功有异常会被catch,然后继续尝试下一个包名来创建 
  40.      * 
  41.      * @param name 可能为TextView 
  42.      */ 
  43.     private View createViewByPkgList(Context context, String name, AttributeSet attrs) { 
  44.         for (String prefix : sClassPrefixList) { 
  45.             try { 
  46.                 View view = createView(context, prefix + name, attrs); 
  47.                 if (view != null) { 
  48.                     return view
  49.                 } 
  50.             } catch (Exception e) { 
  51.                 e.printStackTrace(); 
  52.             } 
  53.         } 
  54.         return null
  55.     } 
  56.     /** 
  57.      * 真正的开始创建view 
  58.      * 
  59.      * @param name name 格式为xxx.xxx.xxxView 
  60.      */ 
  61.     private View createView(Context context, String name, AttributeSet attrs) { 
  62.         Constructor<? extends View> constructor = sConstructorMap.get(name); 
  63.         if (null == constructor) { 
  64.             try { 
  65.                 Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass 
  66.                         (View.class); 
  67.                 constructor = aClass.getConstructor(mConstructorSignature); 
  68.                 sConstructorMap.put(name, constructor); 
  69.             } catch (Exception e) { 
  70.             } 
  71.         } 
  72.         if (null != constructor) { 
  73.             try { 
  74.                 return constructor.newInstance(context, attrs); 
  75.             } catch (Exception e) { 
  76.             } 
  77.         } 
  78.         return null
  79.     } 

上述代码我们基本都是cp的系统源码,从而实现我们自己来创建view,现在我们要开始设置Factory,利用sdk提供的ActivityLifecycleCallbacks的来实现。

  1. // 系统提供的可以监听整个app activity的生命周期 
  2. public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks { 
  3.  @Override 
  4.     public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { 
  5.         // 拿到对应的layoutInflater 创建skinLayoutFactory 并设置进去 
  6.         setFactory2(activity); 
  7.     } 
  8.     /** 
  9.      * 监听到activity 生命周期设置Factory 来拦截系统的view创建 
  10.      * 需要注意的地方为 需要将mFactorySet置为false 
  11.      * 这里有个缺陷 :>28 那么这个属性就不能使用反射来改变了 系统禁止了 
  12.      * 可以考虑直接反射来修改Factory的值 这个系统没有限制 这里没有实践 
  13.      */ 
  14.     private void setFactory2(Activity activity){ 
  15.         LayoutInflater layoutInflater = LayoutInflater.from(activity); 
  16.         try { 
  17.             //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory 
  18.             //如设置过抛出一次 
  19.             //设置 mFactorySet 标签为false 
  20.             Field field = LayoutInflater.class.getDeclaredField("mFactorySet"); 
  21.             field.setAccessible(true); 
  22.             field.setBoolean(layoutInflater, false); 
  23.         } catch (Exception e) { 
  24.             e.printStackTrace(); 
  25.         } 
  26.         SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory(); 
  27.         LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory); 
  28.     } 
  29. public class SkinManager { 
  30.     private static SkinManager instance; 
  31.     private Application application; 
  32.     private SkinActivityLifecycle skinActivityLifecycle; 
  33.     public static void init(Application application) { 
  34.         synchronized (SkinManager.class) { 
  35.             if (null == instance) { 
  36.                 instance = new SkinManager(application); 
  37.             } 
  38.         } 
  39.     } 
  40.     public static SkinManager getInstance() { 
  41.         return instance; 
  42.     } 
  43.     private SkinManager(Application application) { 
  44.         this.application = application; 
  45.         //注册Activity生命周期回调 
  46.         skinActivityLifecycle = new SkinActivityLifecycle(); 
  47.         application.registerActivityLifecycleCallbacks(skinActivityLifecycle); 
  48.     } 

上述完成后,我们运行后可以输出

从这个我们可以我们拦截了系统view的创建来由我们自己创建,至于log输出的这个view列表,大家也应该很熟悉就是DecorView的结构,这里就不做赘述,到了这里我们已经完成了view的创建部分

4、实现换肤

筛选需要的view

上述我们已经拦截了所有的view,实际换肤只需要将需要换肤的view缓存下来就可以了,这里我们通过view的属性来筛选view。

// 只需要设置了这些属性的view

  1. public class SkinAttribute { 
  2. static { 
  3.         mAttributes.add("background"); 
  4.         mAttributes.add("src"); 
  5.         mAttributes.add("textColor"); 
  6.         mAttributes.add("drawableLeft"); 
  7.         mAttributes.add("drawableTop"); 
  8.         mAttributes.add("drawableRight"); 
  9.         mAttributes.add("drawableBottom"); 
  10.     } 
  11.   // 筛选view 
  12.     public void load(View view, AttributeSet attrs) { 
  13.         // 这个view 设置的可以被替换的属性列表 
  14.         List<SkinPair> skinPairs = new ArrayList<>(); 
  15.         for (int i = 0; i < attrs.getAttributeCount(); i++) { 
  16.             //获得属性名 
  17.             String attributeName = attrs.getAttributeName(i); 
  18.             //是否符合 需要筛选的属性名 
  19.             if (mAttributes.contains(attributeName)) { 
  20.                 String attributeValue = attrs.getAttributeValue(i); 
  21.                 // 如果不是通过@符号引用的都不管了 比如?护着#之类的 - 实际?也是可能需要换的,这里为了方便 
  22.                 if (!attributeValue.startsWith("@")) { 
  23.                     continue
  24.                 } 
  25.                 //资源id 
  26.                 int resId = Integer.parseInt(attributeValue.substring(1)); 
  27.                 if (resId != 0) { 
  28.                     //可以被替换的属性 
  29.                     SkinPair skinPair = new SkinPair(attributeName, resId); 
  30.                     skinPairs.add(skinPair); 
  31.                 } 
  32.             } 
  33.         } 
  34.         // 上述已经将这个view需要修改的属性保存进skinPairs了 
  35.         // 判断skinPairs是否为空 ,不为空就将这个view以后属性信息缓存起来 
  36.         if (!skinPairs.isEmpty() || view instanceof TextView) { 
  37.             SkinView skinView = new SkinView(view, skinPairs); 
  38.             // 去修改样式 
  39.             skinView.applySkin(); 
  40.             mSkinViews.add(skinView); 
  41.         } 
  42.     } 
  43.  /** 
  44.      * 遍历view设置样式 
  45.      */ 
  46.     public void applySkin() { 
  47.         for (SkinView mSkinView : mSkinViews) { 
  48.             mSkinView.applySkin(); 
  49.         } 
  50.     } 
  51. // 需要换肤的view和和属性 
  52.     static class SkinView { 
  53.         View view
  54.         List<SkinPair> skinPairs; 
  55.         public SkinView(View view, List<SkinPair> skinPairs) { 
  56.             this.view = view
  57.             this.skinPairs = skinPairs; 
  58.         } 
  59.         // 设置样式  这里都是在皮肤包里面寻找 如果找不到 返回的就是默认的 
  60.         public void applySkin() { 
  61.             for (SkinPair skinPair : skinPairs) { 
  62.                 Drawable left = nulltop = nullright = null, bottom = null
  63.                 switch (skinPair.attributeName) { 
  64.                     case "background"
  65.                         Object background = SkinResources.getInstance().getBackground(skinPair 
  66.                                 .resId); 
  67.                         //Color 
  68.                         if (background instanceof Integer) { 
  69.                             view.setBackgroundColor((Integer) background); 
  70.                         } else { 
  71.                             ViewCompat.setBackground(view, (Drawable) background); 
  72.                         } 
  73.                         break; 
  74.                     case "src"
  75.                         background = SkinResources.getInstance().getBackground(skinPair 
  76.                                 .resId); 
  77.                         if (background instanceof Integer) { 
  78.                             ((ImageView) view).setImageDrawable(new ColorDrawable((Integer
  79.                                     background)); 
  80.                         } else { 
  81.                             ((ImageView) view).setImageDrawable((Drawable) background); 
  82.                         } 
  83.                         break; 
  84.                     case "textColor"
  85.                         ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList 
  86.                                 (skinPair.resId)); 
  87.                         break; 
  88.                     case "drawableLeft"
  89.                         left = SkinResources.getInstance().getDrawable(skinPair.resId); 
  90.                         break; 
  91.                     case "drawableTop"
  92.                         top = SkinResources.getInstance().getDrawable(skinPair.resId); 
  93.                         break; 
  94.                     case "drawableRight"
  95.                         right = SkinResources.getInstance().getDrawable(skinPair.resId); 
  96.                         break; 
  97.                     case "drawableBottom"
  98.                         bottom = SkinResources.getInstance().getDrawable(skinPair.resId); 
  99.                         break; 
  100.                     default
  101.                         break; 
  102.                 } 
  103.                 if (null != left || null != right || null != top || null != bottom) { 
  104.                     ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(lefttopright
  105.                             bottom); 
  106.                 } 
  107.             } 
  108.         } 
  109.     } 
  110. // 用于保存属性名称和id 
  111.     static class SkinPair { 
  112.         String attributeName; 
  113.         int resId; 
  114.         public SkinPair(String attributeName, int resId) { 
  115.             this.attributeName = attributeName; 
  116.             this.resId = resId; 
  117.         } 
  118.     } 

下面我们在创建view的地方进行筛选并缓存

  1. public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer { 
  2. public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { 
  3.         // 创建view 
  4.         View view = createViewFromTag(context, name, attrs); 
  5.         Log.e("Skin""name = " + name + " , view = " + view); 
  6.         //筛选符合属性的View 
  7.         skinAttribute.load(view, attrs); 
  8.         return view
  9.     } 

到这里,我们创建view的时候通过SkinAttribute类我们可以筛选出可能需要更换皮肤的view,然后保存了每个view和其属性的对关系,我们需要替换的时候就遍历缓存的view然后重新设置的对应属性的。

5、制作皮肤包

新建一个Android project/module

将需要替换的颜色或者图片拷贝的项目中,需注意和原来项目的中的名称要一致。

所有的都替换完成后,直接rebuild,拷贝出生成的apk包

可以将名称改为任何你想要的,比如这里我修改为了theme.skin,这就是皮肤包了

将其拷贝近手机文件下 - 实际应用中应该网络下载之类的

6、加载皮肤包

  1. public class SkinManager extends Observable { 
  2.  /** 
  3.      * 使用皮肤包 
  4.      * 
  5.      * @param path 皮肤包地址 
  6.      */ 
  7.     public void loadSkin(String path) { 
  8.         if (TextUtils.isEmpty(path)) { 
  9.             // 传入空 用默认的 
  10.             SkinPreference.getInstance().setSkin(""); 
  11.             SkinResources.getInstance().reset(); 
  12.         } else { 
  13.             try { 
  14.                 AssetManager assetManager = AssetManager.class.newInstance(); 
  15.                 // 添加资源进入资源管理器 
  16.                 Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String 
  17.                         .class); 
  18.                 addAssetPath.setAccessible(true); 
  19.                 addAssetPath.invoke(assetManager, path); 
  20.                 // 系统resources 
  21.                 Resources resources = application.getResources(); 
  22.                 // 外部资源 sResource 
  23.                 Resources sResource= new Resources(assetManager, resources.getDisplayMetrics(), 
  24.                         resources.getConfiguration()); 
  25.                 //获取外部Apk(皮肤包) 包名 
  26.                 PackageManager mPm = application.getPackageManager(); 
  27.                 PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager 
  28.                         .GET_ACTIVITIES); 
  29.                 String packageName = info.packageName; 
  30.                 // 皮肤包资源传入工具类SkinResources中方便后续查找 
  31.                 SkinResources.getInstance().applySkin(sResource, packageName); 
  32.                 //保存当前使用的皮肤包 
  33.                 SkinPreference.getInstance().setSkin(path); 
  34.             } catch (Exception e) { 
  35.                 e.printStackTrace(); 
  36.             } 
  37.         } 
  38.         //通知观察者 
  39.         setChanged(); 
  40.         notifyObservers(); 
  41.     } 

上面代码将theme.skin加载了,SkinResources是一个工具类,这里传入了传入了外部皮肤包的Resources,举例作用如下

  1. // 根据本app中的资源id寻找皮肤包中的资源id 
  2.  public int getIdentifier(int resId) { 
  3.         if (isDefaultSkin) { 
  4.             return resId; 
  5.         } 
  6.         //在皮肤包中不一定就是 当前程序的 id 
  7.         //获取对应id 在当前的名称 colorPrimary 
  8.         // 所以要先获取当前名称和类型 再去皮肤包中查找对应的id 
  9.         String resName = mAppResources.getResourceEntryName(resId); 
  10.         String resType = mAppResources.getResourceTypeName(resId); 
  11.         int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName); 
  12.         return skinId; 
  13.     } 
  14. // 根据资源id获得颜色 
  15. public int getColor(int resId) { 
  16.         // 如果显示默认皮肤 就返回默认的 
  17.         if (isDefaultSkin) { 
  18.             return mAppResources.getColor(resId); 
  19.         } 
  20.         // 获得在皮肤包的资源id  两个包中的统一名称资源可能id不一样 
  21.         int skinId = getIdentifier(resId); 
  22.         if (skinId == 0) { 
  23.         // 返回皮肤包中的资源 
  24.             return mAppResources.getColor(resId); 
  25.         } 
  26.         return mSkinResources.getColor(skinId); 
  27.     } 

这里加载完皮肤包后,我们需要做的就是通知到view去更新,这里代码就不贴出来了

已经有的页面通知SkinAttribute调用applySkin()去遍历已经缓存的view去设置

后续打开的页面,包括退出重新进入app,那么就要在SkinLayoutFactory调用onCreateView创建view的时候调用SkinAttribute的load方法去设置

至此我们可以实现一些基本的功能,测试一波

这里已经成功的实现了换肤

总结

动态换肤步骤:

采集需要换肤的控件

加载皮肤包

替换资源

责任编辑:武晓燕 来源: Android开发编程
相关推荐

2019-12-11 11:04:22

HTTPS HTTP网络协议

2019-12-03 10:58:58

HTTPS证书网站

2021-06-09 06:24:03

java垃圾回收机Java语言

2021-08-18 07:56:04

AndroidRecyclerVie复用

2018-10-31 15:54:47

Java线程池源码

2021-09-07 06:40:25

AndroidLiveData原理

2018-09-13 15:00:51

JavaHashMap架构师

2021-09-08 06:51:52

AndroidRetrofit原理

2021-10-15 09:19:17

AndroidSharedPrefe分析源码

2021-09-10 07:31:54

AndroidAppStartup原理

2021-09-01 06:48:16

AndroidGlide缓存

2021-10-25 09:41:04

架构运维技术

2018-07-03 15:46:24

Java架构师源码

2019-12-13 09:00:58

架构运维技术

2020-11-03 09:10:18

JUC-Future

2020-01-14 14:37:29

JVMJava体系

2022-06-15 10:04:51

存储选型MySQL

2011-06-23 14:27:48

QT QLibrary 动态库

2019-09-27 09:56:31

软件技术硬件

2021-07-12 23:43:46

AppAndroid优化
点赞
收藏

51CTO技术栈公众号