当前位置:   article > 正文

Android 动态换肤框架原理_android 主题动态换肤

android 主题动态换肤

1.  Android 系统 PhoneWindow 源码阅读

 1.1.  Activity实例化 PhoneWindow

  1. Activity:
  2. final void attach(Context context, ActivityThread aThread,
  3. Instrumentation instr, IBinder token, int ident,
  4. Application application, Intent intent, ActivityInfo info,
  5. CharSequence title, Activity parent, String id,
  6. NonConfigurationInstances lastNonConfigurationInstances,
  7. Configuration config, String referrer, IVoiceInteractor voiceInteractor,
  8. Window window, ActivityConfigCallback activityConfigCallback) {
  9. attachBaseContext(context);
  10. mFragments.attachHost(null /*parent*/);
  11. // Activity 中通过 PhoneWindow 加载布局,PhoneWindow AndrodStudio源码无法查看
  12. // 上这里去看 http://androidxref.com/
  13. mWindow = new PhoneWindow(this, window, activityConfigCallback);
  14. mWindow.setWindowControllerCallback(this);
  15. mWindow.setCallback(this);
  16. mWindow.setOnWindowDismissedCallback(this);
  17. mWindow.getLayoutInflater().setPrivateFactory(this);
  18. if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
  19. mWindow.setSoftInputMode(info.softInputMode);
  20. }
  21. if (info.uiOptions != 0) {
  22. mWindow.setUiOptions(info.uiOptions);
  23. }
  24. mUiThread = Thread.currentThread();
  25. mMainThread = aThread;
  26. mInstrumentation = instr;
  27. mToken = token;
  28. mIdent = ident;
  29. mApplication = application;
  30. mIntent = intent;
  31. mReferrer = referrer;
  32. mComponent = intent.getComponent();
  33. mActivityInfo = info;
  34. mTitle = title;
  35. mParent = parent;
  36. mEmbeddedID = id;
  37. mLastNonConfigurationInstances = lastNonConfigurationInstances;
  38. if (voiceInteractor != null) {
  39. if (lastNonConfigurationInstances != null) {
  40. mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
  41. } else {
  42. mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
  43. Looper.myLooper());
  44. }
  45. }
  46. mWindow.setWindowManager(
  47. (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
  48. mToken, mComponent.flattenToString(),
  49. (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
  50. if (mParent != null) {
  51. mWindow.setContainer(mParent.getWindow());
  52. }
  53. mWindowManager = mWindow.getWindowManager();
  54. mCurrentConfig = config;
  55. mWindow.setColorMode(info.colorMode);
  56. }

1.2.  PhoneWindow 中 内部 DecorView

  1. private DecorView mDecor;
  2. //
  3. int layoutResource;
  4. // 做一系列判断,去加载系统的 layout资源文件
  5. if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
  6. layoutResource = R.layout.screen_swipe_dismiss;
  7. setCloseOnSwipeEnabled(true);
  8. }
  9. ......
  10. // 把系统布局加入到 DecorView 中
  11. // 系统布局 是一个 FrameLayout的 ViewGroup
  12. // id 是 android.R.id.content 叫做 mContentParent
  13. mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
  14. // 自己调用setContenView 布局位置,自己的布局 Activity

内部结构: 

2.  分析 ImageView 拦截

// 继承Activity,那么 返回 ImageView ,继承 AppCompatActivity 返回  AppCompatImageView

为什么?  在 AppCompatImageView 中 ImageView被替换成 AppCompatImageView, 看源码

2.1 .AppCompatActivity  源码   getDelegate

  1. @Override
  2. public void setContentView(@LayoutRes int layoutResID) {
  3. getDelegate().setContentView(layoutResID);
  4. }
  5. @NonNull
  6. public AppCompatDelegate getDelegate() {
  7. if (mDelegate == null) {
  8. mDelegate = AppCompatDelegate.create(this, this);
  9. }
  10. return mDelegate;
  11. }

2.2.  AppCompatDelegate 最终实例化的是 AppCompatDelegateImplV9

  1. private static AppCompatDelegate create(Context context, Window window,
  2. AppCompatCallback callback) {
  3. final int sdk = Build.VERSION.SDK_INT;
  4. if (BuildCompat.isAtLeastN()) {
  5. return new AppCompatDelegateImplN(context, window, callback);
  6. } else if (sdk >= 23) {
  7. return new AppCompatDelegateImplV23(context, window, callback);
  8. } else if (sdk >= 14) {
  9. return new AppCompatDelegateImplV14(context, window, callback);
  10. } else if (sdk >= 11) {
  11. return new AppCompatDelegateImplV11(context, window, callback);
  12. } else {
  13. return new AppCompatDelegateImplV9(context, window, callback);
  14. }
  15. }

最终看 AppCompatDelegateImplV9 的 createView

核心 LayoutInflaterCompat.setFactory2(layoutInflater, this)   那么 this  实现 factory2接口  重写factory2接口方法

  1. public static void setFactory2(
  2. @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
  3. IMPL.setFactory2(inflater, factory);
  4. }
  5. public interface Factory2 extends Factory {
  6. /**
  7. * Version of {@link #onCreateView(String, Context, AttributeSet)}
  8. * that also supplies the parent that the view created view will be
  9. * placed in.
  10. *
  11. * @param parent The parent that the created view will be placed
  12. * in; <em>note that this may be null</em>.
  13. * @param name Tag name to be inflated.
  14. * @param context The context the view is being created in.
  15. * @param attrs Inflation attributes as specified in XML file.
  16. *
  17. * @return View Newly created view. Return null for the default
  18. * behavior.
  19. */
  20. public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
  21. }

 AppCompatDelegateImplV9 的  createView方法

  1. @Override
  2. public View createView(View parent, final String name, @NonNull Context context,
  3. @NonNull AttributeSet attrs) {
  4. if (mAppCompatViewInflater == null) {
  5. mAppCompatViewInflater = new AppCompatViewInflater();
  6. }
  7. boolean inheritContext = false;
  8. if (IS_PRE_LOLLIPOP) {
  9. inheritContext = (attrs instanceof XmlPullParser)
  10. // If we have a XmlPullParser, we can detect where we are in the layout
  11. ? ((XmlPullParser) attrs).getDepth() > 1
  12. // Otherwise we have to use the old heuristic
  13. : shouldInheritContext((ViewParent) parent);
  14. }
  15. return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
  16. IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
  17. true, /* Read read app:theme as a fallback at all times for legacy reasons */
  18. VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
  19. );
  20. // LayoutInflaterCompat.setFactory2(layoutInflater, this);
  21. // 那么 this 就是 setFactory2(
  22. // @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory)
  23. // IMPL.setFactory2(inflater, factory) 的实现
  24. //
  25. @Override
  26. public void installViewFactory() {
  27. LayoutInflater layoutInflater = LayoutInflater.from(mContext);
  28. if (layoutInflater.getFactory() == null) {
  29. LayoutInflaterCompat.setFactory2(layoutInflater, this);
  30. } else {
  31. if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
  32. Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
  33. + " so we can not install AppCompat's");
  34. }
  35. }
  36. }
  37. }

AppCompatDelegateImplV9  中重写 factory2的 createView方法, 这里回调,等待实例化 factory2  调用 createView

 AppCompatDelegateImplV9 的  createView方法 内部 AppCompatViewInflater 调用  createView  控件 替换 如何下

  1. public final View createView(View parent, final String name, @NonNull Context context,
  2. @NonNull AttributeSet attrs, boolean inheritContext,
  3. boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
  4. final Context originalContext = context;
  5. // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
  6. // by using the parent's context
  7. if (inheritContext && parent != null) {
  8. context = parent.getContext();
  9. }
  10. if (readAndroidTheme || readAppTheme) {
  11. // We then apply the theme on the context, if specified
  12. context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
  13. }
  14. if (wrapContext) {
  15. context = TintContextWrapper.wrap(context);
  16. }
  17. View view = null;
  18. // We need to 'inject' our tint aware Views in place of the standard framework versions
  19. switch (name) {
  20. case "TextView":
  21. view = new AppCompatTextView(context, attrs);
  22. break;
  23. case "ImageView":
  24. view = new AppCompatImageView(context, attrs);
  25. }
  26. }
  27. @Override
  28. public void setContentView(int resId) {
  29. ensureSubDecor();
  30. ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
  31. contentParent.removeAllViews();
  32. // 这里是单例
  33. LayoutInflater.from(mContext).inflate(resId, contentParent);
  34. mOriginalWindowCallback.onContentChanged();
  35. }

2.4.  那么什么时候  实例化 factory2  调用 createView,AppCompatDelegateImplV9  中重写 factory2的 createView方法 什么时候调用 ??  看  LayoutInflater 源码 

创建标签的时候 mFactory2.onCreateView, 那么 在 AppCompatViewInflater中的 createView被 调用,替换创建的标签,上面是钩子

  1. public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
  2. final Resources res = getContext().getResources();
  3. if (DEBUG) {
  4. Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
  5. + Integer.toHexString(resource) + ")");
  6. }
  7. final XmlResourceParser parser = res.getLayout(resource);
  8. try {
  9. // 开始解析代码
  10. return inflate(parser, root, attachToRoot);
  11. } finally {
  12. parser.close();
  13. }
  14. }
  15. public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  16. // 解析 标签
  17. rInflate(parser, root, inflaterContext, attrs, false);
  18. }
  19. // 核心代码区
  20. void rInflate(XmlPullParser parser, View parent, Context context,
  21. AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
  22. //
  23. final View view = createViewFromTag(parent, name, context, attrs);
  24. }
  25. // 根据反射创建标签
  26. View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
  27. boolean ignoreThemeAttr) {
  28. // AppCompatViewInflater设置了 mFactory!=null
  29. try {
  30. View view;
  31. if (mFactory2 != null) {
  32. // 这里调用了 mFactory2.onCreateView 内部实现是
  33. // AppCompatViewInflater 中 createView 方法实现 拦截,拦截View的 创建
  34. view = mFactory2.onCreateView(parent, name, context, attrs);
  35. } else if (mFactory != null) {
  36. view = mFactory.onCreateView(name, context, attrs);
  37. } else {
  38. view = null;
  39. }
  40. if (view == null && mPrivateFactory != null) {
  41. view = mPrivateFactory.onCreateView(parent, name, context, attrs);
  42. }
  43. if (view == null) {
  44. final Object lastContext = mConstructorArgs[0];
  45. mConstructorArgs[0] = context;
  46. try {
  47. // 系统的View
  48. if (-1 == name.indexOf('.')) {
  49. view = onCreateView(parent, name, attrs);
  50. } else {
  51. // 表示自定义 View
  52. view = createView(name, null, attrs);
  53. }
  54. } finally {
  55. mConstructorArgs[0] = lastContext;
  56. }
  57. }
  58. return view;
  59. } catch (InflateException e) {
  60. throw e;
  61. }
  62. }
  63. }
  64. public final View createView(String name, String prefix, AttributeSet attrs)
  65. throws ClassNotFoundException, InflateException {
  66. Constructor<? extends View> constructor = sConstructorMap.get(name);
  67. if (constructor != null && !verifyClassLoader(constructor)) {
  68. constructor = null;
  69. sConstructorMap.remove(name);
  70. }
  71. Class<? extends View> clazz = null;
  72. try {
  73. Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
  74. // 从 构造器中 获取View,如果存在,那么直接获取 ,
  75. // 不存在,通过反射创建 View ,存入 Map中 , 比如ImageView创建一次即可
  76. if (constructor == null) {
  77. // Class not found in the cache, see if it's real, and try to add it
  78. clazz = mContext.getClassLoader().loadClass(
  79. prefix != null ? (prefix + name) : name).asSubclass(View.class);
  80. if (mFilter != null && clazz != null) {
  81. boolean allowed = mFilter.onLoadClass(clazz);
  82. if (!allowed) {
  83. failNotAllowed(name, prefix, attrs);
  84. }
  85. }
  86. constructor = clazz.getConstructor(mConstructorSignature);
  87. constructor.setAccessible(true);
  88. sConstructorMap.put(name, constructor);
  89. } else {
  90. // If we have a filter, apply it to cached constructor
  91. if (mFilter != null) {
  92. // Have we seen this name before?
  93. Boolean allowedState = mFilterMap.get(name);
  94. if (allowedState == null) {
  95. // New class -- remember whether it is allowed
  96. clazz = mContext.getClassLoader().loadClass(
  97. prefix != null ? (prefix + name) : name).asSubclass(View.class);
  98. boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
  99. mFilterMap.put(name, allowed);
  100. if (!allowed) {
  101. failNotAllowed(name, prefix, attrs);
  102. }
  103. } else if (allowedState.equals(Boolean.FALSE)) {
  104. failNotAllowed(name, prefix, attrs);
  105. }
  106. }
  107. }
  108. }

     LayoutInflater 中加载  来源, 是系统的一个服务,系统服务注册 , SystemServiceRegistry:

  1. // 获取系统服务的时候从 Map中获取即可
  2. // 系统服务 SYSTEM_SERVICE_NAMES 保存到这个Map中
  3. registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
  4. new CachedServiceFetcher<LayoutInflater>() {
  5. @Override
  6. public LayoutInflater createService(ContextImpl ctx) {
  7. return new PhoneLayoutInflater(ctx.getOuterContext());
  8. }});
  9. private static <T> void registerService(String serviceName, Class<T> serviceClass,
  10. ServiceFetcher<T> serviceFetcher) {
  11. SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
  12. SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
  13. }

动态换肤: 既然上面  AppCompatActivity 可以把ImageView 替换 AppCompatImageView,通过设置, 
那么我们也可以重写factory  来拦截View 的创建, 实现动态换肤恭喜

3  动态换肤原理:

 换肤原理:

任何一个apk中,资源文件  color , mip , drawable ....  
android:textColor="?android:colorPrimary"
android:textColor="@color/text_color"
android:background="@mipmap/btn"
只要是名称 res_name 相同 ,那么任何一个apk 生成的 id(索引值是相同的) ,那么我们只需要把资源放入 一个 skin1.apk中即可,和主apk res_name相同,图片名称相同,颜色 名称相同 在color.xml 下

然后读取主 apk 控件属性对应 的value 的 int值   ,去资源apk中 找到对应资源即可,替换主apk中资源

只要是名称 res_name 相同 ,那么任何一个apk 生成的 id(索引值是相同的),如何理解: 看图

主apk resourec资源文件:

皮肤skin.apk  resourec资源文件:

主apk 颜色资源文件

skin 换肤 apk 颜色资源文件

然后读取主 apk 控件属性对应 的value 的 int值   ,去资源apk中 找到对应资源即可

资源文件替换代码:

1. 首先 创建一个 skin.apk ,如图 

 2.  adb push  skinp\build\outputs\apk\debug\red.apk /sdcard  

测试 资源替换代码 如何 :

  1. /**
  2. * 测试皮肤替换 : adb push skinp\build\outputs\apk\debug\red.apk /sdcard
  3. * @param view
  4. */
  5. public void testSkin(View view) {
  6. try {
  7. // 加载系统下皮肤
  8. String skinPath= Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator +"red.apk";
  9. File file=new File(skinPath);
  10. if(!file.exists()){
  11. Log.e(Tag,"文件不存在");
  12. return;
  13. }
  14. AssetManager asset = AssetManager.class.newInstance();
  15. // 读取 本地一个 .skin 里面资源
  16. Resources superRes = getResources();
  17. // 添加本地下载好的资源皮肤 Native 层 c和 c++ 怎么搞得
  18. Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
  19. method.setAccessible(true);
  20. // 执行反射方法
  21. method.invoke(asset,skinPath);
  22. mSkinResources=new Resources(asset,superRes.getDisplayMetrics(),superRes.getConfiguration());
  23. packageName= getPackageManager().getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES).packageName ;
  24. Log.e(Tag, "packageName:"+packageName);
  25. // 根据 包名, 资源文件名称 , 在 drawabel目录下 , 获取id 值 对应的 0x7f06000054
  26. int drawableId= mSkinResources.getIdentifier("btn", "drawable",packageName);
  27. // 获取 颜色的 id值
  28. int colorId= mSkinResources.getIdentifier("text_color","color",packageName);
  29. Log.e(Tag, "packageName:"+packageName + " drawable:"+ drawableId);
  30. // 根据id值 获取到 skin.apk 中 drawable对象
  31. // Drawable drawable=ContextCompat.getDrawable(this,drawableId); // 获取自己目录下
  32. Drawable drawable=mSkinResources.getDrawable(drawableId);
  33. // 根据id 值获取到颜色
  34. ColorStateList colorStateList = mSkinResources.getColorStateList(colorId);
  35. // 修改控件
  36. showImg.setBackground(drawable);
  37. showText.setTextColor(colorStateList);
  38. } catch (InstantiationException e) {
  39. e.printStackTrace();
  40. } catch (IllegalAccessException e) {
  41. e.printStackTrace();
  42. } catch (InvocationTargetException e) {
  43. e.printStackTrace();
  44. } catch (NoSuchMethodException e) {
  45. e.printStackTrace();
  46. }
  47. }

综上  换肤框架实现 源码:

  1. package mk.denganzhi.com.zhiwenku;
  2. import android.Manifest;
  3. import android.app.Activity;
  4. import android.content.Context;
  5. import android.content.pm.PackageManager;
  6. import android.content.res.AssetManager;
  7. import android.content.res.ColorStateList;
  8. import android.content.res.Resources;
  9. import android.graphics.drawable.Drawable;
  10. import android.os.Environment;
  11. import android.support.v4.app.ActivityCompat;
  12. import android.support.v4.content.ContextCompat;
  13. import android.support.v4.view.LayoutInflaterCompat;
  14. import android.support.v4.view.LayoutInflaterFactory;
  15. import android.support.v7.app.AppCompatActivity;
  16. import android.os.Bundle;
  17. import android.support.v7.widget.AppCompatImageView;
  18. import android.text.TextUtils;
  19. import android.util.AttributeSet;
  20. import android.util.Log;
  21. import android.view.LayoutInflater;
  22. import android.view.View;
  23. import android.widget.ImageView;
  24. import android.widget.TextView;
  25. import java.io.File;
  26. import java.lang.reflect.Constructor;
  27. import java.lang.reflect.InvocationTargetException;
  28. import java.lang.reflect.Method;
  29. import java.util.ArrayList;
  30. import java.util.HashMap;
  31. import java.util.List;
  32. public class MainActivity extends AppCompatActivity {
  33. String Tag="denganzhi";
  34. private static final List<String> mAttributes = new ArrayList<>();
  35. static {
  36. mAttributes.add("background");
  37. mAttributes.add("src");
  38. mAttributes.add("textColor");
  39. mAttributes.add("drawableLeft");
  40. mAttributes.add("drawableTop");
  41. mAttributes.add("drawableRight");
  42. mAttributes.add("drawableBottom");
  43. mAttributes.add("skinTypeface");
  44. }
  45. List<SkinView> skinViews=new ArrayList<>();
  46. ImageView showImg=null;
  47. TextView showText=null;
  48. @Override
  49. protected void onCreate(Bundle savedInstanceState) {
  50. /**
  51. * LayoutInflater LayoutInflater =
  52. (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  53. LayoutInflater 是一个系统服务, 单例
  54. */
  55. LayoutInflater layoutInflater= LayoutInflater.from(this);
  56. // // setFactory
  57. // LayoutInflaterCompat.setFactory(layoutInflater, new LayoutInflaterFactory() {
  58. // @Override
  59. // public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
  60. // Log.e("denganzhi1","创建View被拦截:"+name);
  61. //
  62. //
  63. // // 1. 创建View
  64. //
  65. // // 2. 解析属性
  66. //
  67. // // 3. 同意交给SkinManager管理
  68. //
  69. // if(name.equals("ImageView")){
  70. // TextView textView=new TextView(MainActivity.this);
  71. // textView.setText("拦截");
  72. // return textView;
  73. // }
  74. // return null;
  75. // }
  76. // });
  77. // setFactory2
  78. LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
  79. @Override
  80. public View onCreateView(String name, Context context, AttributeSet attrs) {
  81. Log.e("denganzhi1","创建View被拦截1:"+name);
  82. return null;
  83. }
  84. @Override
  85. public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
  86. // 反射 classLoader
  87. View view = createViewFromTag(name, context, attrs);
  88. // 自定义View
  89. if(null == view){
  90. view = createView(name, context, attrs);
  91. }
  92. SkinView skinView=new SkinView(); // 控件集合
  93. skinView.setView(view);
  94. List<SkinAttr> skinAttrs=new ArrayList<>(); // 每一个控件存放属性的集合
  95. int attrLength= attrs.getAttributeCount();
  96. for (int index=0;index<attrLength; index++){
  97. // 获取名称,值
  98. String attrName = attrs.getAttributeName(index);
  99. // 不需要换肤 属性值
  100. if (!mAttributes.contains(attrName)) {
  101. continue;
  102. }
  103. // 不符合 换肤条件
  104. String attrValue = attrs.getAttributeValue(index);
  105. if (attrValue.startsWith("#")) {
  106. continue;
  107. }
  108. /**
  109. *
  110. * android:background="?android:colorPrimary" 使用系统颜色值 ?16843827
  111. android:background="#000000" 不符合换肤条件
  112. android:background="@mipmap/ic_launcher" 使用之定义 @2131361793
  113. 所有的 value 都会别转化为 int 值
  114. */
  115. // attrName:background attValue:@2131361792
  116. int resId = 0 ;
  117. if (attrValue.startsWith("@") || attrValue.startsWith("?")) {
  118. attrValue = attrValue.substring(1);
  119. resId= Integer.parseInt(attrValue);
  120. }
  121. if(resId==0){ //不符合条件
  122. continue;
  123. }
  124. SkinAttr skinAttr =new SkinAttr();
  125. skinAttr.setSkyType(attrName);
  126. // skinAttr.setmResName(resName);
  127. skinAttr.setmResId(resId);
  128. skinAttrs.add(skinAttr);
  129. }
  130. if(skinAttrs.size()>0 ){
  131. skinView.setViewName(name);
  132. skinView.setmAttrs(skinAttrs); // 添加属性集合
  133. skinViews.add(skinView); // 添加View
  134. Log.e(Tag,"添加的View:"+ name + " skinView: "+ skinView );
  135. }
  136. // if(name.equals("ImageView")){
  137. // TextView textView=new TextView(MainActivity.this);
  138. // textView.setText("拦截");
  139. // return textView;
  140. // }
  141. return view;
  142. }
  143. });
  144. super.onCreate(savedInstanceState);
  145. setContentView(R.layout.activity_main);
  146. showImg = findViewById(R.id.showImg);
  147. showText = findViewById(R.id.showText);
  148. if (ContextCompat.checkSelfPermission(this,
  149. Manifest.permission.READ_EXTERNAL_STORAGE)
  150. != PackageManager.PERMISSION_GRANTED)
  151. {
  152. ActivityCompat.requestPermissions(this,
  153. new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
  154. 110);
  155. } else
  156. {
  157. }
  158. // myImageView = findViewById(R.id.myImageView);
  159. // Log.e("denganzhi",myImageView.getClass().toString());
  160. }
  161. // 比如根据 @123456 获取 颜色图片 名称
  162. public String getResName(String attrValue) {
  163. // 如果 android:background="#ffffff" 那么过滤掉,不换皮肤
  164. if (attrValue.startsWith("@") || attrValue.startsWith("?")) {
  165. attrValue = attrValue.substring(1);
  166. // 根据id 去插件包中找 资源
  167. int resId = Integer.parseInt(attrValue);
  168. // 获取资源类型
  169. String resourceTypeName = getResources().getResourceTypeName(resId);
  170. Log.e(Tag,"resourceTypeName: " +resourceTypeName);
  171. // 通过id 获取资源名字
  172. return getResources().getResourceEntryName(resId);
  173. }
  174. return null;
  175. }
  176. // 比如根据 @123456 获取 颜色图片 名称 btn_bg
  177. public String getResNamebyResId(int resId) {
  178. // 如果 android:background="#ffffff" 那么过滤掉,不换皮肤
  179. // 通过id 获取资源名字
  180. return getResources().getResourceEntryName(resId);
  181. }
  182. // 类型 mip 、color、drawable、layout.....
  183. public String getResTypebyResId(int resId){
  184. // 获取资源类型
  185. String resourceTypeName = getResources().getResourceTypeName(resId);
  186. return resourceTypeName;
  187. }
  188. Resources mSkinResources =null;
  189. String packageName=null;
  190. public void changeSkin(View view) {
  191. Log.e(Tag,"换肤List:"+skinViews);
  192. try {
  193. // 加载系统下皮肤
  194. String skinPath= Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator +"red.apk";
  195. File file=new File(skinPath);
  196. if(!file.exists()){
  197. Log.e(Tag,"文件不存在");
  198. return;
  199. }
  200. AssetManager asset = AssetManager.class.newInstance();
  201. // 读取 本地一个 .skin 里面资源
  202. Resources superRes = getResources();
  203. // 添加本地下载好的资源皮肤 Native 层 c和 c++ 怎么搞得
  204. Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
  205. method.setAccessible(true);
  206. // 执行反射方法
  207. method.invoke(asset,skinPath);
  208. mSkinResources=new Resources(asset,superRes.getDisplayMetrics(),superRes.getConfiguration());
  209. // 获取资源文件的包名
  210. packageName= getPackageManager().getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES).packageName ;
  211. if(!TextUtils.isEmpty(packageName) && mSkinResources!=null ){
  212. for(int i=0;i<skinViews.size();i++){
  213. SkinView skinView= skinViews.get(i);
  214. View viewSkin= skinView.getView();
  215. List<SkinAttr> skinAttrs= skinView.getmAttrs();
  216. for (int j=0;j<skinAttrs.size();j++){
  217. SkinAttr skinAttr= skinAttrs.get(j);
  218. String skyType= skinAttr.getSkyType();
  219. int mResId= skinAttr.getmResId();
  220. String resName= getResNamebyResId(mResId);
  221. String resType = getResTypebyResId(mResId);
  222. if(TextUtils.isEmpty(resName)){ // 没有找到
  223. continue;
  224. }
  225. // attrName:textColor attValue:@2130968660 resName:text_color
  226. // attrName:textColor attValue:?16843827 resName:colorPrimary
  227. Log.e(Tag, "packageName:"+packageName+" resName:"+resName + " resType:"+ resType);
  228. // 需要换肤的背景 background
  229. if(skyType.equals("background")){
  230. // 背景可以是图片 也可能是颜色
  231. if(resType.equals("color")){
  232. int background= mSkinResources.getColor(mResId);
  233. viewSkin.setBackgroundColor((Integer) background);
  234. }else{
  235. Drawable drawable= getDrawableByName(resName);
  236. ImageView imageView= (ImageView) viewSkin;
  237. imageView.setBackground(drawable);
  238. }
  239. // 需要换肤的 textcolor
  240. }else if(skyType.equals("textColor")){
  241. // int colorId= mSkinResources.getIdentifier("text_color","color",packageName);
  242. // ColorStateList colorStateList= mSkinResources.getColorStateList(resId);
  243. ColorStateList colorStateList= getColorByName(resName);
  244. TextView textView= (TextView)viewSkin;
  245. textView.setTextColor(colorStateList);
  246. }else if(skyType.equals("src")){
  247. }else if(skyType.equals("skinTypeface")){
  248. // 如何修改字体,思路
  249. // 可以判断这里是 viewSkin 是TextView Button 然后设置 , 第二个 参数传递不同路径即可
  250. // textView.setTypeface( Typeface.createFromAsset(this.getAssets(), "fonts/CodeBold.ttf"));
  251. }
  252. }
  253. }
  254. }
  255. } catch (NoSuchMethodException e) {
  256. e.printStackTrace();
  257. } catch (IllegalAccessException e) {
  258. e.printStackTrace();
  259. } catch (InstantiationException e) {
  260. e.printStackTrace();
  261. } catch (InvocationTargetException e) {
  262. e.printStackTrace();
  263. }
  264. }
  265. public ColorStateList getColorByName(String resName){
  266. int resId= mSkinResources.getIdentifier(resName,"color",packageName);
  267. ColorStateList colorStateList= mSkinResources.getColorStateList(resId);
  268. Log.e(Tag,"colorId:"+resId);
  269. return colorStateList;
  270. }
  271. public void restoreSkin(View view) {
  272. }
  273. private static final String[] mClassPrefixlist = {
  274. "android.widget.",
  275. "android.view.",
  276. "android.webkit."
  277. };
  278. private View createViewFromTag(String name, Context context, AttributeSet attrs) {
  279. //包含自定义控件
  280. if (-1 != name.indexOf(".")) {
  281. return null;
  282. }
  283. //
  284. View view = null;
  285. for (int i = 0; i < mClassPrefixlist.length; i++) {
  286. view = createView(mClassPrefixlist[i] + name, context, attrs);
  287. if(null != view){
  288. break;
  289. }
  290. }
  291. return view;
  292. }
  293. private static final Class[] mConstructorSignature =
  294. new Class[]{Context.class, AttributeSet.class};
  295. private static final HashMap<String, Constructor<? extends View>> mConstructor =
  296. new HashMap<String, Constructor<? extends View>>();
  297. private View createView(String name, Context context, AttributeSet attrs) {
  298. Constructor<? extends View> constructor = mConstructor.get(name);
  299. if (constructor == null) {
  300. try {
  301. //通过全类名获取class
  302. Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
  303. //获取构造方法
  304. constructor = aClass.getConstructor(mConstructorSignature);
  305. mConstructor.put(name, constructor);
  306. } catch (Exception e) {
  307. e.printStackTrace();
  308. }
  309. }
  310. if (null != constructor) {
  311. try {
  312. return constructor.newInstance(context, attrs);
  313. } catch (Exception e) {
  314. e.printStackTrace();
  315. }
  316. }
  317. return null;
  318. }
  319. }

 SkinView.java

  1. package mk.denganzhi.com.zhiwenku;
  2. import android.view.View;
  3. import java.util.List;
  4. /**
  5. * Created by Administrator on 2020/5/24.
  6. */
  7. public class SkinView {
  8. private View view;
  9. private String viewName;
  10. private List<SkinAttr> mAttrs;
  11. public View getView() {
  12. return view;
  13. }
  14. public void setView(View view) {
  15. this.view = view;
  16. }
  17. public List<SkinAttr> getmAttrs() {
  18. return mAttrs;
  19. }
  20. public String getViewName() {
  21. return viewName;
  22. }
  23. public void setViewName(String viewName) {
  24. this.viewName = viewName;
  25. }
  26. public void setmAttrs(List<SkinAttr> mAttrs) {
  27. this.mAttrs = mAttrs;
  28. }
  29. @Override
  30. public String toString() {
  31. return "SkinView{" +
  32. "view=" + view +
  33. ", viewName='" + viewName + '\'' +
  34. ", mAttrs=" + mAttrs +
  35. '}';
  36. }
  37. }
SkinAttr.java
  1. public class SkinAttr {
  2. private String mResName;
  3. private String skyType;
  4. private int mResId;
  5. public String getmResName() {
  6. return mResName;
  7. }
  8. public void setmResName(String mResName) {
  9. this.mResName = mResName;
  10. }
  11. public String getSkyType() {
  12. return skyType;
  13. }
  14. public void setSkyType(String skyType) {
  15. this.skyType = skyType;
  16. }
  17. public int getmResId() {
  18. return mResId;
  19. }
  20. public void setmResId(int mResId) {
  21. this.mResId = mResId;
  22. }
  23. @Override
  24. public String toString() {
  25. return "SkinAttr{" +
  26. "mResName='" + mResName + '\'' +
  27. ", skyType='" + skyType + '\'' +
  28. ", mResId=" + mResId +
  29. '}';
  30. }
  31. }

源码地址:https://download.csdn.net/download/dreams_deng/12454900

2023.6.17重新理解;

1.  看源码, 

【大概原理:Android动态换肤框架-换肤原理 - 简书

  1. -- 从 setContentView(R.layout.activity_main); 点入:
  2. AppCompatDelegateImpl.java
  3. public void setContentView(int resId) {
  4. LayoutInflater.from(mContext).inflate(resId, contentParent);
  5. }
  6. LayoutInflater.java
  7. public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
  8. //Resources 可以理解为xml解析器
  9. final Resources res = getContext().getResources();
  10. }
  11. public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  12. //获取节点name
  13. final String name = parser.getName();
  14. //创建View通过工厂或者
  15. final View temp = createViewFromTag(root, name, inflaterContext, attrs);
  16. root.addView(temp, params);
  17. }
  18. View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
  19. boolean ignoreThemeAttr) {
  20. //如果有 mFactory2 工厂, 通过 mFactory2 工厂创建
  21. if (mFactory2 != null) {
  22. view = mFactory2.onCreateView(parent, name, context, attrs);
  23. // //如果有 mFactory 工厂, 通过 mFactory 工厂创建
  24. } else if (mFactory != null) {
  25. view = mFactory.onCreateView(name, context, attrs);
  26. } else {
  27. view = null;
  28. }
  29. //如果没有工厂
  30. if (view == null) {
  31. final Object lastContext = mConstructorArgs[0];
  32. mConstructorArgs[0] = context;
  33. try {
  34. if (-1 == name.indexOf('.')) {
  35. //如果自定义控件
  36. view = onCreateView(parent, name, attrs);
  37. } else {
  38. //如果是系统控件
  39. view = createView(name, null, attrs);
  40. }
  41. } finally {
  42. mConstructorArgs[0] = lastContext;
  43. }
  44. }
  45. }
  46. public final View createView(String name, String prefix, AttributeSet attrs)
  47. throws ClassNotFoundException, InflateException {
  48. //获取控件的.class类
  49. clazz = mContext.getClassLoader().loadClass(
  50. prefix != null ? (prefix + name) : name).asSubclass(View.class);
  51. //通过放射获取构造函数
  52. constructor = clazz.getConstructor(mConstructorSignature);
  53. constructor.setAccessible(true);
  54. //创建对象!
  55. final View view = constructor.newInstance(args);
  56. }
  57. public final View createView(String name, String prefix, AttributeSet attrs)
  58. throws ClassNotFoundException, InflateException {
  59. //获取控件的.class类
  60. clazz = mContext.getClassLoader().loadClass(
  61. prefix != null ? (prefix + name) : name).asSubclass(View.class);
  62. //通过放射获取构造函数
  63. constructor = clazz.getConstructor(mConstructorSignature);
  64. constructor.setAccessible(true);
  65. //创建对象!
  66. final View view = constructor.newInstance(args);
  67. }

 大概原理就是系统 解析xml控件,createViewFromTag()的时候, 如果mFactory2不能与空,使用mFactory2 来创建控件,那么在app的onCreate中可以重写 Factory2, 

创建系统控件,自定义控件, 拦截获取控件的属性保存到一个集合中

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. LayoutInflater layoutInflater= LayoutInflater.from(this);
  4. // setFactory2
  5. LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
  6. @Override
  7. public View onCreateView(String name, Context context, AttributeSet attrs) {
  8. Log.e(Tag ,"创建View被拦截1:"+name);
  9. return null;
  10. }
  11. @Override
  12. public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
  13. // 反射 classLoader
  14. View view = createViewFromTag(name, context, attrs);
  15. // 自定义View
  16. if(null == view){
  17. view = createView(name, context, attrs);
  18. }
  19. Log.e(Tag ,"onCreateView:"+name);
  20. // SkinView skinView=new SkinView(); // 控件集合
  21. // skinView.setView(view);
  22. // List<SkinAttr> skinAttrs=new ArrayList<>(); // 每一个控件存放属性的集合
  23. int attrLength= attrs.getAttributeCount();
  24. for (int index=0;index<attrLength; index++){
  25. // 获取名称,值
  26. String attrName = attrs.getAttributeName(index);
  27. // 不需要换肤 属性值
  28. if (!mAttributes.contains(attrName)) {
  29. continue;
  30. }
  31. // 不符合 换肤条件
  32. String attrValue = attrs.getAttributeValue(index);
  33. if (attrValue.startsWith("#")) {
  34. continue;
  35. }
  36. /**
  37. *
  38. * android:background="?android:colorPrimary" 使用系统颜色值 ?16843827
  39. android:background="#000000" 不符合换肤条件
  40. android:background="@mipmap/ic_launcher" 使用之定义 @2131361793
  41. 所有的 value 都会别转化为 int 值
  42. */
  43. // attrName:background attValue:@2131361792
  44. int resId = 0 ;
  45. if (attrValue.startsWith("@") || attrValue.startsWith("?")) {
  46. Log.e("TAG", attrValue);
  47. String newattrValue = attrValue.substring(1);
  48. resId= Integer.parseInt(newattrValue);
  49. //这个值就是 android.R.color.colorPrimary 的id
  50. // int color= getResources().getColor(resId);
  51. Log.e("TAG",attrName+" "+ attrValue +" "+ Integer.toHexString(resId) +" " );
  52. // <attr name="colorPrimary" format="color" />
  53. //
  54. if(attrValue.startsWith("@")){
  55. // ff6200ee 直接获取默认颜色值
  56. Log.e("TAG", Integer.toHexString(getResources().getColor(resId)) +" " );
  57. // type: color name: black + 包名,获取插件中的颜色
  58. String resourceTypeName =getResources().getResourceTypeName(resId);
  59. String resourceEntryName = getResources().getResourceEntryName(resId);
  60. Log.e("TAG","@: "+resourceTypeName+ " "+ resourceEntryName);
  61. }
  62. // 需要通过如下方式获取颜色值:
  63. // android:textColor="?android:colorPrimary"
  64. // resId=android:colorPrimary attr.xml 的id
  65. // val值是:ff6200ee
  66. if(attrValue.startsWith("?")){
  67. int[] attrs1=new int[]{resId};
  68. int[] resIds = new int[attrs1.length];
  69. TypedArray a = context.obtainStyledAttributes(attrs1);
  70. for (int i = 0; i < attrs1.length; i++) {
  71. resIds[i] = a.getResourceId(i, 0);
  72. Log.e("TAG", " " + Integer.toHexString(getResources().getColor( resIds[i])) + " " );
  73. }
  74. a.recycle();
  75. String resourceTypeName =getResources().getResourceTypeName( resIds[0] );
  76. String resourceEntryName = getResources().getResourceEntryName(resIds[0]);
  77. // ?: color purple_500
  78. Log.e("TAG","?: "+resourceTypeName+ " "+ resourceEntryName);
  79. }
  80. if(attrName.equals("textColor")){
  81. // int colorId= getResources().getColor(resId);
  82. // Log.e("TAG", "hex:" + Integer.toHexString(colorId));
  83. }else{
  84. }
  85. }
  86. if(resId==0){ //不符合条件
  87. continue;
  88. }
  89. // SkinAttr skinAttr =new SkinAttr();
  90. // skinAttr.setSkyType(attrName);
  91. // // skinAttr.setmResName(resName);
  92. // skinAttr.setmResId(resId);
  93. // skinAttrs.add(skinAttr);
  94. }
  95. // if(skinAttrs.size()>0 ){
  96. // skinView.setViewName(name);
  97. // skinView.setmAttrs(skinAttrs); // 添加属性集合
  98. // skinViews.add(skinView); // 添加View
  99. // Log.e(Tag,"添加的View:"+ name + " skinView: "+ skinView );
  100. // }
  101. // if(name.equals("ImageView")){
  102. // TextView textView=new TextView(MainActivity.this);
  103. // textView.setText("拦截");
  104. // return textView;
  105. // }
  106. return view;
  107. }
  108. });
  109. super.onCreate(savedInstanceState);
  110. setContentView(R.layout.activity_main);
  111. imageView = findViewById(R.id.imageView);
  112. textView = findViewById(R.id.textView);
  113. test_color2 = findViewById(R.id.test_color2);
  114. test_color2.setTextColor(getResources().getColor(R.color.test_color));
  115. }
  116. }

 通过上面代码采集完毕,保存起来

  1. 1. setFactory2 设置,采集系统控件需要换肤属性
  2. 装起来:
  3. key:控件
  4. kev-value: 属性-app属性值!
  5. private List<SkinView> skinViews = new ArrayList<>();
  6. View view; 比如textView
  7. List<SkinPain> skinPains;
  8. static class SkinPain {
  9. String attributeName; //比如textcolor,
  10. int resId; // value值
  11. public SkinPain(String attributeName, int resId) {
  12. this.attributeName = attributeName;
  13. this.resId = resId;
  14. }
  15. }

有一个东西:可以读取apk皮肤包的 drawable, string, 给app用

博客:(2条消息) Android应用程序插件化研究之AssertManager_weixin_34159110的博客-CSDN博客

 上面已经获取到了每个控件属性值和 控件属性名

  根据值,获取对应的 

resourceTypeName、resourceEntryName
  1. /**
  2. *
  3. * android:background="?android:colorPrimary" 使用系统颜色值 ?16843827
  4. android:background="#000000" 不符合换肤条件
  5. android:background="@mipmap/ic_launcher" 使用之定义 @2131361793
  6. 所有的 value 都会别转化为 int 值
  7. */
  8. // attrName:background attValue:@2131361792
  9. int resId = 0 ;
  10. if (attrValue.startsWith("@") || attrValue.startsWith("?")) {
  11. Log.e("TAG", attrValue);
  12. String newattrValue = attrValue.substring(1);
  13. resId= Integer.parseInt(newattrValue);
  14. //这个值就是 android.R.color.colorPrimary 的id
  15. // int color= getResources().getColor(resId);
  16. Log.e("TAG",attrName+" "+ attrValue +" "+ Integer.toHexString(resId) +" " );
  17. // <attr name="colorPrimary" format="color" />
  18. //
  19. if(attrValue.startsWith("@")){
  20. // ff6200ee 直接获取默认颜色值
  21. Log.e("TAG", Integer.toHexString(getResources().getColor(resId)) +" " );
  22. // type: color name: black + 包名,获取插件中的颜色
  23. String resourceTypeName =getResources().getResourceTypeName(resId);
  24. String resourceEntryName = getResources().getResourceEntryName(resId);
  25. Log.e("TAG","@: "+resourceTypeName+ " "+ resourceEntryName);
  26. }
  27. // 需要通过如下方式获取颜色值:
  28. // android:textColor="?android:colorPrimary"
  29. // resId=android:colorPrimary attr.xml 的id
  30. // val值是:ff6200ee
  31. if(attrValue.startsWith("?")){
  32. int[] attrs1=new int[]{resId};
  33. int[] resIds = new int[attrs1.length];
  34. TypedArray a = context.obtainStyledAttributes(attrs1);
  35. for (int i = 0; i < attrs1.length; i++) {
  36. resIds[i] = a.getResourceId(i, 0);
  37. Log.e("TAG", " " + Integer.toHexString(getResources().getColor( resIds[i])) + " " );
  38. }
  39. a.recycle();
  40. String resourceTypeName =getResources().getResourceTypeName( resIds[0] );
  41. String resourceEntryName = getResources().getResourceEntryName(resIds[0]);
  42. // ?: color purple_500
  43. Log.e("TAG","?: "+resourceTypeName+ " "+ resourceEntryName);
  44. }

根据上一篇

  1. ------------------------------------------------------------------------------------------------------
  2. 1. 通过分析上面源码,自己写factory2, 创建view, 把View和需要换肤的属性保存起来
  3. ①. 如果有多个activity如何解决: application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
  4. 所有的actiivty创建以后都会走这里个方法
  5. 走这个方法的时候,设置factory2,那么就可以获取到所有属性了
  6. ②.
  7. try {
  8. //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
  9. //如设置过抛出一次
  10. //设置 mFactorySet 标签为false
  11. Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
  12. field.setAccessible(true);
  13. field.setBoolean(layoutInflater, false);
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. 2. 设置上面属性以后,获取到所有需要替换控件的 集合,然后走, 使用在 factory2中采集所有控件属性可以替换的
  18. setContentView(R.layout.activity_main); 方法!
  19. ***** 总结:
  20. 替换东西如下;
  21. ====0. background(颜色,图片),src,drawableLeft,drawableTop,drawableRight,drawableBottom,textColor
  22. 如何替换:
  23. 主apk和资源apk
  24. 根据 android:background="@mipmap/btn"
  25. drawable/bnt找到图片id , 根据id getResources获取图片,
  26. 写的博客; https://blog.csdn.net/dreams_deng/article/details/106320048
  27. ====1. 状态栏实现逻辑 : SkinThemeUtils.updateStatusBarColor
  28. 状态栏颜色;
  29. 如果配置 R.attr.colorPrimaryDark 用这个
  30. 没有配置用系统的: android.R.attr.statusBarColor, android.R.attr
  31. .navigationBarColor
  32. 可以自己设置: getWindow().setStatusBarColor(SkinResources.getInstance().getColor(statusBarColorResId[0]));
  33. android:textColor="?android:colorPrimary"
  34. //对应三个值:
  35. //resName: btn
  36. // resType: drawable
  37. // resId: R.id.btn对用的R文件的值
  38. // value值是 R.android.colorPrimary的R 值,
  39. // 然后在通过这个id获取颜色,
  40. // ColorStateList colorStateList = mSkinResources.getColorStateList(colorId);
  41. //
  42. ====2. 字体换肤
  43. 如何实现:
  44. app中: <string name="typeface"/>
  45. 皮肤包中:<string name="typeface">font/global.ttf</string>
  46. 读取皮肤包的val,
  47. 根据:Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
  48. skinTypefacePath 就是 font/global.ttf
  49. 如果是app包中:Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
  50. 设置给textview即可
  51. 颜色:
  52. mSkinResources.getColor(skinId)
  53. 多个颜色:
  54. mSkinResources.getColorStateList(skinId);
  55. 图片:
  56. mSkinResources.getDrawable(skinId);
  57. 字体:
  58. Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
  59. 1. setFactory2 设置,采集系统控件需要换肤属性
  60. 装起来:
  61. key:控件
  62. kev-value: 属性-app属性值!
  63. private List<SkinView> skinViews = new ArrayList<>();
  64. View view; 比如textView
  65. List<SkinPain> skinPains;
  66. static class SkinPain {
  67. String attributeName; //比如textcolor,
  68. int resId; // value
  69. public SkinPain(String attributeName, int resId) {
  70. this.attributeName = attributeName;
  71. this.resId = resId;
  72. }
  73. }
  74. 2. skinViews 获取完毕,下载皮肤apk
  75. 3. 开始替换皮肤
  76. ==== 3. recycleview 中的item 替换字体
  77. === 4. 自定义view实现 ,自定义属性
  78. 定义一个接口, public interface SkinViewSupport , 自定义控件实现, 换肤的时候调用一下方法,在apk包读取对应的属性,替换即可
  79. 上面缺点:setFactory2如果android改源码了,无法实现了!!
  80. 如何实现:
  81. 1.把皮肤app包放在assert下,直接去读取app皮肤包,替换!!
  82. ************************************************************************************************************************************************************************************
  83. Android9.0 直接hide 调了一些api,不是所有,屏蔽放射,
  84. 把 api 放入黑名单中,不可以放射!!
  85. /****
  86. * 夜间模式和日间模式实现:
  87. * 夜间读取: values-night 颜色值
  88. * 日间: 读取values 中的值
  89. */

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/86902
推荐阅读
相关标签
  

闽ICP备14008679号