Skip to content

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

License

Notifications You must be signed in to change notification settings

CoderAlee/PaintedSkin

Repository files navigation

PaintedSkin

概述

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

Gitter build Hex.pm

最新版本

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

版本履历

  • V3.0.0

    • 支持AndroidX.
  • V3.1.0

    • 剥离依赖,上传至公网Maven
  • V3.2.0

    • ISwitchThemeSkinObserver 增加 onThemeSkinSwitchRunOnUiThread 接口
  • V3.3.2

    • 支持 tint 属性换肤
  • V3.4.0

    • 适配 minSdkVersion 至19
  • V3.4.2

    • Config 增加性能模式,当使用体验优先时Activity将被允许在后台换肤
  • V3.5.0

    • 修复换肤时卡顿问题、修复统计换肤View数量不正确问题
  • V3.5.1

    • 增加对android:drawablexxxdrawablexxxCompat 的属性支持
  • V3.5.2

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

框架实现原理

功能介绍

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

使用

添加依赖

  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 都不参与换肤。
public final class App extends Application {
    static {
        Config.getInstance().setSkinMode(Config.SkinMode.REPLACE_ALL);
    }
}
PaintedSkin 支持调试模式与严格模式:
  • 调试模式下将输出框架内的一些关键节点Log以及换肤任务执行耗时时长。
  • 严格模式下如果框架内出现错误将直接抛出异常。
public final class App extends Application {
    static {
         Config.getInstance().setEnableDebugMode(false);
         Config.getInstance().setEnableStrictMode(false);
    }
}
PaintedSkin 支持性能优先与体验优先:
  • 性能优先模式下如果触发换肤的时候部分 Window 处于后台或不可见时,将不会立即换肤。而是在其恢复到前台或可见时执行换肤;
  • 体验优先模式下无论Window处于何种状态下都将立即执行换肤;
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 的目录下有如下资源:

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

      在皮肤包对应 res/values/colors.xml 的目录下有如下资源:

      <color name="textColor">#FF000000</color>
    • 如果想要替换 Button 背景图片,在 apk 中有 res/mipmap/bg_button.png ,在皮肤包中 res/mipmap/bg_button.png

  4. 在皮肤包工程的 build.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:
@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<Integer, String> 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 提供对应属性的换肤执行器:
public class ViewSkinExecutor<T extends View> extends BaseSkinExecutor<T> {

    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 ATTRIBUTE_BACKGROUND:
                view.setBackgroundColor(color);
                break;
            case ATTRIBUTE_FOREGROUND:
                applyDrawable(view, new ColorDrawable(color), attrName);
                break;
            default:
                break;
        }
    }


    @Override
    protected void applyDrawable(@NonNull T view, @NonNull Drawable drawable, @NonNull String attrName) {
        switch (attrName) {
            case ATTRIBUTE_BACKGROUND:
                view.setBackground(drawable);
                break;
            case ATTRIBUTE_FOREGROUND:
                view.setForeground(drawable);
                break;
            default:
                break;
        }
    }
}
  1. 将自定义的ThemeSkinExecutorBuilder添加到框架中:
ThemeSkinService.getInstance().addThemeSkinExecutorBuilder(xxx);

ConstraintLayout 换肤兼容包使用

public final class App extends Application {
    static {
        ConstraintLayoutCompat.init();
    }
}

TypefacePlugin 使用

public final class App extends Application {
    static {
        TypefacePlugin.init();
    }
    
    @Override
    public void onCreate() {
        super.onCreate();
       TypefacePlugin.getInstance().setEnable(true).switchTypeface(Typeface);
    }
}

混淆配置

项目需要混淆编译时,请在 proguard-rules.pro 中添加如下规则

-keep class org.alee.reflex.** { *; }
-keep class org.alee.component.skin.** { *; }
Copyright [2018] [MingYu.Liu]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •