1. 程式人生 > >Android Android-skin-support 換膚方案 原理講解

Android Android-skin-support 換膚方案 原理講解

文章目錄

前言

請先檢視這兩篇文章

思考一下

上面已經說明了,我們自定義Factory2 就可以達到 無需shape的解決方案

那麼同理,換膚我們怎麼做呢?

先整理一下思路

  1. 自定義兩個 color 的值 分別是
 <color name="text_color">#ff000000</color>
 <color name="main_color_night">#ffffffff</color>
 // 也就是main_color 白天的時候為 黑色
 // 夜間的時候 為 白色
  1. 在自定義的 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);
	    }
}
  1. 既然 View 建立成我們自己的 比如 SkinCompatTextView , 那麼我們不就可以輕鬆實現 屬性獲取,比如字型顏色 然後我們根據不同面板,載入不同顏色
  2. 這樣不就可以換膚了

問題:

那麼如果我們想動態換膚,怎麼辦?

耶? 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
然後返回

當然這個類還有很多功能,我們這裡只研究重點

結束語

  1. Android-skin-support 也是通過 自定義Factory的方式 實現
  2. 將系統傳過來的 view 如 TextView 建立成 自己的 SkinCompatTextView
  3. 通過 xxx_Helper 進行資源操作,並對外提供 applySkin 換膚方法
  4. SkinCompatResources 根據不同面板 返回不同的顏色值 (只說思路,還有很多功能)