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

PaintedSkin

    一款解決Android App 換膚框架墩邀,極低的侵入性與學(xué)習(xí)成本票罐。

效果展示

框架效果視頻


最新版本

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

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


框架實現(xiàn)原理

TODO

功能介紹

  1. 支持XML全部View換膚
  2. 支持XML指定View換膚
  3. 支持代碼創(chuàng)建View換膚
  4. 支持自定義View范咨、三方庫提供的View刹碾、自定義屬性換膚
  5. 支持絕大部分基礎(chǔ)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" 標(biāo)簽的View 將不參與換膚;

SkinMode.REPLACE_MARKED 只有添加了skin:enable="true"標(biāo)簽的View才參與換膚迷帜;

SkinMode.DO_NOT_REPLACE 任何View都不參與換膚

API:

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

PaintedSkin 支持調(diào)試模式與嚴(yán)格模式:

調(diào)試模式下將輸出框架內(nèi)的一些關(guān)鍵節(jié)點Log以及換膚任務(wù)執(zhí)行耗時時長物舒;

嚴(yán)格模式下如果框架內(nèi)出現(xiàn)錯誤將直接拋出異常;

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ā)人員調(diào)用初始化代碼戏锹,只需要在實現(xiàn)了IOptionFactory 接口的實現(xiàn)類上添加注解@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);

皮膚包構(gòu)建

  1. 新建Android application工程

  2. 皮膚工程包名不能和宿主應(yīng)用包名相同

  3. 將需要換膚的資源放置于res對應(yīng)目錄下

    例如 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);

進(jìn)階用法

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

自定義View、三方庫View換膚

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

  1. 實現(xiàn)IThemeSkinExecutorBuilder 接口,用于解析支持換膚屬性并創(chuàng)建對應(yīng)屬性的換膚執(zhí)行器悉盆《⒒纾可以參考框架內(nèi)自帶的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";
    /**
     * 換膚支持的屬性 進(jìn)度條背景
     */
    @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";
    /**
     * 換膚支持的屬性 填充內(nèi)容
     */
    @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 提供對應(yīng)屬性的換膚執(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.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市焕盟,隨后出現(xiàn)的幾起案子秋秤,更是在濱河造成了極大的恐慌,老刑警劉巖脚翘,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灼卢,死亡現(xiàn)場離奇詭異,居然都是意外死亡来农,警方通過查閱死者的電腦和手機鞋真,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沃于,“玉大人涩咖,你說我怎么就攤上這事±夸蹋” “怎么了抠藕?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蒋困。 經(jīng)常有香客問我,道長敬辣,這世上最難降的妖魔是什么雪标? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮溉跃,結(jié)果婚禮上村刨,老公的妹妹穿的比我還像新娘。我一直安慰自己撰茎,他們只是感情好嵌牺,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著龄糊,像睡著了一般逆粹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炫惩,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天僻弹,我揣著相機與錄音,去河邊找鬼他嚷。 笑死蹋绽,一個胖子當(dāng)著我的面吹牛芭毙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播卸耘,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼退敦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚣抗?” 一聲冷哼從身側(cè)響起苛聘,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忠聚,沒想到半個月后设哗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡两蟀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年网梢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赂毯。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡战虏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出党涕,到底是詐尸還是另有隱情烦感,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布膛堤,位于F島的核電站手趣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏肥荔。R本人自食惡果不足惜绿渣,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望燕耿。 院中可真熱鬧中符,春花似錦、人聲如沸誉帅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚜锨。三九已至档插,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踏志,已是汗流浹背阀捅。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留针余,地道東北人饲鄙。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓凄诞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忍级。 傳聞我的和親對象是個殘疾皇子帆谍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容