在上一节中讲解了动态换肤的原理以及动态换肤的基本实现,本篇文章来讲解动态换肤扩展(Fragment、状态栏、自定义view、字体).

Fragment 扩展

Fragment 不用设置fractory2,只要activity中设置了fractory2就不用在设置就可以实现换肤,所以fragment不用任何修改.那么是为什么呢?

我们都知道在fragment是通过,onCreateView 传递一个LayoutInflater,来实现view的加载

  1. override fun onCreateView(
  2. inflater: LayoutInflater,
  3. container: ViewGroup?,
  4. savedInstanceState: Bundle?
  5. ): View? {
  6. return inflater.inflate(R.layout.fragment_music, container, false)
  7. }

那么这个LayoutInflater 是从哪里来的呢? 相信大家也能猜出个大概,其实就是和加载Fragment的Activity有关,我们看一下系统的Fragment源码

  1. @Deprecated
  2. @NonNull
  3. @RestrictTo(LIBRARY_GROUP_PREFIX)
  4. public LayoutInflater getLayoutInflater(@Nullable Bundle savedFragmentState) {
  5. if (mHost == null) {
  6. throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the "
  7. + "Fragment is attached to the FragmentManager.");
  8. }
  9. LayoutInflater result = mHost.onGetLayoutInflater();
  10. LayoutInflaterCompat.setFactory2(result, mChildFragmentManager.getLayoutInflaterFactory());
  11. return result;
  12. }

在上述代码中,Fragment通过 mHostonGetLayoutInflater 获取的,我们来看一下这个 mHost 是什么? mhost是一个接口类FragmentHostCallback. 代码如下,可以看出Fragment的很多操作都和mHost有关,包括获取上下文等操作,那么我们就可以断定这个mHost 肯定是从Activity中传递过来的

  1. // Host this fragment is attached to.
  2. FragmentHostCallback mHost;
  3. @Nullable
  4. public Context getContext() {
  5. return mHost == null ? null : mHost.getContext();
  6. }
  7. @Nullable
  8. final public FragmentActivity getActivity() {
  9. return mHost == null ? null : (FragmentActivity) mHost.getActivity();
  10. }
  11. .....

FragmentActivity 的源码如下: FrgmentActivity 的代码中实现了FragmentHostCallback,那么Framgment的 mHost 其实就是FragmentActivity的实现接口类

  1. class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements
  2. ViewModelStoreOwner,
  3. OnBackPressedDispatcherOwner {
  4. public HostCallbacks() {
  5. super(FragmentActivity.this /*fragmentActivity*/);
  6. }
  7. .....
  8. @Override
  9. @NonNull
  10. public LayoutInflater onGetLayoutInflater() {
  11. return FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.this);
  12. }
  13. ......
  14. }

也就是说mHost.onGetLayoutInflater();其实调用的就是如下代码

  1. public LayoutInflater onGetLayoutInflater() {
  2. return FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.this);
  3. }
  4. /**
  5. * Convenience for calling
  6. * {@link android.view.Window#getLayoutInflater}.
  7. */
  8. @NonNull
  9. public LayoutInflater getLayoutInflater() {
  10. return getWindow().getLayoutInflater();
  11. }

那么具体实现的LayoutInflater就是PhoneLayoutInfalter,代码如下:

  1. public class PhoneLayoutInflater extends LayoutInflater {
  2. private static final String[] sClassPrefixList = {
  3. "android.widget.",
  4. "android.webkit.",
  5. "android.app."
  6. };
  7. /**
  8. * Instead of instantiating directly, you should retrieve an instance
  9. * through {@link Context#getSystemService}
  10. *
  11. * @param context The Context in which in which to find resources and other
  12. * application-specific things.
  13. *
  14. * @see Context#getSystemService
  15. */
  16. public PhoneLayoutInflater(Context context) {
  17. super(context);
  18. }
  19. protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
  20. super(original, newContext);
  21. }
  22. /** Override onCreateView to instantiate names that correspond to the
  23. widgets known to the Widget factory. If we don't find a match,
  24. call through to our super class.
  25. */
  26. @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
  27. for (String prefix : sClassPrefixList) {
  28. try {
  29. View view = createView(name, prefix, attrs);
  30. if (view != null) {
  31. return view;
  32. }
  33. } catch (ClassNotFoundException e) {
  34. // In this case we want to let the base class take a crack
  35. // at it.
  36. }
  37. }
  38. return super.onCreateView(name, attrs);
  39. }
  40. public LayoutInflater cloneInContext(Context newContext) {
  41. return new PhoneLayoutInflater(this, newContext);
  42. }
  43. }

PhoneLayoutInfalter实现流程和LayoutInfalter的流程是一样的,只不过通过cloneInContext,重新new了一个LayoutInflater 也相当于克隆了一个LayoutInflater,调用了Layoutflater的一个构造方法,同时也将factory2进行了赋值.

  1. protected LayoutInflater(LayoutInflater original, Context newContext) {
  2. mContext = newContext;
  3. mFactory = original.mFactory;
  4. mFactory2 = original.mFactory2;
  5. mPrivateFactory = original.mPrivateFactory;
  6. setFilter(original.mFilter);
  7. }

那么我们再回到Fragment中的getLayoutInflater中,不知道大家有没有发现Fragment又重新对factory2进行了赋值,那么为什么我们还能动态换肤成功呢?

  1. public LayoutInflater getLayoutInflater(@Nullable Bundle savedFragmentState) {
  2. if (mHost == null) {
  3. throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the "
  4. + "Fragment is attached to the FragmentManager.");
  5. }
  6. LayoutInflater result = mHost.onGetLayoutInflater();
  7. LayoutInflaterCompat.setFactory2(result, mChildFragmentManager.getLayoutInflaterFactory());
  8. return result;
  9. }

我们来看一下setFactory2的实现如下: 很显然当第二次进行赋值时mFactory不等于空,而是new FactoryMerger进行了赋值,将两个factory进行了合并

  1. public void setFactory2(Factory2 factory) {
  2. if (mFactorySet) {
  3. throw new IllegalStateException("A factory has already been set on this LayoutInflater");
  4. }
  5. if (factory == null) {
  6. throw new NullPointerException("Given factory can not be null");
  7. }
  8. mFactorySet = true;
  9. if (mFactory == null) {
  10. mFactory = mFactory2 = factory;
  11. } else {
  12. mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
  13. }
  14. }

看如下代码,会先对第一次赋值对mF12进行加载view操作如果返回为空,就使用第二此赋值对f1进行加载view操作.调用了Factory2的onCreateView(parent, name, context, attrs)方法

  1. private static class FactoryMerger implements Factory2 {
  2. private final Factory mF1, mF2;
  3. private final Factory2 mF12, mF22;
  4. FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
  5. mF1 = f1;
  6. mF2 = f2;
  7. mF12 = f12;
  8. mF22 = f22;
  9. }
  10. public View onCreateView(String name, Context context, AttributeSet attrs) {
  11. View v = mF1.onCreateView(name, context, attrs);
  12. if (v != null) return v;
  13. return mF2.onCreateView(name, context, attrs);
  14. }
  15. public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
  16. View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
  17. : mF1.onCreateView(name, context, attrs);
  18. if (v != null) return v;
  19. return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
  20. : mF2.onCreateView(name, context, attrs);
  21. }
  22. }
  23. .........
  24. View view;
  25. if (mFactory2 != null) {
  26. view = mFactory2.onCreateView(parent, name, context, attrs);
  27. } else if (mFactory != null) {
  28. view = mFactory.onCreateView(name, context, attrs);
  29. } else {
  30. view = null;
  31. }

综上,Fragment加载view会先加载我们自定义的Factory2,如果返回为空就使用Fragment设置的Factory2,所以Fragment 动态换肤不需要做任何处理.

状态栏扩展

状态栏的颜色改变,我们只需要通过代码动态修改状态栏的颜色即可,获取在style中的设置然后,在Activity的onCreate和动态更新皮肤的时候在SkinLayoutFactory进行调用如下方法即可.

  1. private static int[] APPCOMPAT_COLOR_PRINARY_DARK_ATIRS = {
  2. androidx.appcompat.R.attr.colorPrimaryDark
  3. };
  4. //高优先级
  5. private static int[] STATUSBAR_COLOR_ATTRS = {
  6. android.R.attr.statusBarColor, android.R.attr.navigationBarColor
  7. };
  8. //更新状态栏
  9. @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  10. public static void updateStatusBar(Activity activity) {
  11. int[] statusBarColorIDs = getResId(activity, STATUSBAR_COLOR_ATTRS);
  12. if (statusBarColorIDs[0] == 0) {//如果没有配置状态栏颜色则读取colorPrimaryDark
  13. int statusBarColorID = getResId(activity, APPCOMPAT_COLOR_PRINARY_DARK_ATIRS)[0];
  14. if (statusBarColorID != 0) {
  15. //读取皮肤包中的颜色值
  16. int color = SkinResources.getInstance().getColor(statusBarColorID);
  17. activity.getWindow().setStatusBarColor(color);
  18. }
  19. } else {
  20. activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor(statusBarColorIDs[0]));
  21. }
  22. if (statusBarColorIDs[1] != 0) {
  23. activity.getWindow().setNavigationBarColor(SkinResources.getInstance().getColor
  24. (statusBarColorIDs[1]));
  25. }
  26. }

字体扩展

如何实现对皮肤包中对字体更改呢? 我们需要定义一个自定义的属性,在attrs中自定义一个属性,然后这个属性值就是字体的文件地址一般都放在assets/font中

  1. <!-- 设置自定义属性 自定义字体 -->
  2. <attr name="skinTypeFace" format="string" />

然后在App中的style基础的theme中设置,在strings中 设置string属性,这样我们就可以在皮肤包中设置string 属性动态的修改为皮肤包资源的字体文件

  1. <style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
  2. <!-- Customize your theme here. -->
  3. <item name="colorPrimary">@color/colorPrimary</item>
  4. <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  5. <item name="colorAccent">@color/colorAccent</item>
  6. <item name="skinTypeFace">@string/typeface</item>
  7. </style>
  8. <style name="AppTheme" parent="BaseTheme"></style>

从资源中找到字体文件,代码如下:

  1. private static int[] TYPRFACE_ATTR = {R.attr.skinTypeFace};
  2. public static Typeface updateTypeface(Activity activity) {
  3. int skinTypefaceId = getResId(activity, TYPRFACE_ATTR)[0];
  4. return SkinResources.getInstance().getTypeface(skinTypefaceId);
  5. }
  6. public Typeface getTypeface(int resId) {
  7. String skinTypefacePath = getString(resId);
  8. if (TextUtils.isEmpty(skinTypefacePath)) {
  9. return Typeface.DEFAULT;
  10. }
  11. try {
  12. Typeface typeface;
  13. if (isDefalueSkin) {
  14. typeface = Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
  15. return typeface;
  16. }
  17. typeface = Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
  18. return typeface;
  19. } catch (RuntimeException e) {
  20. }
  21. return Typeface.DEFAULT;
  22. }

我们获取到Typeface 设置到TextView 中去. 这段代码是很基础的,在SkinAttribute中我们拿到了需要采集的view,更换皮肤时会对采集的view集合进行遍历,判断是否为TextView,如果是TextView则进行设置setTypeface.这样就实现了字体的替换.

  1. if (view instanceof TextView) {
  2. ((TextView) view).setTypeface(typeface);
  3. }

这样上述代码我们就实现了全局的字体更换.那么面对变态的需求如何实现局部的字体更换呢?

在上述中我们自定义了一个属性

  1. <attr name="skinTypeFace" format="string" />

然后在指定的使用不同字体的view,设置该属性

  1. <Button
  2. skinTypeFace="@string/typeface2"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:layout_marginTop="8dp"
  6. android:gravity="center"
  7. android:text="测试文字颜色与selector换肤"
  8. android:textColor="@color/selector_color_test"
  9. android:textSize="22sp"
  10. tools:ignore="MissingPrefix" />

这样我们只需要在皮肤包中的string.xml中添加typeface2,设置字体路径,然后在SkinAttribute中添加要采集的属性skinTypeFace

  1. static {
  2. mAttributes.add("background");
  3. mAttributes.add("src");
  4. mAttributes.add("textColor");
  5. mAttributes.add("drawableLeft");
  6. mAttributes.add("drawableTop");
  7. mAttributes.add("drawableRight");
  8. mAttributes.add("drawableBottom");
  9. mAttributes.add("skinTypeFace");
  10. }

在使用皮肤包的时候,applySkin判断skinTypeFace属性是否存在,如果存在则获取Typeface进行设置.

  1. case "skinTypeFace"://布局的字体更换
  2. Typeface typeface1 = SkinResources.getInstance().getTypeface(skinPair.resId);
  3. applySkinTypeface(typeface1);
  4. break;

这样我们就实现了局部字体的更换.

自定义view扩展

关于自定义view,就需要实现一个接口类,然后自己去实现换肤

  1. public interface SkinViewSupport {
  2. void applySkin();
  3. }

如自定义TabLayout,通过SkinResources来获取资源ID的值进行赋值即可

  1. public class MyTabLayout extends TabLayout implements SkinViewSupport {
  2. int tabIndicatorColorResId;
  3. int tabTextColorResId;
  4. public MyTabLayout(Context context) {
  5. this(context, null, 0);
  6. }
  7. public MyTabLayout(Context context, AttributeSet attrs) {
  8. this(context, attrs, 0);
  9. }
  10. public MyTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
  11. super(context, attrs, defStyleAttr);
  12. TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
  13. defStyleAttr, 0);
  14. tabIndicatorColorResId = a.getResourceId(R.styleable.TabLayout_tabIndicatorColor, 0);
  15. tabTextColorResId = a.getResourceId(R.styleable.TabLayout_tabTextColor, 0);
  16. a.recycle();
  17. }
  18. @Override
  19. public void applySkin() {
  20. if (tabIndicatorColorResId != 0) {
  21. int color = SkinResources.getInstance().getColor(tabIndicatorColorResId);
  22. setSelectedTabIndicatorColor(color);
  23. }
  24. if (tabTextColorResId != 0) {
  25. ColorStateList colorStateList = SkinResources.getInstance().getColorStateList(tabTextColorResId);
  26. setTabTextColors(colorStateList);
  27. }
  28. }
  29. }

同时需要在SkinAttribute中,将其添加到采集到view集合中去,确保其被添加到集合中.

  1. else if (view instanceof TextView || view instanceof SkinViewSupport) {
  2. //没有属性满足 但是需要修改字体
  3. SkinView skinView = new SkinView(view, mSkinPars);
  4. skinView.applySkin(typeface);
  5. skinViews.add(skinView);
  6. }

在更换皮肤时,遍历采集的view,在字体之后添加此方法,让自定义view自己去实现换肤

  1. private void applySkinSupportView() {
  2. if (view instanceof SkinViewSupport) {
  3. ((SkinViewSupport) view).applySkin();
  4. }
  5. }