android 換膚框架搭建及使用 (3 完結篇)

語言: CN / TW / HK

theme: github

本系列計劃3篇:

  1. Android 換膚之資源(Resources)加載(一)
  2. setContentView() / LayoutInflater源碼分析(二)
  3. 換膚框架搭建(三) --- 本篇

tips: 本篇只説實現思路,以及使用,具體細節請下載代碼查看!

本篇實現效果:

| fragment換膚 | recyclerView換膚 | 自定義view屬性換膚 | | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | 打開 | 打開 | 打開 | | 動態換膚 | dialog換膚 | | | 打開 | 打開 | |

回顧

第一篇中: 我們可以通過這段代碼來創建自己的Resource來加載另一個apk中的資源

java   try (     // 創建AssetManager     AssetManager assetManager = AssetManager.class.newInstance()   ) {     // 反射調用 創建AssetManager#addAssetPath     Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);  ​     // 獲取到當前apk在手機中的路徑     String path = getApplicationContext().getPackageResourcePath();        /// 反射執行方法     method.invoke(assetManager, path);        // 創建自己的Resources     Resources resources = new Resources(assetManager, createDisplayMetrics(), createConfiguration());        // 根據id來獲取圖片     Drawable drawable = resources.getDrawable(R.drawable.ic_launcher_background, null);        // 設置圖片     mImageView.setImageDrawable(drawable);      } catch (Exception e) {     e.printStackTrace();   }      // 這些關於屏幕的就用原來的就可以   public DisplayMetrics createDisplayMetrics() {       return getResources().getDisplayMetrics();   }      public Configuration createConfiguration() {       return getResources().getConfiguration();   }

第二篇中: 我們分析了setContentView() 加載流程, 並且分析了LayoutInflater加載view流程

並且我們知道了如何通過Factory來攔截View創建

第二篇不是最近寫的,是很早之前寫的.這裏正好適合,就當作第二篇來使用!

攔截代碼:

kotlin  class CustomParseActivity : AppCompatActivity() {      override fun onCreate(savedInstanceState: Bundle?) {          val layoutInflater = LayoutInflater.from(this)          // 如果factory2 == null就創建          if (layoutInflater.factory2 == null) {              LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 {                // SystemAppCompatViewInflater 是粘貼自系統源碼 [AppCompatViewInflater]                  val compatInflater = SystemAppCompatViewInflater()                  override fun onCreateView(                      parent: View?,                      name: String,                      context: Context,                      attrs: AttributeSet,                 ): View? {                    // 在這裏就可以攔截view的創建                                         // Factory創建view                        val view = compatInflater.createView(parent, name, context, attrs, false,                          true,                            true,                           false                     )                                        return view                 }               ...              })         }        // 必須在super 之前          super.onCreate(savedInstanceState)          setContentView(activity_custom_parse)     }  }

項目搭建思路

要想達到換膚效果,其實就是加載另一個APK中的資源文件,然後實現替換

現在我們已經知道了如何加載另一個APK中的資源,我們只需要保存起來需要替換的view即可,然後再特定的時機去調用它

在點擊換膚的時候,刷新所有保存的view對象,讓它自己去加載另一個APK中的資源即可

首先我們需要規定替換哪些資源:

例如有一個view:

xml  <Button      android:id="@+id/bt1"      android:layout_width="match_parent"      android:layout_height="wrap_content"      android:background="@color/global_background"      android:text="@string/global_re_skin"      android:textSize="@dimen/global_def_text_font"      android:textColor="@color/global_text_color" />

這裏我們就可以替換

  • background
  • text
  • textSize
  • textColor

因為這些屬性是經常用的,並且是引用的資源文件中的資源,我想沒人需要替換width / height

知道了需要替換哪些資源後,我們就可以在解析view的時候來保存起來這些屬性,然後在某個時機的時候手動刷新即可

整個框架搭建我是採用的 Application.ActivityLifecycleCallbacks 這個類可以監聽到activity所有的生命週期

並且採用了觀察者設計模式,單例等設計模式,來實現點擊的時候刷新需要改變屬性的view

在使用的時候 只需要 一行代碼就可以搞定 java  #Application.java  public void onCreate(){ SkinManager.init(this);    }

在解析屬性的時候,我採用了enum的特性 方便解析給view對應屬性賦值

例如這樣: java  public enum SkinReplace {      ANDROID_BACKGROUND("background") {          @Override          void loadResource(View view, SkinAttr attr) {              view.setBackgroundColor(XXX);         }     };        private final String mName;  ​      SkinReplace(String value) {          mName = value;     }  ​      abstract void loadResource(View view, SkinAttr value);  }

框架小細節

初始化factory

Application.ActivityLifecycleCallbacks#onActivityCreated() 執行時機為:

  • AppCompatActivity.super.onCreate() 之後
  • setContentView() 之前

我們由第二篇知道,Factory是在super.onCreate()中初始化的,並且Factory只能初始化一次,

在android28之前一般通過反射 LayoutInflater.mFactorySet 屬性為false來實現加載我們的Factory

但是android28之後就不行了

那麼android28之後版本我們可以通過反射來直接替換掉系統的Factory即可

java  // 通過反射替換掉系統的factory  private SkinLayoutInflaterFactory forceSetFactory2(LayoutInflater inflater, Activity activity) {      Class<LayoutInflater> inflaterClass = LayoutInflater.class;      try {          String mFactoryStr = "mFactory";          Field mFactory = inflaterClass.getDeclaredField(mFactoryStr);          mFactory.setAccessible(true);  ​          String mFactory2Str = "mFactory2";          Field mFactory2 = inflaterClass.getDeclaredField(mFactory2Str);          mFactory2.setAccessible(true);          SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity);          // 改變factory          mFactory2.set(inflater, skinLayoutInflaterFactory);          mFactory.set(inflater, skinLayoutInflaterFactory);          return skinLayoutInflaterFactory;     } catch (Exception e) {          e.printStackTrace();     }      return null;  }

一定創建View成功

我們粘貼出來 AppCompatViewInflater.java的時候,只能創建系統的view

image-20230106140416691

我們必須創建view,因為我們需要通過view上的屬性來判斷它是否需要"換膚"

那麼我們需要在這裏的時候自己反射創建view[粘貼自LayoutInflater源碼]

image-20230106140610061

這裏看不懂沒關係,如果單純的使用來説 一點也不重要!

使用框架前提

  1. 有一個皮膚包, 在一篇中皮膚包如何製作我説的很詳細了!

image-20230106132303825

  1. 將皮膚包放入到手機內存中
  2. 記得讀寫權限,保證能夠正常訪問手機內存中的數據
  3. 引入lib-skin
  4. 在 Application.onCreate() 中初始化: SkinManager.init(this);

可以想像一下網易雲,QQ等大廠的換膚, 點擊一個按鈕,然後下載一個皮膚包存儲到手機中,然後我們去讀取這個皮膚包的內容

最終我們只需要生成對應的皮膚包給到後台,然後我們就實現了動態的更換皮膚!

在Activity中換膚

如果你已經將皮膚包放入到了手機內存中,並且已經初始化了SkinManager

那麼替換皮膚只需要一行代碼:

java  SkinManager.getInstance().loadSkin("皮膚包的在手機中的路徑",Activity);

如果你不想使用皮膚包,那麼也只需要一行代碼:

java   SkinManager.getInstance().reset();

現在你已經可以實現

  • src
  • text
  • text_color
  • text_size
  • background

換膚了!

如果還需要其他屬性換膚,下面會提到,別急!

在Fragment中使用換膚

在fragment中使用皮膚包只需要注意一點:

在view創建完成的時候調用:

java public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); SkinManager.getInstance().tryInitSkin(getActivity()); }

這是為了避免第一次初始化的時候加載不到皮膚

其他任何改變都不需要!

在RecyclerView中使用換膚

不需要任何處理

換膚:

java SkinManager.getInstance().loadSkin("皮膚包的在手機中的路徑",Activity); // 換膚

恢復默認:

java SkinManager.getInstance().reset();

自定義屬性換膚

首先我們需要隨便自定義一個view

image-20230106135201104

  1. 皮膚包中設置需要替換的資源

image-20230106135410339

  1. 編寫改變屬性的方法:

image-20230106135551412

4.在SkinReplace中規定需要改變的屬性,並且通過反射調用對應方法

image-20230106135929195

反射方法: java  /*   * 作者:史大拿   * 創建時間: 1/4/23 8:07 PM   * TODO 自定義反射,反射具體方法屬性   * @param view: 需要反射的對象   * @param methodName: 反射的方法名字   * @param SkinReflectionMethod: 反射具體數據 [類型和參數]   */  public void setCustomAttr(View view, String methodName, SkinReflectionMethod... data) {      try {          Class<?>[] cls = new Class<?>[data.length];          Object[] objects = new Object[data.length];          for (int i = 0; i < data.length; i++) {              cls[i] = data[i].getCls();              objects[i] = data[i].getObj();         }          Method method = view.getClass().getDeclaredMethod(methodName, cls);          method.setAccessible(true);          method.invoke(view, objects);     } catch (Exception e) {          e.printStackTrace();          SkinLog.e("反射失敗;" + e.getMessage() + "\t" + SkinConfig.SKIN_ERROR_7);     }  }

到此還是通過

SkinManager.getInstance().loadSkin("皮膚包的在手機中的路徑",Activity);

換膚即可

動態換膚

動態換膚只需要在

SkinManager.getInstance().loadSkin("皮膚包的在手機中的路徑",Activity);

之後調用對應方法即可

  • drwable SkinManager.getInstance().getDrawable(String)
  • string SkinManager.getInstance().getString(String)
  • color SkinManager.getInstance().getColor(String)
  • dimen SkinManager.getInstance().getFontSize(String)

例如這樣:

java  findViewById(R.id.bt_re_skin).setOnClickListener(v -> {    // 換膚      SkinManager.getInstance().loadSkin(PATH,Activity);            mTextView.setBackground(SkinManager.getInstance().getDrawable("global_skin_drawable_background"));      mTextView.setText(SkinManager.getInstance().getString("global_custom_view_text"));  });

如果app中有一個A資源, 皮膚包中沒有A資源,現在已經換膚了 那麼還是默認使用app中的A資源

但是如果app中沒有A資源,並且皮膚包中也沒有A資源,那麼就報錯了

就是一句話:

如果當前是換膚狀態,那麼優先使用皮膚包中的資源,

如果皮膚包中的資源不存在,則使用app中的資源,如果都不存在,那麼就報錯

Dialog換膚

AlertDialog

java    private AlertDialog alertDialog;    private void showAlertDialog(View v) {        // 避免重複解析皮膚包      if (alertDialog == null) {          View view = getLayoutInflater().inflate(R.layout.item_alert_dialog, null);          alertDialog = new AlertDialog.Builder(this)                 .setView(view)                 .create();     }     if (!alertDialog.isShowing()) {              alertDialog.show();         }          // 初始化第一次,避免第一次的時候沒有換膚效果      SkinManager.getInstance().tryInitSkin(this);  }

dialog換膚也是非常簡單,只需要Dialog.show()

的時候去

java SkinManager.getInstance().tryInitSkin(this);

即可

DialogFragment換膚

這個dialog當作一個fragment用即可

和fragment注意事項相同,需要當view加載完成的時候在嘗試刷新一下

java  @Override  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {      super.onViewCreated(view, savedInstanceState);      SkinManager.getInstance().tryInitSkin(getActivity());  }

最後一點:換膚目前只能替換View的屬性,若想要替換viewGroup屬性,需要這樣寫: ```java

SystemAppCompatViewInflater.java

final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme) {

switch (name) {
    case "TextView":
        view = createTextView(context, attrs);
        verifyNotNull(view, name);
        break;
     ...
    case "LinearLayout":
         view = new LinearLayout(context.attrs);
         break;
}

} ```

完整項目地址

原創不易,您的點贊與關注就是對我最大的支持!

本篇結束,耗時15天從框架搭建到一行代碼換膚,新年前最後一篇,最後祝大家新年快樂~ 年後見 🫡🫡

本系列計劃3篇:

  1. Android 換膚之資源(Resources)加載(一)
  2. setContentView() / LayoutInflater源碼分析(二)
  3. 換膚框架搭建(三) -- 本篇