PaintedSkin 一款解決Android App 換膚功能的框架

PaintedSkin

    一款解決Android App 換膚框架,極低的侵入性與學習成本。

效果展示

框架效果視頻


最新版本

模塊 說明 版本
PaintedSkin 換膚核心包 3.1.6
StandardPlugin 減少代碼侵入的插件包 3.1.6
AutoPlugin 全自動插件包 3.1.6
ConstraintLayoutCompat ConstraintLayout換膚兼容包 3.1.6
TypefacePlugin 替換字體插件 3.1.6

項目地址 喜歡就給個Star 吧!


框架實現原理

TODO

功能介紹

  1. 支持XML全部View換膚
  2. 支持XML指定View換膚
  3. 支持代碼創(chuàng)建View換膚
  4. 支持自定義View、三方庫提供的View、自定義屬性換膚
  5. 支持絕大部分基礎View換膚
  6. 支持差異化換膚(適用于部分View節(jié)日換膚)
  7. [支持全局動態(tài)替換字體](#TypefacePlugin 使用)
  8. 支持通過攔截器攔截View創(chuàng)建過程
  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:PaintedSkin:TAG'
    implementation 'com.github.CoderAlee.PaintedSkin:StandardPlugin:TAG'
    // StandardPlugin 與 AutoPlugin 只需添加一個
    annotationProcessor 'com.github.CoderAlee.PaintedSkin:AopPlugin:TAG'
    implementation 'com.github.CoderAlee.PaintedSkin:AopPlugin:TAG'
    //如果項目中的ConstraintLayout需要換膚則引入
    implementation 'com.github.CoderAlee.PaintedSkin:ConstraintLayoutCompat:TAG'
    // 需要替換字體庫時引入
    implementation 'com.github.CoderAlee.PaintedSkin:TypefacePlugin: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 支持調試模式與嚴格模式:

調試模式下將輸出框架內的一些關鍵節(jié)點Log以及換膚任務執(zhí)行耗時時長;

嚴格模式下如果框架內出現錯誤將直接拋出異常;

API:

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

插件使用

 `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 不再需要開發(fā)人員調用初始化代碼,只需要在實現了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"
            }
        }
    

動態(tài)創(chuàng)建View換膚

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

  TextView textView = new TextView(getContext());
        textView.setTextColor(getResources().getColor(R.color.textColor));
        textView.setText("動態(tài)創(chuàng)建View參與換膚");
        WindowManager.getInstance().getWindowProxy(getContext()).addEnabledThemeSkinView(textView, new SkinElement("textColor", R.color.textColor));
        layout.addView(textView);

進階用法

攔截View創(chuàng)建過程

        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的創(chuàng)建過程其實可以實現很多騷操作,比如上面這段代碼就可以將全局的TextView替換成Button。這比在XML中一個一個修改要快捷方便的多。其中Google 就是通過這種方式將Button 替換為AppCompatButton。AppCompatDelegate也是同樣的技術方案。

自定義View、三方庫View換膚

當自定義View或使用的三方庫View中有自定義屬性需要換膚時:

  1. 實現IThemeSkinExecutorBuilder 接口,用于解析支持換膚屬性并創(chuàng)建對應屬性的換膚執(zhí)行器??梢詤⒖伎蚣軆茸詭У腄efaultExecutorBuilder:
@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;
    }

    /**
     * 需要換膚執(zhí)行器
     *
     * @param view    需要換膚的View
     * @param element 需要執(zhí)行的元素
     * @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 提供對應屬性的換膚執(zhí)行器:
 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);
    }
}

License Apache-2.0

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.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容