徒手擼一個框架-通用換膚框架(網易雲)

前言

換湯不換藥,用了仍是好! 新手也能看得懂的好文章!java

我!Android 初級 入門級開發程序員!很榮幸,在時間不長的從業生涯中,歷來沒遇見過有換膚需求的產品經理!


這真是太 可怕幸福了,苦於技術沒有提升的可能,只能拓展一下本身的知識面了。
這篇 徒手擼一個框架-通用換膚框架(網易雲) 就應運而生了(PS:代碼都是我抄的,不服來戰/滑稽)

原理說明

簡單說:兩個APK,一個安裝包(main.apk),一個皮膚包(skin.apk);當咱們的main.apk須要換膚的時候就經過資源的名字去skin.apk中取相同名字的資源而後進行替換操做。android

必備知識

資源文件的獲取
通常狀況下,咱們都是直接調用獲取資源文件的代碼來獲取資源:程序員

context.getResources().getColor(R.color.colorPrimary);
複製代碼

那麼究竟是什麼在幫咱們來進行資源的獲取操做的?
老規矩,扒一下源碼小姐姐:web


沒毛病,就是獲取一個 Resources對象,多簡單, 就是Resources在管理資源


我源碼小王子,看源碼就是這麼瀟灑!順便看一眼 getcolor()

藏得這麼深,居然還有!再往下看一層

不服氣,居然還有!(順 着資源ID找下去)

最終我找到了這個縮頭烏龜 mAssets。嗯?最終取值的居然不是 Resourecs?????/打臉 我!Android 初級 入門級開發程序員表示不服氣,必須整明白他(ctrl+鼠標左鍵+ mAssets


機器翻譯:
提供對應用程序原始資產文件的訪問;對於大多數應用程序檢索其資源數據的方式請參閱@link resources。此類提供了一個較低級別的API,它容許您打開和讀取與應用程序捆綁在一塊兒的原始文件,這些文件是一個簡單的字節流。
額。。好吧,果真最終真正將資源讀取出來的是 Assetmanager

開始擼碼

抽象一個BaseActivity

動手擼碼前,忽然想到一個問題——雖然是寫demo,可是個人換膚操做難道要在每一個Activity中都實現一邊嗎???固然不行!不偷懶的必定是個假程序員! 果斷抽象一個BaseActivity出來。架構

public class BaseActivity extends AppCompatActivity {

    private LayoutFactory layoutFactory;
    private FrameLayout frameLayout;
    private FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT
            , FrameLayout.LayoutParams.MATCH_PARENT);
    private Unbinder unbinder;
    private Toast toast;
    private View childActivityView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        layoutFactory = new LayoutFactory();
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), layoutFactory);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_base);
        //狀態欄透明
        setTransParentStatusBar();
        //初始化Activity界面的容器
        frameLayout = findViewById(R.id.baseViewContainer);
    }

    @Override
    protected void onResume() {
        super.onResume(); 
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //ButterKnife移除回調
        if (unbinder != null) unbinder.unbind();
        //從容器中移除Activity界面
        removeChildView();
    }

    /** * 返回Activity的View * * @return emptyView : childActivityView */
    public View getChildActivityView() {
        if (childActivityView == null) {
            Log.e("BaseActivity", "getChildActivityView() is error:Have not create an instance of Activity View");
            return new View(this);
        }
        return childActivityView;
    }

    /** * 添加Activity佈局到界面中 * * @param layoutResId 子Activity佈局文件資源ID * @return 子Activity佈局生成的View */
    protected void addContentView(@LayoutRes int layoutResId) {
        removeChildView();
        childActivityView = getLayoutInflater().inflate(layoutResId, frameLayout, false);
        frameLayout.addView(childActivityView, layoutParams);
        //綁定ButterKnife
        unbinder = ButterKnife.bind(this);
    }

    /** * 移除ChildView */
    private void removeChildView() {
        int childCount = frameLayout.getChildCount();
        if (childCount > 0) {
            frameLayout.removeAllViews();
        }
        childActivityView = null;
    }

    /** * 通用toast * * @param msg 信息 */
    protected void toast(String msg) {
        if (toast != null) toast.cancel();
        toast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
        toast.show();
    }

    /** * 狀態欄按鈕點擊事件 * 單獨來用,這個方法沒有@OnClick註解,ButterKnife是不會生成相關點擊事件代碼的 * 可是咱們的子Activity中ButterKnife綁定的點擊事件回調方法中能夠利用super.onViewClicked(view.getId())將ID傳遞過* 來,這樣就能夠一塊兒處理一些公用的控件點擊事件(這裏處理狀態欄中的返回、用戶按鈕) * @param viewId viewId */
    protected void onViewClicked(int viewId) {
        switch (viewId) {
            case R.id.ivBack:
                finish();
                toast("點擊了返回按鈕");
                break;
            case R.id.ivUser:
                toast("點擊了用戶頭像");
        }
    }

    /** * 設置透明狀態欄(PS:別忘了配合android:fitsSystemWindows="true") */
    private void setTransParentStatusBar() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
                    | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);

            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);

            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

            window.setStatusBarColor(Color.TRANSPARENT);
            window.setNavigationBarColor(Color.TRANSPARENT);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Window window = getWindow();
            window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }
}
複製代碼

極爲簡單的佈局文件,只有一個自定義的ActionBarapp

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BaseActivity">

    <FrameLayout android:id="@+id/actionbarLayout" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/skin_actionBarBg" android:fitsSystemWindows="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent">

        <ImageView android:id="@+id/ivBack" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|left" android:src="@drawable/skin_back" />

        <TextView android:id="@+id/tvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="BaseActivity" android:textColor="@color/skin_actionBarTextColor" />

        <ImageView android:id="@+id/ivUser" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right|center_vertical" android:src="@drawable/skin_user" />
    </FrameLayout>
<!--全部的Activity界面都添加在NestedScrollView中的FrameLayout中-->
    <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="0dp" android:fillViewport="true" android:background="#ffffff" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@+id/actionbarLayout">

        <FrameLayout android:id="@+id/baseViewContainer" android:layout_width="match_parent" android:layout_height="wrap_content" />
    </androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼

監聽系統View的生成

  1. 監聽原理
    想要實現實時換膚操做,那必定是要可以監聽View的生成,而且在View的生成過程當中設置咱們想要的元素,好比背景色等。 那麼咱們應該如何監聽View的生成呢?這裏其實谷歌已經給咱們提供好了相關回調接口:LayoutInflater.Factory2 那麼問題又來了,我咋知道這玩意能夠監聽View的生成呢?
LayoutInflater.from(this).inflate(R.layout.activity_main,parent,false);
複製代碼

這行代碼你們不陌生吧,就是這行代碼將咱們的xml轉成了咱們所須要的View!因此,爲了證實我是對的,扒一下源碼!一層一層往下看!
首先 LayoutInflater.from(this)框架

利用context來進行 LayoutInflater 的實例化,參數說明以下:


而後 inflate(R.layout.activity_main,parent,false) 來進行xml的轉換操做:
tips:爲啥最後的參數要寫false呢,能夠看下源碼中的參數說明你就明白了

繼續往下:

再往下:

最終在 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) 方法中找到view的建立過程:

很明顯,假如咱們設置了mFactory2回調參數,那麼View的生死就徹底被咱們掌控了!
最後咱們看一下 Factory2 的註釋:
若是返回一個View,就將他添加到層級架構中去,不然繼續 調用onCreateView(name)方法。(不明白的看上圖中的代碼,onCreateview()方法會一級一級調用)

也就是說,在View的建立過程當中,咱們徹底能夠本身定義要生成一個怎樣View。
2. 實現監聽
首先咱們建立一個Factory2的實現類:


而後在BaseActivity中將這個實現類設置爲監聽入口方法:

至於爲何要放在這裏,咱們能夠看一下 super.onCreate(savedInstanceState) 的父類:

篩選須要換膚的View

上一步咱們已經實現了View生成的監聽,這裏咱們實現View的篩選
在咱們建立的 LayoutFactory2 中進行篩選:ide

public View onCreateView(@Nullable View view, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
   // 注意這裏必定要本身根據傳遞過來的**attributeSet**參數實現View建立,而不能直接使用**view**進行篩選判斷
   // 緣由就是這個 view 並非咱們想要的 View 而是他的ParentView,因此這個View和attributeSet是不匹配的
}
複製代碼

完整的建立源碼:佈局

public class LayoutFactory implements LayoutInflater.Factory2 {

    private List<SkinView> skinViewList = new ArrayList<>();
    private final String[] prefixs = {"android.widget.", "android.view.", "android.webkit."};

    @Nullable
    @Override
    public View onCreateView(@Nullable View view, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
        View viewInstance = null;
        //s就是xml中
        // <TextView 
        // ****
        // ****
        // />
        //的Textview字段
        //因爲咱們建立View是利用的反射,因此建立的時候須要 包名.TextView這樣的格式進行實例化
        if (s.contains(".")) {//包含 . 說明是自定義View,直接能夠用這個
            viewInstance = onCreateView(s, context, attributeSet);
        } else {
            //不是自定義View的則遍歷前綴集合進行實例化,若是實例化爲空則說明不是該前綴下的控件
            //包含View的包也就這三個吧 "android.widget.", "android.view.", "android.webkit."
            for (String prefix : prefixs) {
                viewInstance = onCreateView(prefix + s, context, attributeSet);
                if (viewInstance != null) {
                    addSkinView(viewInstance, attributeSet);
                    break;
                }
            }
        }
        return viewInstance;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
        View view = null;
        try {
            Class aClass = context.getClassLoader().loadClass(s);
            Constructor<? extends View> constructor = aClass.getConstructor(Context.class, AttributeSet.class);
            view = constructor.newInstance(context, attributeSet);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return view;
    }

    /** * 條件篩選後添加須要換膚的View * * @param view Activity中的view */
    void addSkinView(@Nullable View view, @NonNull AttributeSet attributeSet) {
        if (view == null) {
            return;
        }

        List<SkinAttr> skinAttrs = new ArrayList<>();
        String idName = "";
        //遍歷View的屬性而且判斷該View是否須要應用換膚功能
        for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
            //資源ID的具體數值,引用資源文件獲得的資源ID格式是@123456
            String valueString = attributeSet.getAttributeValue(i);
            //若是不是直接引用了資源文件的屬性則忽略
            if (!valueString.startsWith("@")) {
                continue;
            }
            //資源值
            int value = Integer.parseInt(valueString.substring(1));

            //資源ID的名字
            String valueName = view.getResources().getResourceEntryName(value);

            //屬性名
            String name = attributeSet.getAttributeName(i);

            //資源ID的類型
            String type = view.getResources().getResourceTypeName(value);

            //找到了view的Id,取Id的name
            if (type.equals("id")) {
                idName = valueName;
            }
            //以 skin_ 爲資源名開頭的則說明須要換膚
            if (valueName.indexOf("skin_") == 0) {
                skinAttrs.add(new SkinAttr(idName, name, type, valueName, value));
            }
        }

        if (skinAttrs.size() > 0) {
            SkinView skinView = new SkinView(view, skinAttrs);
            skinViewList.add(skinView);
        }
    }

    private String getSimpleName() {
        return LayoutFactory.class.getSimpleName();
    }

    /** * 換膚操做 */
    public void changeNewSkin(Context context,String skinResourcePath) {
        SkinResourceManager.getInstance().setContext(context);
        SkinResourceManager.getInstance().loadSkin(skinResourcePath);
        if (skinViewList.size() == 0) {
            return;
        }
        for (SkinView skinView : skinViewList) {
            skinView.changeNewSkin();
        }
    }


    /** * 須要換膚的View的封裝 */
    class SkinView {
        //須要換膚的View
        private View view;
        //這個View中須要替換成皮膚包中資源的屬性集合
        List<SkinAttr> skinAttrList;

        public SkinView(View view, List<SkinAttr> skinAttrList) {
            this.view = view;
            this.skinAttrList = skinAttrList;
        }
        
        //該View進行換膚操做
        public void changeNewSkin() {
            for (SkinAttr skinAttr : skinAttrList) {
                if (skinAttr.name.equals("background")) {//設置背景
                    if (skinAttr.type.equals("color")) {
                        view.setBackgroundColor(SkinResourceManager.getInstance().getColor(skinAttr.value));
                    }
                    if (skinAttr.type.equals("drawable")) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                            view.setBackground(SkinResourceManager.getInstance().getDrawable(skinAttr.value));
                        } else {
                            view.setBackgroundDrawable(SkinResourceManager.getInstance().getDrawable(skinAttr.value));
                        }
                    }
                } else if (skinAttr.name.equals("textColor") && view instanceof TextView) { //設置字體顏色
                    ((TextView) view).setTextColor(SkinResourceManager.getInstance().getColor(skinAttr.value));
                } else if (skinAttr.name.equals("text") && view instanceof TextView) {//設置文字
                    ((TextView) view).setText(SkinResourceManager.getInstance().getString(skinAttr.value));
                } else if (skinAttr.name.equals("src") && view instanceof ImageView) {//設置圖片資源
                    ((ImageView) view).setImageDrawable(SkinResourceManager.getInstance().getDrawable(skinAttr.value));
                }
            }
        }
    }

    /** * 單條控件屬性元素封裝 */
    class SkinAttr {
        //View Id的名字
        private String idName;
        //屬性名,eg:background,textColor..
        private String name;
        //屬性類型,eg:@color,@drawable,@String
        private String type;
        //資源Id的name
        private String valueName;
        //資源ID
        private int value;

        public SkinAttr(String idName, String name, String type, String valueName, int value) {
            this.idName = idName;
            this.name = name;
            this.type = type;
            this.valueName = valueName;
            this.value = value;
        }

        public String getIdName() {
            return idName;
        }

        public String getName() {
            return name;
        }

        public String getType() {
            return type;
        }

        public String getValueName() {
            return valueName;
        }

        public int getValue() {
            return value;
        }
    }
}
複製代碼

從皮膚包中獲取資源的類:字體

public class SkinResourceManager {
    private static final SkinResourceManager skinResourceManager = new SkinResourceManager();
    /** * 皮膚包的包名 */
    private String mPackageName;

    public static SkinResourceManager getInstance() {
        return skinResourceManager;
    }


    private Context mContext;

    public Resources mSkinResources;

    private String apkPath;

    private SkinResourceManager() {
    }

    public void setContext(Context context) {
        mContext = context.getApplicationContext();
    }


    public void loadSkin(String skinResourcePtah) {
        if (TextUtils.isEmpty(skinResourcePtah)){
            mPackageName=mContext.getPackageName();
        }else {
            try {
               AssetManager manager = AssetManager.class.newInstance();
                Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                method.invoke(manager, skinResourcePtah);
                //當前應用的resources對象,獲取到屏幕相關的參數和配置
                Resources res = mContext.getResources();
                //getResources()方法經過 AssetManager的addAssetPath方法,構造出Resource對象,因爲是Library層的代碼,因此須要用到反射
                mSkinResources = new Resources(manager, res.getDisplayMetrics(), res.getConfiguration());
                mPackageName = mContext.getPackageManager().getPackageArchiveInfo(skinResourcePtah, PackageManager.GET_ACTIVITIES).packageName;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //經過ID獲取drawable對象
    public Drawable getDrawable(int id) {
        Drawable drawable = mContext.getResources().getDrawable(id);
        if (mSkinResources != null) {
            String name = mContext.getResources().getResourceEntryName(id);
            Log.i(SkinResourceManager.class.getSimpleName(), "getDrawable()--name=" + name + "--packageName=" + mPackageName);
            int resId = mSkinResources.getIdentifier(name, "drawable", mPackageName);
            if (resId > 0) {
                return mSkinResources.getDrawable(resId);
            }
        }
        return drawable;
    }

    //經過ID獲取顏色值
    public int getColor(int id) {
        int color = mContext.getResources().getColor(id);
        if (mSkinResources != null) {
            String name = mContext.getResources().getResourceEntryName(id);
            Log.i(SkinResourceManager.class.getSimpleName(), "getColor()--name=" + name + "--packageName=" + mPackageName);
            int resId = mSkinResources.getIdentifier(name, "color", mPackageName);
            if (resId > 0) {
                return mSkinResources.getColor(resId);
            }
        }
        return color;
    }

    public String getString(int id) {
        String str = mContext.getResources().getString(id);
        if (mSkinResources != null) {
            String name = mContext.getResources().getResourceEntryName(id);
            Log.i(SkinResourceManager.class.getSimpleName(), "getDrawable()--name=" + name + "--packageName=" + mPackageName);
            int resId = mSkinResources.getIdentifier(name, "string", mPackageName);
            if (resId > 0) {
                Log.i(SkinResourceManager.class.getSimpleName(), "getDrawable()--name=" + name + "--packageName=" + mPackageName+"--get="+mSkinResources.getString(resId));
                return mSkinResources.getString(resId);
            }
        }
        return str;
    }
}
複製代碼

建立一個皮膚包

新建一個 skin_test module,該module是 application 類型,能夠 buildapk

添加皮膚須要的 drawable、colors、strings

build一下,生成皮膚apk文件

將生成的皮膚apk更名並放到對應手機的目錄中:

應用換膚

新建一個Main2Activity用於換膚操做
當點擊換膚按鈕時,將會切換至藍色皮膚樣式,點擊換膚默認按鈕時恢復默認紅色皮膚
界面樣式以下:

BaseActivity中新建換膚方法


最終調用咱們建立的LayoutFactory2中的換膚方法進行遍歷換膚

Main2Activity中應用換膚操做
這裏用 SP 來持久保存當前應用的皮膚資源路徑

其餘Activity中同時也應用換膚onResume() 中判斷一下是否須要換膚便可

完結

至此整套換膚流程就結束了

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息