MCPcopy Index your code
hub / github.com/CoderAlee/PaintedSkin

github.com/CoderAlee/PaintedSkin @3.5.2.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release 3.5.2.1 ↗ · + Follow
786 symbols 1,907 edges 150 files 362 documented · 46%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

PaintedSkin

    一款解决Android App 换肤框架,极低的侵入性与学习成本。

Gitter build Hex.pm


最新版本

模块 说明 版本
PaintedSkin 换肤核心包
StandardPlugin 减少代码侵入的插件包
AutoPlugin 全自动插件包
ConstraintLayoutCompat ConstraintLayout换肤兼容包
TypefacePlugin 替换字体插件

版本履历

V3.0.0
  • [x] 支持AndroidX.
V3.1.0
  • [x] 剥离依赖,上传至公网Maven
V3.2.0
  • [x] ISwitchThemeSkinObserver增加onThemeSkinSwitchRunOnUiThread 接口
V3.3.2
  • [x] 支持tint属性换肤
V3.4.0
  • [x] 适配minSdkVersion至19
V3.4.2
  • [x] Config增加性能模式,当使用体验优先时Activity将被允许在后台换肤
V3.5.0
  • [x] 修复换肤时卡顿问题、修复统计换肤View数量不正确问题
V3.5.1
  • [x] 增加对android:drawablexxxdrawablexxxCompat 的属性支持
V3.5.2

- [x] 增强对Android低版本使用矢量图的兼容,同时移除默认皮肤包获取不到Drawable时提供默认颜色逻辑.

框架实现原理


功能介绍

  1. 支持XML全部View换肤
  2. 支持XML指定View换肤
  3. 支持代码创建View换肤
  4. 支持自定义View、三方库提供的View、自定义属性换肤
  5. 支持绝大部分基础View换肤
  6. 支持差异化换肤(适用于部分View节日换肤)
  7. 支持全局动态替换字体
  8. 支持通过拦截器拦截View创建过程
  9. 支持Androidx、support
  10. 支持定制扩展
  11. 不会与其他依赖LayoutInflater.Factory 的库冲突

使用

添加依赖

  1. 在工程的build.gradle文件中添加:

buildscript {
    repositories {
        maven { url "https://jitpack.io" } // 必须添加
    }
    dependencies {
        ...
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10' // 如果不使用AutoPlugin可以不添加
    }
    allprojects {
        maven { url "https://jitpack.io" } // 必须添加
    }
}

  1. 如需使用AutoPlugin,在项目appbuild.gradle文件中添加:

apply plugin: 'android-aspectjx' 
android {
    ...
}

  1. 在项目appbuild.gradle文件中添加::

dependencies {
    // 依赖的反射库
    implementation 'com.github.CoderAlee:Reflex:1.2.0'
    // 核心库
    implementation 'com.github.CoderAlee.PaintedSkin:painted-skin:TAG'
    implementation 'com.github.CoderAlee.PaintedSkin:standard-plugin:TAG'
    // StandardPlugin 与 AutoPlugin 只需添加一个
    annotationProcessor 'com.github.CoderAlee.PaintedSkin:auto-plugin:TAG'
    implementation 'com.github.CoderAlee.PaintedSkin:auto-plugin:TAG'
    //如果项目中的ConstraintLayout需要换肤则引入
    implementation 'com.github.CoderAlee.PaintedSkin:constraintlayout-compat:TAG'
    // 需要替换字体库时引入
    implementation 'com.github.CoderAlee.PaintedSkin:typeface-plugin:TAG'
    ...
}

运行配置

PaintedSkin支持三种换肤模式:

SkinMode.REPLACE_ALL 所有View都参与换肤,添加了skin:enable="false" 标签的View 将不参与换肤;

SkinMode.REPLACE_MARKED 只有添加了skin:enable="true"标签的View才参与换肤;

SkinMode.DO_NOT_REPLACE 任何View都不参与换肤

API:

public final class App extends Application {
    static {
        Config.getInstance().setSkinMode(Config.SkinMode.REPLACE_ALL);
    }
}

PaintedSkin 支持调试模式与严格模式:

调试模式下将输出框架内的一些关键节点Log以及换肤任务执行耗时时长;

严格模式下如果框架内出现错误将直接抛出异常;

API:

public final class App extends Application {
    static {
         Config.getInstance().setEnableDebugMode(false);
         Config.getInstance().setEnableStrictMode(false);
    }
}

PaintedSkin 支持性能优先与体验优先:

性能优先模式下如果触发换肤的时候部分Window处于后台或不可见时,将不会立即换肤。而是在其恢复到前台或可见时执行换肤;

体验优先模式下无论Window处于何种状态下都将立即执行换肤;

API:

public final class App extends Application {
    static {
         Config.getInstance().setPerformanceMode(Config.PerformanceMode.PERFORMANCE_PRIORITY);
          Config.getInstance().setPerformanceMode(Config.PerformanceMode.EXPERIENCE_FIRST);
    }
}

插件使用

StandardPlugin 使用:

public final class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        WindowManager.getInstance().init(this,new OptionFactory());
    }
}
final class OptionFactory implements IOptionFactory {
    @Override
    public int defaultTheme() {
        return 0;
    }

    @Override
    public IThemeSkinOption requireOption(int theme) {
        switch (theme) {
            case 1:
                return new NightOption();
            default:
                return null;
        }
    }
}

AutoPlugin 不再需要开发人员调用初始化代码,只需要在实现了IOptionFactory 接口的实现类上添加注解@Skin 即可:

@Skin
public final class OptionFactory implements IOptionFactory {
    @Override
    public int defaultTheme() {
        return 0;
    }

    @Override
    public IThemeSkinOption requireOption(int theme) {
        switch (theme) {
            case 1:
                return new NightOption();
            default:
                return null;
        }
    }
}

主题配置

class NightOption implements IThemeSkinOption {

    @Override
    public LinkedHashSet<String> getStandardSkinPackPath() {
        LinkedHashSet<String> pathSet = new LinkedHashSet<>();
        pathSet.add("/sdcard/night.skin");
        return pathSet;
    }
}

换肤

 ThemeSkinService.getInstance().switchThemeSkin(int theme);

皮肤包构建

  1. 新建Android application工程

  2. 皮肤工程包名不能和宿主应用包名相同

  3. 将需要换肤的资源放置于res对应目录下

例如 Button 文字颜色

APK 中res/values/colors.xml

xml <color name="textColor">#FFFFFFFF</color>

皮肤包中 res/values/colors.xml

xml <color name="textColor">#FF000000</color>

例如 Button 背景图片

APK 中 res/mipmap/bg_button.png

皮肤包中 res/mipmap/bg_button.png

  1. 在皮肤包工程的build.gradle文件中添加:

gradle applicationVariants.all { variant -> variant.outputs.all { output -> outputFileName = "xxx.skin" } }

动态创建View换肤

核心接口WindowManager.getInstance().getWindowProxy(getContext()).addEnabledThemeSkinView(View,SkinElement);

  TextView textView = new TextView(getContext());
        textView.setTextColor(getResources().getColor(R.color.textColor));
        textView.setText("动态创建View参与换肤");
        WindowManager.getInstance().getWindowProxy(getContext()).addEnabledThemeSkinView(textView, new SkinElement("textColor", R.color.textColor));
        layout.addView(textView);

进阶用法

拦截View创建过程

        ThemeSkinService.getInstance().getCreateViewInterceptor().add(new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                return onCreateView(name, context, attrs);
            }

            @Nullable
            @Override
            public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                if (TextUtils.equals(name,"TextView")){
                    return new Button(context, attrs);
                }
                return null;
            }
        });

通过拦截View的创建过程其实可以实现很多骚操作,比如上面这段代码就可以将全局的TextView替换成Button。这比在XML中一个一个修改要快捷方便的多。其中Google 就是通过这种方式将Button 替换为AppCompatButton。AppCompatDelegate也是同样的技术方案。

#### 自定义View、三方库View换肤

当自定义View或使用的三方库View中有自定义属性需要换肤时:

  1. 实现IThemeSkinExecutorBuilder 接口,用于解析支持换肤属性并创建对应属性的换肤执行器。可以参考框架内自带的DefaultExecutorBuilder:

```java @RestrictTo(RestrictTo.Scope.LIBRARY) public final class DefaultExecutorBuilder implements IThemeSkinExecutorBuilder { / * 换肤支持的属性 背景 */ @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_BACKGROUND = "background"; / * 换肤支持的属性 前景色 / @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_FOREGROUND = "foreground"; / * 换肤支持的属性 字体颜色 / @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_TEXT_COLOR = "textColor"; / * 换肤支持的属性 暗示字体颜色 */ @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_TEXT_COLOR_HINT = "textColorHint"; / * 换肤支持的属性 选中时高亮背景颜色 / @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_TEXT_COLOR_HIGH_LIGHT = "textColorHighlight"; / * 换肤支持的属性 链接的颜色 / @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_TEXT_COLOR_LINK = "textColorLink"; / * 换肤支持的属性 进度条背景 */ @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_PROGRESS_DRAWABLE = "progressDrawable"; / * 换肤支持的属性 ListView分割线 / @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_LIST_VIEW_DIVIDER = "divider"; / * 换肤支持的属性 填充内容 / @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_SRC = "src"; /* * 换肤支持的属性 按钮背景 / @RestrictTo(RestrictTo.Scope.LIBRARY) public static final String ATTRIBUTE_BUTTON = "button"; private static final Map SUPPORT_ATTR = new HashMap<>();

   static {
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_background, ATTRIBUTE_BACKGROUND);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_foreground, ATTRIBUTE_FOREGROUND);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColor, ATTRIBUTE_TEXT_COLOR);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColorHint, ATTRIBUTE_TEXT_COLOR_HINT);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColorHighlight, ATTRIBUTE_TEXT_COLOR_HIGH_LIGHT);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColorLink, ATTRIBUTE_TEXT_COLOR_LINK);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_progressDrawable, ATTRIBUTE_PROGRESS_DRAWABLE);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_divider, ATTRIBUTE_LIST_VIEW_DIVIDER);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_src, ATTRIBUTE_SRC);
       SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_button, ATTRIBUTE_BUTTON);
   }

   /**
    * 解析支持换肤的属性
    *
    * @param context      {@link Context}
    * @param attributeSet {@link AttributeSet}
    * @return {@link SkinElement}
    */
   @RestrictTo(RestrictTo.Scope.LIBRARY)
   @Override
   public Set<SkinElement> parse(@NonNull Context context, @NonNull AttributeSet attributeSet) {
       TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.BasicSupportAttr);
       if (null == typedArray) {
           return null;
       }
       Set<SkinElement> elementSet = new HashSet<>();
       try {
           for (Integer key : SUPPORT_ATTR.keySet()) {
               try {
                   if (typedArray.hasValue(key)) {
                       elementSet.add(new SkinElement(SUPPORT_ATTR.get(key), typedArray.getResourceId(key, -1)));
                   }
               } catch (Throwable ignored) {
               }
           }
       } catch (Throwable ignored) {
       } finally {
           typedArray.recycle();
       }
       return elementSet;
   }

   /**
    * 需要换肤执行器
    *
    * @param view    需要换肤的View
    * @param element 需要执行的元素
    * @return {@link ISkinExecutor}
    */
   @Override
   @RestrictTo(RestrictTo.Scope.LIBRARY)
   public ISkinExecutor requireSkinExecutor(@NonNull View view, @NonNull SkinElement element) {
       return BasicViewSkinExecutorFactory.requireSkinExecutor(view, element);
   }

   /**
    * 是否支持属性
    *
    * @param view     View
    * @param attrName 属性名称
    * @return true: 支持
    */
   @Override
   @RestrictTo(RestrictTo.Scope.LIBRARY)
   public boolean isSupportAttr(@NonNull View view, @NonNull String attrName) {
       return SUPPORT_ATTR.containsValue(attrName);
   }

} ```

  1. 继承BaseSkinExecutor 提供对应属性的换肤执行器:

    ```java public class ViewSkinExecutor extends BaseSkinExecutor {

      public ViewSkinExecutor(@NonNull SkinElement fullElement) {
          super(fullElement);
      }
    
      @Override
      protected void applyColor(@NonNull T view, @NonNull ColorStateList colorStateList, @NonNull String attrName) {
          switch (attrName) {
              case ATTRIBUTE_BACKGROUND:
              case ATTRIBUTE_FOREGROUND:
                  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                      applyDrawable(view, new ColorStateListDrawable(colorStateList), attrName);
                  } else {
                      applyColor(view, colorStateList.getDefaultColor(), attrName);
                  }
                  break;
              default:
                  break;
          }
      }
    
      @Override
      protected void applyColor(@NonNull T view, int color, @NonNull String attrName) {
          switch (attrName) {
              case A
    

Extension points exported contracts — how you extend this code

IExpandedFactory2 (Interface)
@author: MY.Liu @date: 2021/2/14 @description: xxxx [19 implementers]
Library/PaintedSkin/src/main/java/org/alee/component/skin/factory2/IExpandedFactory2.java
OnItemClickListener (Interface)
RecyclerView Item 点击监听 详细描述 @author MingYu.Liu created in 2021/10/28
BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/basic/recycler/OnItemClickListener.java
ISkinOptionFactoryLoader (Interface)
@author: MY.Liu @date: 2021/2/19 @description: xxxx
Library/Plugin/AopPlugin/src/main/java/org/alee/component/skin/plugin/ISkinOptionFactoryLoader.java
IThemeSkinExecutorBuilder (Interface)
@author: MY.Liu @date: 2021/2/14 @description: xxxx [4 implementers]
Library/PaintedSkin/src/main/java/org/alee/component/skin/parser/IThemeSkinExecutorBuilder.java
IThemeSkinAble (Interface)
* 换肤能力 * * 实现了此接口的View既支持动态换肤 * * @author MingYu.Liu * created in 2022/9/13 * */
BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/basic/template/IThemeSkinAble.kt
IThemeSkinService (Interface)
@author: MY.Liu @date: 2021/2/14 @description: xxxx [4 implementers]
Library/PaintedSkin/src/main/java/org/alee/component/skin/service/IThemeSkinService.java
IUIProcess (Interface)
* 通过模板收敛UI处理代码 * * 详细描述 * * @author MingYu.Liu * created in 2022/9/13 * */
BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/basic/template/IUIProcess.kt
ITask (Interface)
摘要 详细描述 @author MingYu.Liu created in 2021/10/26 [4 implementers]
Library/PaintedSkin/src/main/java/org/alee/component/skin/util/task/template/ITask.java

Core symbols most depended-on inside this repo

put
called by 45
Library/PaintedSkin/src/main/java/org/alee/component/skin/collection/SparseStack.java
apply
called by 37
Library/PaintedSkin/src/main/java/org/alee/component/skin/pack/LoadThemeSkinQueue24.java
getInstance
called by 33
Library/PaintedSkin/src/main/java/org/alee/component/skin/service/Config.java
getInstance
called by 33
Library/PaintedSkin/src/main/java/org/alee/component/skin/util/PrintUtil.java
run
called by 32
Library/PaintedSkin/src/main/java/org/alee/component/skin/executor/BaseSkinExecutor.java
updateLayoutParams
called by 30
BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/util/layout/AttributesExt.kt
isEmpty
called by 30
Library/PaintedSkin/src/main/java/org/alee/component/skin/collection/SparseStack.java
printE
called by 23
Library/PaintedSkin/src/main/java/org/alee/component/skin/util/PrintUtil.java

Shape

Method 537
Class 144
Function 82
Interface 22
Enum 1

Languages

Java68%
Kotlin32%

Modules by API surface

BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/util/MMKVUtil.kt25 symbols
BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/basic/recycler/BaseRecyclerAdapter.kt21 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/util/task/TaskPool.java19 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/pack/BaseSkinResourcesProvider.java19 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/service/Config.java14 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/collection/SparseStack.java14 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/pack/BaseThemeSkinPack.java13 symbols
BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/util/layout/ViewGroupExt.kt13 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/service/DefaultService.java12 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/pack/BaseStandardSkinResourcesProvider.java12 symbols
Library/PaintedSkin/src/main/java/org/alee/component/skin/factory2/ExpandedFactory2Manager.java12 symbols
BasicDemo/src/main/java/org/alee/demo/skin/basic/ability/util/layout/FragmentExt.kt12 symbols

For agents

$ claude mcp add PaintedSkin \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact