一款解决Android App 换肤框架,极低的侵入性与学习成本。
| 模块 | 说明 | 版本 |
|---|---|---|
| PaintedSkin | 换肤核心包 | |
| StandardPlugin | 减少代码侵入的插件包 | |
| AutoPlugin | 全自动插件包 | |
| ConstraintLayoutCompat | ConstraintLayout换肤兼容包 | |
| TypefacePlugin | 替换字体插件 |
ISwitchThemeSkinObserver增加onThemeSkinSwitchRunOnUiThread 接口tint属性换肤minSdkVersion至19Config增加性能模式,当使用体验优先时Activity将被允许在后台换肤android:drawablexxx 与 drawablexxxCompat 的属性支持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" } // 必须添加
}
}
AutoPlugin,在项目app的build.gradle文件中添加:
apply plugin: 'android-aspectjx'
android {
...
}
app的build.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);
新建Android application工程
皮肤工程包名不能和宿主应用包名相同
将需要换肤的资源放置于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
build.gradle文件中添加:gradle
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "xxx.skin"
}
}
核心接口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);
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中有自定义属性需要换肤时:
```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);
}
} ```
继承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
$ claude mcp add PaintedSkin \
-- python -m otcore.mcp_server <graph>