Android Android-skin-support 換膚方案 原理講解
阿新 • • 發佈:2018-12-14
文章目錄
前言
請先檢視這兩篇文章
思考一下
上面已經說明了,我們自定義Factory2 就可以達到 無需shape的解決方案
那麼同理,換膚我們怎麼做呢?
先整理一下思路
- 自定義兩個 color 的值 分別是
<color name="text_color">#ff000000</color>
<color name="main_color_night">#ffffffff</color>
// 也就是main_color 白天的時候為 黑色
// 夜間的時候 為 白色
- 在自定義的 Factory2 (如WidSkinFactory2)中 建立自己的View 比如模仿 AppCompatActivity中 createView真正操作的類 AppCompatViewInflater.java
public class AppCompatViewInflater { final View createView(...){ switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; default: break; } } @NonNull protected AppCompatTextView createTextView(Context context, AttributeSet attrs) { return new AppCompatTextView(context, attrs); } }
- 既然 View 建立成我們自己的 比如 SkinCompatTextView , 那麼我們不就可以輕鬆實現 屬性獲取,比如字型顏色 然後我們根據不同面板,載入不同顏色
- 這樣不就可以換膚了
問題:
那麼如果我們想動態換膚,怎麼辦?
耶? View都是我們自己建立的了,那麼我們在 自定義Factory2 (如WidSkinFactory2)中,記錄下這些 View,然後定義某個方法,重新整理新的面板不就好了?
開源庫中 找到答案
為了驗證我們的想法更或者說是看看別人是否有更好的解決方案
我們來 提取關鍵幾個類進行講解
檢視Application
這裡的原始碼 其實就是註冊一個 Application.ActivityLifecycleCallbacks
在 Lifecycle onActivityCreated() 方法中 進行 installLayoutFactory
也就是將系統的 Factory2 替換為 自己的 SkinCompatDelegate <繼承於 LayoutInflaterFactory>
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
// 框架換膚日誌列印
Slog.DEBUG = true;
SkinCompatManager.withoutActivity(this)
.addInflater(new SkinAppCompatViewInflater()) // 基礎控制元件換膚
...
.loadSkin();
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
}
SkinCompatDelegate.java
public class SkinCompatDelegate implements LayoutInflaterFactory {
private final Context mContext;
private SkinCompatViewInflater mSkinCompatViewInflater;
private List<WeakReference<SkinCompatSupportable>> mSkinHelpers = new ArrayList<>();
private SkinCompatDelegate(Context context) {
mContext = context;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createView(parent, name, context, attrs);
if (view == null) {
return null;
}
// 快取 View 這裡跟我們 上面思考的問題一樣
// 記錄View 用於後期動態換膚
if (view instanceof SkinCompatSupportable) {
mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
}
return view;
}
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mSkinCompatViewInflater == null) {
mSkinCompatViewInflater = new SkinCompatViewInflater();
}
...
mSkinCompatViewInflater.createView(parent, name, context, attrs);
}
//換膚
public void applySkin() {
if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {
for (WeakReference ref : mSkinHelpers) {
if (ref != null && ref.get() != null) {
((SkinCompatSupportable) ref.get()).applySkin();
}
}
}
}
}
SkinCompatViewInflater.java
public class SkinCompatViewInflater {
private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) {
View view = null;
// 根據 你在 application 中 add 的 Infater 迴圈判斷,是否建立View成功
for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) {
view = inflater.createView(context, name, attrs);
if (view == null) {
continue;
} else {
break;
}
}
return view;
}
}
這裡我們先分析個最簡單的
SkinAppCompatViewInflater.java
public class SkinAppCompatViewInflater{
switch (name) {
// 建立自己的View,也就是印證了 思考 <2>
case "TextView":
view = new SkinCompatTextView(context, attrs);
}
}
SkinCompatTextView.java
public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
private SkinCompatTextHelper mTextHelper;
private SkinCompatBackgroundHelper mBackgroundTintHelper;
public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 換膚的幫助類,單一原則
mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper = SkinCompatTextHelper.create(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
}
// 動態換膚
@Override
public void applySkin() {
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySkin();
}
if (mTextHelper != null) {
mTextHelper.applySkin();
}
}
}
SkinCompatTextHelper.java
public class SkinCompatTextHelper {
public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
// ...
final Context context = mView.getContext();
a = context.obtainStyledAttributes(attrs, R.styleable.SkinTextAppearance, defStyleAttr, 0);
// 顏色
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
}
a.recycle();
// 執行
applySkin();
}
@Override
public void applySkin() {
...
applyTextColorResource();
}
private void applyTextColorResource() {
mTextColorResId = checkResourceId(mTextColorResId);
// 是否有ID
if (mTextColorResId != INVALID_ID) {
// 通過 工具 傳入 id 比如上面思考的 main_color
// 如果是夜間 他就會 返回 main_color_night
ColorStateList color = SkinCompatResources.getColorStateList(mView.getContext(), mTextColorResId);
mView.setTextColor(color);
}
}
}
SkinCompatResources 的原始碼就不講解了,就是上面所說的
根據不同的模式,將原有ID_xxx模式,如 main_color_night
然後返回
當然這個類還有很多功能,我們這裡只研究重點
結束語
- Android-skin-support 也是通過 自定義Factory的方式 實現
- 將系統傳過來的 view 如 TextView 建立成 自己的 SkinCompatTextView
- 通過 xxx_Helper 進行資源操作,並對外提供 applySkin 換膚方法
- SkinCompatResources 根據不同面板 返回不同的顏色值 (只說思路,還有很多功能)