采集
大致流程
-
监听所有activity的生命周期回调
//SkinActivityLifecycle application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
-
创建activity的时候自定义布局工厂
//SkinLayoutFactory @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) { //activity在创建的时候拿到布局加载器 LayoutInflater layoutInflater = LayoutInflater.from(activity); //创建一个皮肤工厂 SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory(); //给当前activity的布局加载器添加这个工厂 LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory); }
-
在布局工厂中寻找出所有view的可替换皮肤的标签并保存
//SkinAttribute public void load(View view, AttributeSet attributeSet){ //……具体寻找标签和保存的操作 }
具体实现
1. Application
//application中初始化皮肤管理类SkinManager
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
SkinManager.getInstance().init(this);
}
}
2. SkinManager
//皮肤管理类,用于注册activity的生命周期监听和加载替换皮肤
public class SkinManager extends Observable {
private Application application;
//单例
private static class OnSkinManager {
private static SkinManager skinManager = new SkinManager();
}
public static SkinManager getInstance() {
return OnSkinManager.skinManager;
}
/**
* 初始化
*
* @param application 当前app的application对象
*/
public void init(Application application) {
this.application = application;
//初始化一个SharedPreferences,用于存储用户使用的皮肤
SkinPreference.init(application);
//初始化皮肤资源类
SkinResources.init(application);
//注册activity的生命周期回调监听
application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
}
}
3. SkinActivityLifecycle
//activity的生命周期监听,在每一个activity创建的时候会去寻找皮肤资源并保存和替换
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
//缓存当前activity使用到的Factory,用于在该activity销毁的时候清除掉使用的Factory
private Map<Activity, SkinLayoutFactory> cacheFactoryMap = new HashMap<>();
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {
try {
//activity在创建的时候拿到布局加载器
LayoutInflater layoutInflater = LayoutInflater.from(activity);
//参考LayoutInflater源码中的字段mFactorySet的作用:
//mFactorySet如果添加过一次会变成true,再次添加LayoutInflater的时候则会抛出异常
//以下处理的目的是为了修改LayoutInflater源码中的字段mFactorySet的状态,使之不抛出异常
//得到字段mFactorySet
Field mFactorysets = LayoutInflater.class.getDeclaredField("mFactorySet");
//设置字段mFactorySet可以被访问
mFactorysets.setAccessible(true);
//设置字段mFactorySet的值为false
mFactorysets.setBoolean(layoutInflater, false);
//创建一个皮肤工厂
SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory();
//给当前activity的布局加载器添加这个工厂
LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
//添加观察者,观察者也可以使用接口代替
SkinManager.getInstance().addObserver(skinLayoutFactory);
//添加缓存,以便于activity在销毁的时候删除观察者,以免造成内存泄漏
cacheFactoryMap.put(activity, skinLayoutFactory);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
//删除观察者
SkinLayoutFactory skinLayoutFactory = cacheFactoryMap.remove(activity);
//注销观察者
SkinManager.getInstance().deleteObserver(skinLayoutFactory);
}
}
4. SkinLayoutFactory
//布局换肤的工厂类,用于采集需要换肤的view
public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
//系统原生view的路径,属于这些路径的才可以换肤,减少消耗和判断
private static final String[] mClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit.",
};
//获取view的class的构造方法的参数,一个view有多个构造方法,每个构造方法的参数不同
private static final Class[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};
//缓存已经通过反射得到某个view的构造函数,例如textview、button的构造方法,减少内存开销和加快业务流程
private static final HashMap<String, Constructor<? extends View>> mConstructorCache = new HashMap<>();
//view属性处理类
private SkinAttribute skinAttribute;
//初始化的时候去创建SkinAttribute类
public SkinLayoutFactory() {
this.skinAttribute = new SkinAttribute();
}
//在创建view的时候去采集view,这里一个layout.xml文件中的所有view标签都会在创建的时候进入该方法
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
//如果是系统的view,则可以通过全类名得到view
View view = createViewFromTag(s, context, attributeSet);
//如果通过全类名拿不到view,则说明当前view是自定义view
//如果是自定义view则调用createview方法
if (view == null) {
view = createView(s, context, attributeSet);
}
//将当前view的所有参数遍历,拿到符合换肤的参数以及对应的resid
//第一步采集view,在这里已经完成
skinAttribute.load(view, attributeSet);
return view;
}
/**
* 创建原生view
* @param name 标签名。例如:TextView;Button
* @param context 上下文
* @param attributeSet 标签参数
* @return
*/
private View createViewFromTag(String name, Context context, AttributeSet attributeSet) {
//检查当前view是否是自定义的view或者android的新view
//例如:自定义的,com.xxx.xxx.CustormView
//系统的,com.androidx.action.AtionBar
if (name.contains(".")) {
//如果是自定义的或者是系统view则另做处理
return null;
} else {
//这里获取原生view
View view = null;
//循环去判断当前view的前缀,例如Layout的前缀是android.widget.
//这里拼接出view的全类名进行反射
//如果通过反射拿到了view,说明当前全类名是正确的
//如果通过反射抛出异常了则说明当前全类名是错误的
//只有通过反射拿到了正确的构造方法才能通过构造方法new出当前view对象
for (int i = 0; i < mClassPrefixList.length; i++) {
//拼接如果是原生标签,则去创建,获取到全类名
view = createView(mClassPrefixList[i] + name, context, attributeSet);
if (view != null) {//通过全类名拿到了view,直接返回出去
break;
}
}
return view;
}
}
/**
* 创建一个view
*
* @param name 全类名
* @param context 上下文
* @param attributeSet 标签参数
* @return
*/
private View createView(String name, Context context, AttributeSet attributeSet) {
//添加缓存,一个xml中如果有多个重复的view,例如多个textview或者button,则缓存的作用就体现出来了
//只要是相同的view,则不需要每次都去通过反射拿view
Constructor<? extends View> constructor = mConstructorCache.get(name);
//没有缓存的构造方法则创建
if (constructor == null) {
try {
//通过全类名拿到class对象
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
//获取到当前class对象中的构造方法
constructor = aClass.getConstructor(mConstructorSignature);
//将构造方法缓存起来
mConstructorCache.put(name, constructor);
} catch (Exception e) {
//如果抛出异常,说明这个全类名不正确,则直接返回null
return null;
}
}
//构造方法获取到了
if (null != constructor) {
try {
//这个操作相当于new 一个对象,new的时候传入构造方法的参数
return constructor.newInstance(context, attributeSet);
} catch (Exception e) {
//如果抛出异常,说明这个构造方法和传递进来的参数不正确
//一般view的构造方法都有一个是:
//public xxx(Context context, AttributeSet attrs){}
return null;
}
}
return null;
}
5. SkinAttribute
//Describe: view的属性处理类,采集view和替换资源
public class SkinAttribute {
//需要换肤的属性集合,已经找出来了
private static final List<String> mAttribute = new ArrayList<>();
//需要换肤的view
private List<SkinView> skinViews = new ArrayList<>();
//以下这些事需要换肤的属性,如果自己需要替换那些标签属性,则可以继续添加
static {
mAttribute.add("background");
mAttribute.add("src");
mAttribute.add("textColor");
mAttribute.add("drawableLeft");
mAttribute.add("drawableRight");
mAttribute.add("drawableTop");
mAttribute.add("drawableBottom");
}
/**
* 寻找view的可换肤属性并缓存起来
*
* @param view view
* @param attributeSet 属性
*/
public void load(View view, AttributeSet attributeSet) {
List<SkinPain> skinPains = new ArrayList<>();
//先筛选一遍,需要修改属性的才往下走
for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
//获取属性名字
String attributeName = attributeSet.getAttributeName(i);
//如果当前属性名字是需要修改的属性则去处理
if (mAttribute.contains(attributeName)) {
//拿到属性值,@2130968664
String attributeValue = attributeSet.getAttributeValue(i);
//写死的色值,暂时不修改
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
//?开头的是系统参数,如下修改
if (attributeValue.startsWith("?")) {
//拿到去掉?后的值。
//强转成int,系统编译后的值为int型,即R文件中的id,例如:?123456
//系统的资源id下只有一个标签,类似于resource标签下的style标签,但是style下只有一个item标签
//所以只拿第一个attrid;
int attrId = Integer.parseInt(attributeValue.substring(1));
//获得资源id
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
//其他正常的标签则直接拿到@color/black中在R文件中的@123456
//去掉@后的值则可以直接通过setColor(int resId);传入
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
//保存属性名字和对应的id用于换肤使用
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
}
}
}
//如果当前view检查出来了需要替换的资源id,则保存起来
//上面的循环已经循环出了当前view中的所有需要换肤的标签和redId
if (!skinPains.isEmpty()) {
SkinView skinView = new SkinView(view, skinPains);
skinViews.add(skinView);
}
}
//保存:参数->id
public class SkinPain {
String attrubuteName;//参数名
int resId;//资源id
public SkinPain(String attrubuteName, int resId) {
this.attrubuteName = attrubuteName;
this.resId = resId;
}
}
//保存view与之对应的SkinPain对象
public class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
}
}
制作
- 一个没有java代码的apk包,里面有所有相对应名字的资源文件
- 放到服务器或者手机sd卡中用于加载并替换
替换
注意事项
- 制作好的皮肤包需要先下载到手机sd卡中,也可在app中内置几套默认皮肤
- 换肤需要读写sd卡权限
- 注意内存泄漏问题
1. 加载皮肤包资源文件
//换肤
public void change(View view) {
//拿到sd卡中的皮肤包
String path = Environment.getExternalStorageDirectory() + File.separator + "skin_apk_1.apk";
//加载
SkinManager.getInstance().loadSkin(path);
}
2.loadSkin(String filePath)
public class SkinManager extends Observable {
/**
* 加载皮肤,并保存当前使用的皮肤
*
* @param skinPath 皮肤路径 如果为空则使用默认皮肤
*/
public void loadSkin(String skinPath) {
//如果传递进来的皮肤文件路径是null,则表示使用默认的皮肤
if (TextUtils.isEmpty(skinPath)) {
//存储默认皮肤
SkinPreference.getInstance().setSkin("");
//清空皮肤资源属性
SkinResources.getInstance().reset();
} else {
//传递进来的有皮肤包的文件路径则加载
try {
//皮肤包文件不存在
if (!new File(skinPath).exists()) {
Toast.makeText(application, "文件不存在", Toast.LENGTH_LONG).show();
return;
}
//反射创建AssetManager
AssetManager assetManager = AssetManager.class.newInstance();
//通过反射得到方法:public int addAssetPath(String path)方法
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//设置当前方法可以被访问
addAssetPath.setAccessible(true);
//调用该方法,传入皮肤包文件路径
addAssetPath.invoke(assetManager, skinPath);
//得到当前app的Resources
Resources appResource = application.getResources();
//根据当前的显示与配置(横竖屏、语言等)创建皮肤包的Resources
Resources skinResource = new Resources(
assetManager,
appResource.getDisplayMetrics(),
appResource.getConfiguration());
//保存当前用户设置的皮肤包路径
SkinPreference.getInstance().setSkin(skinPath);
//获取外部皮肤包的包名,首先得到PackageManager对象
PackageManager packageManager = application.getPackageManager();
//通过getPackageArchiveInfo得到外部皮肤包文件的包信息
PackageInfo info = packageManager.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
if (info == null) {
//一般解析失败的原因有:
//1,没有sd卡权限
//2,皮肤包打包有问题
Toast.makeText(application, "解析皮肤包失败", Toast.LENGTH_LONG).show();
return;
}
//得到皮肤包包名
String packageName = info.packageName;
//开始设置皮肤
SkinResources.getInstance().applySkin(skinResource, packageName);
} catch (Exception e) {
e.printStackTrace();
}
}
//一下观察者操作可以用接口代替
//通知所有采集的View更新皮肤
setChanged();
//被观察者通知所有观察者
notifyObservers(null);
}
}
3. SkinResources
/**
* 皮肤资源类
* 用来加载本地默认的资源或者皮肤包中的资源
*/
public class SkinResources {
private static SkinResources instance;
//皮肤包的资源
private Resources mSkinResources;
//皮肤包包名
private String mSkinPkgName;
//是否加载默认的皮肤资源
private boolean isDefaultSkin = true;
//默认的皮肤资源
private Resources mAppResources;
private SkinResources(Context context) {
mAppResources = context.getResources();
}
public static void init(Context context) {
if (instance == null) {
synchronized (SkinResources.class) {
if (instance == null) {
instance = new SkinResources(context);
}
}
}
}
public static SkinResources getInstance() {
return instance;
}
public void reset() {
mSkinResources = null;
mSkinPkgName = "";
isDefaultSkin = true;
}
public void applySkin(Resources resources, String pkgName) {
mSkinResources = resources;
mSkinPkgName = pkgName;
//是否使用默认皮肤
isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
}
/**
* 查找资源的关键方法
* 通过当前包的资源id得到资源名和属性名,然后再皮肤包中查找对应的资源id并返回
* @param resId
* @return
*/
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮肤包中不一定就是 当前程序的 id
//获取对应id 在当前的名称 colorPrimary
String resName = mAppResources.getResourceEntryName(resId);
String resType = mAppResources.getResourceTypeName(resId);
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
return skinId;
}
public int getColor(int resId) {
if (isDefaultSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
/**
* 可能是Color 也可能是drawable
*
* @return
*/
public Object getBackground(int resId) {
String resourceTypeName = mAppResources.getResourceTypeName(resId);
if (resourceTypeName.equals("color")) {
return getColor(resId);
} else {
// drawable
return getDrawable(resId);
}
}
public String getString(int resId) {
try {
if (isDefaultSkin) {
return mAppResources.getString(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getString(resId);
}
return mSkinResources.getString(skinId);
} catch (Resources.NotFoundException e) {
}
return null;
}
public Typeface getTypeface(int resId) {
String skinTypefacePath = getString(resId);
if (TextUtils.isEmpty(skinTypefacePath)) {
return Typeface.DEFAULT;
}
try {
//使用皮肤包
if (isDefaultSkin) {
return Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
}
return Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
} catch (RuntimeException e) {
}
return Typeface.DEFAULT;
}
4. 观察者接收到了修改皮肤的通知
public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer{
//通知观察者,在这里接收到了消息
@Override
public void update(Observable o, Object arg) {
//更换皮肤
skinAttribute.applySkin();
}
}
5. 修改皮肤
public class SkinAttribute {
//保存的所有的view进行替换皮肤
public void applySkin() {
//所有保存的需要修改皮肤的view的SkinView对象
for (SkinView skinView : skinViews) {
skinView.appSkin();
}
}
//保存view与之对应的SkinPain对象
public class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
//替换皮肤资源,这里是实际的替换操作
//通过SkinResources对象获得皮肤包的资源
public void appSkin() {
//训话所有记录的需要换服的skinpain对象
for (SkinPain skinPain : skinPains) {
Drawable left = null, right = null, top = null, bottom = null;
switch (skinPain.attrubuteName) {
case "background"://更换背景色
//获得resid的资源
Object background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {
view.setBackgroundColor((int) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src"://更换图片
background = SkinResources.getInstance().getBackground(skinPain.resId);
if (view instanceof ImageView) {
ImageView imageView = ((ImageView) view);
if (background instanceof Integer) {
imageView.setImageDrawable(new ColorDrawable((Integer) background));
} else if (background instanceof Drawable) {
imageView.setImageDrawable((Drawable) background);
}
}
break;
case "textColor"://更换字体颜色
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPain.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPain.resId);
((TextView) view).setCompoundDrawables(left, top, right, bottom);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
default:
break;
}
}
}
}
}
Fragment换肤
根据Android源码中Fragment的Factory传递可以看出,最后Fragment的Factory和Activity的Factory会合并,所以Fragment换肤不需要额外操作
源码分析
//fragment的方法
//1,在创建布局加载器的时候传递进去Factory
@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();
//2,继续往下走
LayoutInflaterCompat.setFactory2(result, mChildFragmentManager.getLayoutInflaterFactory());
return result;
}
//2,给布局加载器赋值Factory
public static void setFactory2(
@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
//3,继续往下走
inflater.setFactory2(factory);
if (Build.VERSION.SDK_INT < 21) {
final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
// The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
// We will now try and force set the merged factory to mFactory2
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
// Else, we will force set the original wrapped Factory2
forceSetFactory2(inflater, factory);
}
}
}
//3,判断当前工厂是否为null,若为null,则直接赋值;若!=null,则进行一个工厂替换操作
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 {
//4,工厂替换操作
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
//4,工厂替换,将fragment的工厂替换成activity的工厂
private static class FactoryMerger implements Factory2 {
FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
mF1 = f1;
mF2 = f2;
mF12 = f12;
mF22 = f22;
}
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);
}
}
导航栏换肤
//兼容,如果状态栏的色值没有拿到,则使用系统默认的
private static int[] a = {R.attr.colorPrimaryDark};
//状态栏和navigationBar
private static int[] b = {android.R.attr.statusBarColor, android.R.attr.navigationBarColor};
/**
* 修改导航栏的颜色
*
* @param activity
*/
public static void updateStatusBarColor(Activity activity) {
//5.0以上才能修改
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int[] resbarIds = getResId(activity, b);
//如果有该值,则可以替换状态栏颜色
if (resbarIds[0] != 0) {
activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor(resbarIds[0]));
} else {
//没有值,则使用兼容色值colorPrimaryDark
int resbarId = getResId(activity, a)[0];
if (resbarId != 0) {
activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor(resbarId));
}
}
//底部NavigationBar如果存在则也要改变色值
if (resbarIds[1] != 0) {
activity.getWindow().setNavigationBarColor(SkinResources.getInstance().getColor(resbarIds[1]));
}
}
}
字体替换
全局字体替换
public class SkinThemeUtils {
//默认字体
private static int[] c = {R.attr.skinTypeface};
/**
* 更新字体
*
* @param activity
*/
public static Typeface getSkinTypeface(Activity activity) {
int skinTypefaceId = getResId(activity, c)[0];
return SkinResources.getInstance().getTypeface(skinTypefaceId);
}
/**
* 根据参数的值拿到参数的资源id
*
* @param context
* @param attrs 参数值
* @return
*/
public static int[] getResId(Context context, int[] attrs) {
int[] ints = new int[attrs.length];
//获得样式属性
TypedArray typedArray = context.obtainStyledAttributes(attrs);
for (int i = 0; i < typedArray.length(); i++) {
ints[i] = typedArray.getResourceId(i, 0);
}
typedArray.recycle();
return ints;
}
}
public class SkinAttribute {
//添加字体标签
static {
mAttribute.add("skinTypeface");
}
/**
* 加载view的属性缓存起来
*
* @param view view
* @param attributeSet 属性
*/
public void load(View view, AttributeSet attributeSet) {
//其他代码
//...
//如果当前view检查出来了需要替换的资源id,则保存起来
if (!skinPains.isEmpty() || view instanceof TextView) {
SkinView skinView = new SkinView(view, skinPains);
//在收集view的标签的时候就进行替换字体的操作
skinView.applySkin(typeface);
skinViews.add(skinView);
}
}
//保存的所有的view进行替换皮肤,这里传递进来全局保存的字体对象
public void applySkin() {
for (SkinView skinView : skinViews) {
skinView.applySkin(typeface);
}
}
//设置字体
public void setTypeface(Typeface typeface) {
this.typeface = typeface;
}
//保存view与之对应的SkinPain对象
public class SkinView {
//其他代码
//...
//替换皮肤资源
public void applySkin(Typeface typeface) {
applyTypeface(typeface);
for (SkinPain skinPain : skinPains) {
switch (skinPain.attrubuteName) {
//其他代码
//...
case "skinTypeface":
applyTypeface(SkinResources.getInstance().getTypeface(skinPain.resId));
break;
default:
break;
}
}
}
//替换字体
private void applyTypeface(Typeface typeface) {
if (view instanceof TextView) {
((TextView) view).setTypeface(typeface);
}
}
}
}
public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
//通知观察者,在这里接收到了消息
@Override
public void update(Observable o, Object arg) {
//其他代码
//...
Typeface typeface=SkinThemeUtils.getSkinTypeface(activity);
skinAttribute.setTypeface(typeface);
//注意,设置完typeface之后才能去替换皮肤
//更换皮肤
skinAttribute.applySkin();
}
}
attrs.xml中定义
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="skinTypeface" format="string" />
</resources>
styles.xlm中定义
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!--其他属性-->
<item name="skinTypeface">@string/typeface</item>
</style>
</resources>
strings.xml中定义
<resources>
<!--用于默认的全局字体,在base application theme中定义的字段-->
<string name="typeface">font/hwxk.ttf</string>
</resources>
单个字体替换
同全局字体替换一样
strings.xml中定义
<resources>
<!-- 用于单个字体替换,在需要设置字体的空间中过去设置-->
<string name="typeface_2">font/wryh.ttf</string>
</resources>
在布局中使用
<TextView
skinTypeface="@string/typeface_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
在换肤替换字体的时候,回去找关键字“skinTypeface”,我们在TextView中定了该标签,则该view就会被记录下来,并且去皮肤包中寻找同样的标签@string/typeface_2所对应的字体文件路径
自定义View换肤
定义自定义view换肤的监听接口
/**
* Describe:自定义view用到的换肤接口
*/
public interface SkinViewSupport {
void applySkin();
}
自定义属性
<resources>
<declare-styleable name="CircleView">
<attr name="circleTextColor" format="string" />
</declare-styleable>
</resources>
自定义view
//自定义view要实现换肤接口
public class CircleView extends View implements SkinViewSupport {
private int colorResId;
private Paint mTextPain;
public CircleView(Context context) {
super(context, null);
}
//构造方法
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//拿到自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
colorResId = typedArray.getColor(R.styleable.CircleView_circleTextColor, Color.RED);
typedArray.recycle();
//画一个圆
mTextPain = new Paint();
//设置颜色
mTextPain.setColor(getResources().getColor(colorResId));
//抗锯齿
mTextPain.setAntiAlias(true);
//文本相对于原点中见
mTextPain.setTextAlign(Paint.Align.CENTER);
}
//实现接口
@Override
public void applySkin() {
if (colorResId != 0) {
int color = SkinResources.getInstance().getColor(colorResId);
mTextPain.setColor(color);
//更新view
invalidate();
}
}
}
修改SkinAttribute
public class SkinAttribute {
/**
* 加载view的属性缓存起来
*
* @param view view
* @param attributeSet 属性
*/
public void load(View view, AttributeSet attributeSet) {
//其他代码
//...
//如果当前view检查出来了需要替换的资源id,则保存起来
if (!skinPains.isEmpty() || view instanceof TextView || view instanceof SkinViewSupport) {
SkinView skinView = new SkinView(view, skinPains);
skinView.applySkin(typeface);
skinViews.add(skinView);
}
}
//替换皮肤资源
public void applySkin(Typeface typeface) {
//其他代码
//...
applySkinSupport();
//其他代码
//...
}
//替换自定义view皮肤
private void applySkinSupport() {
if (view instanceof SkinViewSupport) {
//这里会调用自定义view中的接口
((SkinViewSupport) view).applySkin();
}
}
}
夜间/日间换肤
AppcompatDelegate的四种模式
-
MODE_NIGHT_YES:夜间模式
-
MODE_NIGHT_NO:日间模式
-
MODE_NIGHT_FOLLOW_SYSTEM:根据系统设置决定是否设置夜间模式
-
MODE_NIGHT_AUTO:根据当前时间来自动切换夜间/日间模式
在项目中新建vaules-night,并生成相应的夜间模式文件,例如:
colors.xml
strings.xml
Application中设置全局的夜间/日间模式
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
//初始化app的时候就去设置日间/夜间模式
//根据app上次退出的状态来判断是否需要设置夜间模式,提前在SharedPreference中存了一个是
// 否是夜间模式的boolean值
boolean isNightMode = NightModeConfig.getInstance().getNightMode(this);
if (isNightMode) {//夜间
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {//日间
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
}
}
当用户修改了日间/夜间模式的时候在每个activity中调用以下代码
/**
* 夜间模式
*/
public void night() {
//获取当前的夜间/日间模式
int currentMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
//如果当前模式不是夜间,则进行替换
if (currentMode != Configuration.UI_MODE_NIGHT_YES) {
//保存夜间模式状态,Application中可以根据这个值判断是否设置夜间模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
//ThemeConfig主题配置,这里只是保存了是否是夜间模式的boolean值
NightModeConfig.getInstance().setNightMode(getApplicationContext(), true);
recreate();//需要recreate才能生效
}
}
/**
* 日间模式
*/
public void day() {
//获取当前的夜间/日间模式
int currentMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
//如果当前模式不是日间,则进行替换
if (currentMode == Configuration.UI_MODE_NIGHT_YES) {
//保存夜间模式状态,Application中可以根据这个值判断是否设置夜间模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
//ThemeConfig主题配置,这里只是保存了是否是夜间模式的boolean值
NightModeConfig.getInstance().setNightMode(getApplicationContext(), false);
recreate();//需要recreate才能生效
}
}
hook
android 9.0 以后部分反射无法使用,使用hook替代