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
功能介紹
- 支持XML全部View換膚
- 支持XML指定View換膚
- 支持代碼創(chuàng)建View換膚
- 支持自定義View范咨、三方庫提供的View刹碾、自定義屬性換膚
- 支持絕大部分基礎(chǔ)View換膚
- 支持差異化換膚(適用于部分View節(jié)日換膚)
- [支持全局動態(tài)替換字體](#TypefacePlugin 使用)
- 支持通過攔截器攔截View創(chuàng)建過程
- 支持Androidx燥撞、support
- 支持定制擴展
- 不會與其他依賴LayoutInflater.Factory 的庫沖突
使用
添加依賴
- 在工程的
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: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)建
新建Android application工程
皮膚工程包名不能和宿主應(yīng)用包名相同
-
將需要換膚的資源放置于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
-
在皮膚包工程的
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中有自定義屬性需要換膚時:
- 實現(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);
}
}
- 繼承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;
}
}
}
- 將自定義的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.