Android 常用換膚方式以及原理分析

JavaNoober發表於2018-09-05

常用方法

1.通過Theme切換主題

通過在setContentView之前設定Theme實現主題切換。 在styles.xml定義一個夜間主題和白天主題:

<style name="LightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <!--主題背景-->
    <item name="backgroundTheme">@color/white</item>
</style>

<style name="BlackTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <!--主題背景-->
    <item name="backgroundTheme">@color/dark</item>
</style>
複製程式碼

設定主要切換主題View的背景:

<android.support.constraint.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"
    android:background="?attr/backgroundTheme"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="切換主題"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
複製程式碼

切換主題:

通過呼叫setTheme()

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setTheme(R.style.BlackTheme);
    setContentView(R.layout.activity_main);
}


finish();
Intent intent = getIntent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
overridePendingTransition(0, 0);
複製程式碼

效果如下:

Android 常用換膚方式以及原理分析

2.通過AssetManager切換主題

下載皮膚包,通過AssetManager載入皮膚包裡面的資原始檔,實現資源替換。

ClassLoader

Android可以通過classloader獲取已安裝apk或者未安裝apk、dex、jar的context物件,從而通過反射去獲取Class、資原始檔等。

載入已安裝應用的資源

Android 常用換膚方式以及原理分析

//獲取已安裝app的context物件
Context context = ctx.getApplicationContext().createPackageContext("com.noob.resourcesapp", 		Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
//獲取已安裝app的resources物件
Resources resources = context.getResources();
//通過resources獲取classloader,反射獲取R.class
Class aClass = context.getClassLoader().loadClass("com.noob.resourcesapp.R$drawable");
int resId = (int) aClass.getField("icon_collect").get(null);
imageView.setImageDrawable(resources.getDrawable(id));
複製程式碼

載入未安裝應用的資源

String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.apk";
//通過反射獲取未安裝apk的AssetManager
AssetManager assetManager = AssetManager.class.newInstance();
//通過反射增加資源路徑
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.invoke(assetManager, apkPath);
File dexDir = ctx.getDir("dex", Context.MODE_PRIVATE);
if (!dexDir.exists()) {
    dexDir.mkdir();
}
//獲取未安裝apk的Resources
Resources resources = new Resources(assetManager, ctx.getResources().getDisplayMetrics(),
        ctx.getResources().getConfiguration());
//獲取未安裝apk的ClassLoader
ClassLoader classLoader = new DexClassLoader(apkPath, dexDir.getAbsolutePath(), null, ctx.getClassLoader());
//反射獲取class
Class aClass = classLoader.loadClass("com.noob.resourcesapp.R$drawable");
int id = (int) aClass.getField("icon_collect").get(null);
imageView.setImageDrawable(resources.getDrawable(id));
複製程式碼

LayoutInflater.Factory

分析setContentView原始碼

LayoutInflater.Factory是如何被呼叫的

Android 常用換膚方式以及原理分析
setContentView最終呼叫了inflate方法,我們來看一下inflate方法的原始碼
Android 常用換膚方式以及原理分析
Android 常用換膚方式以及原理分析
inflate最終呼叫了createViewFromTag方法來建立View,在這之中用到了factory,如果factory存在就用factory建立物件,如果不存在就由系統自己去建立
Android 常用換膚方式以及原理分析

我們在setContentView之前呼叫測試程式碼 測試程式碼:

LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            Log.e("MainActivity", "name :" + name);
            int count = attrs.getAttributeCount();
            for (int i = 0; i < count; i++) {
                Log.e("MainActivity", "AttributeName :" + attrs.getAttributeName(i) + "AttributeValue :"+ attrs.getAttributeValue(i));
            }
            return null;
        }
});
複製程式碼

log日誌:

Android 常用換膚方式以及原理分析

結果發現我們可以獲取一個layout的所有View,此時我們就可以對View進行皮膚切換效果。

通過AssetManager切換主題總結

通過AssetManager和LayoutInflater.Factory配合就可以達到呼叫外部資源獲取皮膚的方法。如果想要動態更新,只需要把需要動態更新的View存起來,去遍歷設定皮膚,或者用eventBus去通知也可以。

對比

上述兩種方法是市面上大多數換膚框架的實現原理。
通過Theme切換主題:
優點:實現簡單,配置簡單
缺點:需要重啟應用;是固定皮膚,不能動態切換
通過AssetManager切換主題:
優點:不需要重啟應用;可以動態載入主題,用於盈利 缺點:實現較為複雜;皮膚包比較佔資源

專案地址:github.com/JavaNoober/…

相關文章