在上一节中讲解了动态换肤的原理以及动态换肤的基本实现,本篇文章来讲解动态换肤扩展(Fragment、状态栏、自定义view、字体).
Fragment 扩展
Fragment 不用设置fractory2,只要activity中设置了fractory2就不用在设置就可以实现换肤,所以fragment不用任何修改.那么是为什么呢?
我们都知道在fragment是通过,onCreateView 传递一个LayoutInflater,来实现view的加载
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {return inflater.inflate(R.layout.fragment_music, container, false)}
那么这个LayoutInflater 是从哪里来的呢? 相信大家也能猜出个大概,其实就是和加载Fragment的Activity有关,我们看一下系统的Fragment源码
@Deprecated@NonNull@RestrictTo(LIBRARY_GROUP_PREFIX)public LayoutInflater getLayoutInflater(@Nullable Bundle savedFragmentState) {if (mHost == null) {throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the "+ "Fragment is attached to the FragmentManager.");}LayoutInflater result = mHost.onGetLayoutInflater();LayoutInflaterCompat.setFactory2(result, mChildFragmentManager.getLayoutInflaterFactory());return result;}
在上述代码中,Fragment通过 mHost 的 onGetLayoutInflater 获取的,我们来看一下这个 mHost 是什么? mhost是一个接口类FragmentHostCallback. 代码如下,可以看出Fragment的很多操作都和mHost有关,包括获取上下文等操作,那么我们就可以断定这个mHost 肯定是从Activity中传递过来的
// Host this fragment is attached to.FragmentHostCallback mHost;@Nullablepublic Context getContext() {return mHost == null ? null : mHost.getContext();}@Nullablefinal public FragmentActivity getActivity() {return mHost == null ? null : (FragmentActivity) mHost.getActivity();}.....
FragmentActivity 的源码如下: FrgmentActivity 的代码中实现了FragmentHostCallback,那么Framgment的 mHost 其实就是FragmentActivity的实现接口类
class HostCallbacks extends FragmentHostCallback<FragmentActivity> implementsViewModelStoreOwner,OnBackPressedDispatcherOwner {public HostCallbacks() {super(FragmentActivity.this /*fragmentActivity*/);}.....@Override@NonNullpublic LayoutInflater onGetLayoutInflater() {return FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.this);}......}
也就是说mHost.onGetLayoutInflater();其实调用的就是如下代码
public LayoutInflater onGetLayoutInflater() {return FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.this);}/*** Convenience for calling* {@link android.view.Window#getLayoutInflater}.*/@NonNullpublic LayoutInflater getLayoutInflater() {return getWindow().getLayoutInflater();}
那么具体实现的LayoutInflater就是PhoneLayoutInfalter,代码如下:
public class PhoneLayoutInflater extends LayoutInflater {private static final String[] sClassPrefixList = {"android.widget.","android.webkit.","android.app."};/*** Instead of instantiating directly, you should retrieve an instance* through {@link Context#getSystemService}** @param context The Context in which in which to find resources and other* application-specific things.** @see Context#getSystemService*/public PhoneLayoutInflater(Context context) {super(context);}protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {super(original, newContext);}/** Override onCreateView to instantiate names that correspond to thewidgets known to the Widget factory. If we don't find a match,call through to our super class.*/@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {for (String prefix : sClassPrefixList) {try {View view = createView(name, prefix, attrs);if (view != null) {return view;}} catch (ClassNotFoundException e) {// In this case we want to let the base class take a crack// at it.}}return super.onCreateView(name, attrs);}public LayoutInflater cloneInContext(Context newContext) {return new PhoneLayoutInflater(this, newContext);}}
PhoneLayoutInfalter实现流程和LayoutInfalter的流程是一样的,只不过通过cloneInContext,重新new了一个LayoutInflater 也相当于克隆了一个LayoutInflater,调用了Layoutflater的一个构造方法,同时也将factory2进行了赋值.
protected LayoutInflater(LayoutInflater original, Context newContext) {mContext = newContext;mFactory = original.mFactory;mFactory2 = original.mFactory2;mPrivateFactory = original.mPrivateFactory;setFilter(original.mFilter);}
那么我们再回到Fragment中的getLayoutInflater中,不知道大家有没有发现Fragment又重新对factory2进行了赋值,那么为什么我们还能动态换肤成功呢?
public LayoutInflater getLayoutInflater(@Nullable Bundle savedFragmentState) {if (mHost == null) {throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the "+ "Fragment is attached to the FragmentManager.");}LayoutInflater result = mHost.onGetLayoutInflater();LayoutInflaterCompat.setFactory2(result, mChildFragmentManager.getLayoutInflaterFactory());return result;}
我们来看一下setFactory2的实现如下: 很显然当第二次进行赋值时mFactory不等于空,而是new FactoryMerger进行了赋值,将两个factory进行了合并
public void setFactory2(Factory2 factory) {if (mFactorySet) {throw new IllegalStateException("A factory has already been set on this LayoutInflater");}if (factory == null) {throw new NullPointerException("Given factory can not be null");}mFactorySet = true;if (mFactory == null) {mFactory = mFactory2 = factory;} else {mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);}}
看如下代码,会先对第一次赋值对mF12进行加载view操作如果返回为空,就使用第二此赋值对f1进行加载view操作.调用了Factory2的onCreateView(parent, name, context, attrs)方法
private static class FactoryMerger implements Factory2 {private final Factory mF1, mF2;private final Factory2 mF12, mF22;FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {mF1 = f1;mF2 = f2;mF12 = f12;mF22 = f22;}public View onCreateView(String name, Context context, AttributeSet attrs) {View v = mF1.onCreateView(name, context, attrs);if (v != null) return v;return mF2.onCreateView(name, context, attrs);}public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs): mF1.onCreateView(name, context, attrs);if (v != null) return v;return mF22 != null ? mF22.onCreateView(parent, name, context, attrs): mF2.onCreateView(name, context, attrs);}}.........View view;if (mFactory2 != null) {view = mFactory2.onCreateView(parent, name, context, attrs);} else if (mFactory != null) {view = mFactory.onCreateView(name, context, attrs);} else {view = null;}
综上,Fragment加载view会先加载我们自定义的Factory2,如果返回为空就使用Fragment设置的Factory2,所以Fragment 动态换肤不需要做任何处理.
状态栏扩展
状态栏的颜色改变,我们只需要通过代码动态修改状态栏的颜色即可,获取在style中的设置然后,在Activity的onCreate和动态更新皮肤的时候在SkinLayoutFactory进行调用如下方法即可.
private static int[] APPCOMPAT_COLOR_PRINARY_DARK_ATIRS = {androidx.appcompat.R.attr.colorPrimaryDark};//高优先级private static int[] STATUSBAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr.navigationBarColor};//更新状态栏@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)public static void updateStatusBar(Activity activity) {int[] statusBarColorIDs = getResId(activity, STATUSBAR_COLOR_ATTRS);if (statusBarColorIDs[0] == 0) {//如果没有配置状态栏颜色则读取colorPrimaryDarkint statusBarColorID = getResId(activity, APPCOMPAT_COLOR_PRINARY_DARK_ATIRS)[0];if (statusBarColorID != 0) {//读取皮肤包中的颜色值int color = SkinResources.getInstance().getColor(statusBarColorID);activity.getWindow().setStatusBarColor(color);}} else {activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor(statusBarColorIDs[0]));}if (statusBarColorIDs[1] != 0) {activity.getWindow().setNavigationBarColor(SkinResources.getInstance().getColor(statusBarColorIDs[1]));}}
字体扩展
如何实现对皮肤包中对字体更改呢? 我们需要定义一个自定义的属性,在attrs中自定义一个属性,然后这个属性值就是字体的文件地址一般都放在assets/font中
<!-- 设置自定义属性 自定义字体 --><attr name="skinTypeFace" format="string" />
然后在App中的style基础的theme中设置,在strings中
<style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar"><!-- Customize your theme here. --><item name="colorPrimary">@color/colorPrimary</item><item name="colorPrimaryDark">@color/colorPrimaryDark</item><item name="colorAccent">@color/colorAccent</item><item name="skinTypeFace">@string/typeface</item></style><style name="AppTheme" parent="BaseTheme"></style>
从资源中找到字体文件,代码如下:
private static int[] TYPRFACE_ATTR = {R.attr.skinTypeFace};public static Typeface updateTypeface(Activity activity) {int skinTypefaceId = getResId(activity, TYPRFACE_ATTR)[0];return SkinResources.getInstance().getTypeface(skinTypefaceId);}public Typeface getTypeface(int resId) {String skinTypefacePath = getString(resId);if (TextUtils.isEmpty(skinTypefacePath)) {return Typeface.DEFAULT;}try {Typeface typeface;if (isDefalueSkin) {typeface = Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);return typeface;}typeface = Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);return typeface;} catch (RuntimeException e) {}return Typeface.DEFAULT;}
我们获取到Typeface 设置到TextView 中去. 这段代码是很基础的,在SkinAttribute中我们拿到了需要采集的view,更换皮肤时会对采集的view集合进行遍历,判断是否为TextView,如果是TextView则进行设置setTypeface.这样就实现了字体的替换.
if (view instanceof TextView) {((TextView) view).setTypeface(typeface);}
这样上述代码我们就实现了全局的字体更换.那么面对变态的需求如何实现局部的字体更换呢?
在上述中我们自定义了一个属性
<attr name="skinTypeFace" format="string" />
然后在指定的使用不同字体的view,设置该属性
<ButtonskinTypeFace="@string/typeface2"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="8dp"android:gravity="center"android:text="测试文字颜色与selector换肤"android:textColor="@color/selector_color_test"android:textSize="22sp"tools:ignore="MissingPrefix" />
这样我们只需要在皮肤包中的string.xml中添加typeface2,设置字体路径,然后在SkinAttribute中添加要采集的属性skinTypeFace
static {mAttributes.add("background");mAttributes.add("src");mAttributes.add("textColor");mAttributes.add("drawableLeft");mAttributes.add("drawableTop");mAttributes.add("drawableRight");mAttributes.add("drawableBottom");mAttributes.add("skinTypeFace");}
在使用皮肤包的时候,applySkin判断skinTypeFace属性是否存在,如果存在则获取Typeface进行设置.
case "skinTypeFace"://布局的字体更换Typeface typeface1 = SkinResources.getInstance().getTypeface(skinPair.resId);applySkinTypeface(typeface1);break;
这样我们就实现了局部字体的更换.
自定义view扩展
关于自定义view,就需要实现一个接口类,然后自己去实现换肤
public interface SkinViewSupport {void applySkin();}
如自定义TabLayout,通过SkinResources来获取资源ID的值进行赋值即可
public class MyTabLayout extends TabLayout implements SkinViewSupport {int tabIndicatorColorResId;int tabTextColorResId;public MyTabLayout(Context context) {this(context, null, 0);}public MyTabLayout(Context context, AttributeSet attrs) {this(context, attrs, 0);}public MyTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,defStyleAttr, 0);tabIndicatorColorResId = a.getResourceId(R.styleable.TabLayout_tabIndicatorColor, 0);tabTextColorResId = a.getResourceId(R.styleable.TabLayout_tabTextColor, 0);a.recycle();}@Overridepublic void applySkin() {if (tabIndicatorColorResId != 0) {int color = SkinResources.getInstance().getColor(tabIndicatorColorResId);setSelectedTabIndicatorColor(color);}if (tabTextColorResId != 0) {ColorStateList colorStateList = SkinResources.getInstance().getColorStateList(tabTextColorResId);setTabTextColors(colorStateList);}}}
同时需要在SkinAttribute中,将其添加到采集到view集合中去,确保其被添加到集合中.
else if (view instanceof TextView || view instanceof SkinViewSupport) {//没有属性满足 但是需要修改字体SkinView skinView = new SkinView(view, mSkinPars);skinView.applySkin(typeface);skinViews.add(skinView);}
在更换皮肤时,遍历采集的view,在字体之后添加此方法,让自定义view自己去实现换肤
private void applySkinSupportView() {if (view instanceof SkinViewSupport) {((SkinViewSupport) view).applySkin();}}
