在上一节中讲解了动态换肤的原理以及动态换肤的基本实现,本篇文章来讲解动态换肤扩展(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;
@Nullable
public Context getContext() {
return mHost == null ? null : mHost.getContext();
}
@Nullable
final public FragmentActivity getActivity() {
return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}
.....
FragmentActivity
的源码如下: FrgmentActivity 的代码中实现了FragmentHostCallback,那么Framgment的 mHost
其实就是FragmentActivity的实现接口类
class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements
ViewModelStoreOwner,
OnBackPressedDispatcherOwner {
public HostCallbacks() {
super(FragmentActivity.this /*fragmentActivity*/);
}
.....
@Override
@NonNull
public 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}.
*/
@NonNull
public 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 the
widgets 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) {//如果没有配置状态栏颜色则读取colorPrimaryDark
int 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,设置该属性
<Button
skinTypeFace="@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();
}
@Override
public 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();
}
}