采集

大致流程

  1. 监听所有activity的生命周期回调

    //SkinActivityLifecycle
    application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
    
  2. 创建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);
    }
    
  3. 在布局工厂中寻找出所有view的可替换皮肤的标签并保存

    //SkinAttribute
    public void load(View view, AttributeSet attributeSet){//……具体寻找标签和保存的操作
    }
    

具体实现

1. Application

//application中初始化皮肤管理类SkinManager
public class App extends Application {@Overridepublic 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销毁的时候清除掉使用的Factoryprivate Map<Activity, SkinLayoutFactory> cacheFactoryMap = new HashMap<>();@Overridepublic void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {try {//activity在创建的时候拿到布局加载器LayoutInflater layoutInflater = LayoutInflater.from(activity);//参考LayoutInflater源码中的字段mFactorySet的作用://mFactorySet如果添加过一次会变成true,再次添加LayoutInflater的时候则会抛出异常//以下处理的目的是为了修改LayoutInflater源码中的字段mFactorySet的状态,使之不抛出异常//得到字段mFactorySetField mFactorysets = LayoutInflater.class.getDeclaredField("mFactorySet");//设置字段mFactorySet可以被访问mFactorysets.setAccessible(true);//设置字段mFactorySet的值为falsemFactorysets.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();}}@Overridepublic 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@Overridepublic View onCreateView(@Nullable View parent, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {//如果是系统的view,则可以通过全类名得到viewView 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.AtionBarif (name.contains(".")) {//如果是自定义的或者是系统view则另做处理return null;} else {//这里获取原生viewView 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,则不需要每次都去通过反射拿viewConstructor<? 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) {//如果抛出异常,说明这个全类名不正确,则直接返回nullreturn 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<>();//需要换肤的viewprivate 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)) {//拿到属性值,@2130968664String 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));//获得资源idresId = 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中的所有需要换肤的标签和redIdif (!skinPains.isEmpty()) {SkinView skinView = new SkinView(view, skinPains);skinViews.add(skinView);}}//保存:参数->idpublic class SkinPain {String attrubuteName;//参数名int resId;//资源idpublic 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;}}
}

制作

  1. 一个没有java代码的apk包,里面有所有相对应名字的资源文件
  2. 放到服务器或者手机sd卡中用于加载并替换

替换

注意事项

  1. 制作好的皮肤包需要先下载到手机sd卡中,也可在app中内置几套默认皮肤
  2. 换肤需要读写sd卡权限
  3. 注意内存泄漏问题

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;}//反射创建AssetManagerAssetManager 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的ResourcesResources appResource = application.getResources();//根据当前的显示与配置(横竖屏、语言等)创建皮肤包的ResourcesResources 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 在当前的名称 colorPrimaryString 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 {// drawablereturn 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{//通知观察者,在这里接收到了消息@Overridepublic 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 mFactory2forceSetFactory2(inflater, (LayoutInflater.Factory2) f);} else {// Else, we will force set the original wrapped Factory2forceSetFactory2(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};//状态栏和navigationBarprivate 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 {//没有值,则使用兼容色值colorPrimaryDarkint 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 {//通知观察者,在这里接收到了消息@Overridepublic 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>

在布局中使用

<TextViewskinTypeface="@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);}//实现接口@Overridepublic void applySkin() {if (colorResId != 0) {int color = SkinResources.getInstance().getColor(colorResId);mTextPain.setColor(color);//更新viewinvalidate();}}
}

修改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的四种模式

  1. MODE_NIGHT_YES:夜间模式

  2. MODE_NIGHT_NO:日间模式

  3. MODE_NIGHT_FOLLOW_SYSTEM:根据系统设置决定是否设置夜间模式

  4. MODE_NIGHT_AUTO:根据当前时间来自动切换夜间/日间模式

在项目中新建vaules-night,并生成相应的夜间模式文件,例如:

colors.xml

strings.xml

Application中设置全局的夜间/日间模式

public class App extends Application {@Overridepublic 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替代

换肤Demo地址

Android 换肤(全局换肤,部分换肤,字体替换,导航栏替换,自定义view换肤,夜间/日间模式)相关推荐

  1. Android中导航栏之自定义导航布局

    Toolbar系列文章导航 Android中导航栏之Toolbar的使用 Android中导航栏之溢出菜单OverflowMenu Android中导航栏之搜索框SearchView Android中 ...

  2. android获取导航栏宽高,Android获取屏幕的宽高度、状态栏、标题栏、导航栏、编辑区域高度...

    目录 0.相关文章: 1.获取屏幕款高度 代码: /** * 获取屏幕宽度 * * @param context 上下文对象 * @return int */ public static int ge ...

  3. 史上最完美的Android沉浸式状态导航栏攻略

    前言 最近我在小破站开发一款新App,叫高能链.我是一个完美主义者,所以不管对架构还是UI,我都是比较抠细节的,在状态栏和导航栏沉浸式这一块,我还是踩了挺多坑,费了挺多精力的.这次我将我踩坑,适配各机 ...

  4. Android开发——底部导航栏设计

    底部导航栏设计 1.依赖配置 2.tabbar的UI实现 3.tabbar的逻辑绑定 4.tabbar的滑动与点击联动 5.tabbar与文本输入的冲突解决方案     其实,常见的Android和微 ...

  5. Android自定义View全解

    目录 目录.png 1. 自定义View基础 1.1 分类 自定义View的实现方式有以下几种 类型 定义 自定义组合控件 多个控件组合成为一个新的控件,方便多处复用 继承系统View控件 继承自Te ...

  6. 基于 Kotlin 一行代码实现 android 导航栏 BottomBar

    主要功能点 构建者模式链式设置导航栏条目 自定义导航栏的字体大小图片大小 支持纯文字类型 支持底部按钮点击事件 代码简洁不到300行,只有一个类 直接拿来用 看效果是否满意 上代码 直接先贴代码Bot ...

  7. Android中导航栏之溢出菜单OverflowMenu

    Toolbar系列文章导航 Android中导航栏之Toolbar的使用 Android中导航栏之溢出菜单OverflowMenu Android中导航栏之搜索框SearchView Android中 ...

  8. 21天学习之二(Android 10.0 SystemUI默认去掉底部导航栏的三种方法)

    活动地址:CSDN21天学习挑战赛 1.概述 在定制化开发中,在SystemUI的一些定制功能中,针对默认去掉底部导航栏的方法有好几种,StatusBar和DisplayPolicy.java中api ...

  9. android界面UI美化:沉浸模式、全透明或半透明状态栏及导航栏的实现

    android api19开始我们就能对顶部状态栏和底部导航栏进行半透明处理了,而api21开始则可以实现全透明状态栏与导航栏以及开启沉浸模式,至于什么是沉浸模式,大家百度一下应该就都知道了,有一点需 ...

最新文章

  1. IEnumerable和IQueryable在使用时的区别
  2. 分享一个帮助用户全屏阅读的jQuery插件 - jQuery fullscreen
  3. 【图像处理】——Python鼠标框选ROI(感兴趣)区域并且保存(含鼠标事件)
  4. LeetCode 94. 二叉树的中序遍历(中序遍历)
  5. 云计算学习路线图课件:云计算和虚拟机有什么区别?
  6. 2019.7.24循环结构以及昨天的预习题。
  7. python函数的特性_Python学习(007)-函数的特性
  8. .NET中Redis安装部署及使用方法
  9. 程序出错后 程序员给测试人员的20条高频回复
  10. rowStyle设置Bootstrap Table行样式
  11. AP聚类算法(Affinity propagation Clustering Algorithm )
  12. 美女视频都想下载,今天我们就来批量下载它们~
  13. KMP复习之poj 3461 Oulipo
  14. Java 处理英文文本标点符号去除
  15. android studio字体加粗属性,android textview字体加粗 Android studio最新水平居中和垂直居中...
  16. opcode加密php代码,总结Opcode缓存和PHP代码的加密
  17. [图文教程]BIOS设置教程
  18. quartz-深度解析
  19. 那些辉煌的背后, 不知装载了多少苦涩
  20. 【转】CVPR2019目标检测汇总

热门文章

  1. 十年经验讲解功能测试的一些基本操作以及报告编写
  2. pdf文件里面水印如何全部去掉
  3. 1024+996=2020,今天注定996,一大波暴击电子美图送给大家!
  4. 网吧电脑怎么录屏?分享3种便捷方法,一键录屏
  5. 2020牛客暑期多校训练营(第五场) F、DPS(签到题)
  6. Winter Is Coming(CF #387 Div. 2)
  7. 各种python数据类型保存成文件
  8. 掌门一对一java待遇,算法太TM重要了
  9. Python之操作Excel、异常处理、网络编程
  10. php 字体倾斜,CSS中font-style定义字体倾斜体样式的代码示例