Android 换肤(全局换肤,部分换肤,字体替换,导航栏替换,自定义view换肤,夜间/日间模式)
采集
大致流程
监听所有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 {@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;}}
}
制作
- 一个没有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;}//反射创建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的四种模式
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 {@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换肤,夜间/日间模式)相关推荐
- Android中导航栏之自定义导航布局
Toolbar系列文章导航 Android中导航栏之Toolbar的使用 Android中导航栏之溢出菜单OverflowMenu Android中导航栏之搜索框SearchView Android中 ...
- android获取导航栏宽高,Android获取屏幕的宽高度、状态栏、标题栏、导航栏、编辑区域高度...
目录 0.相关文章: 1.获取屏幕款高度 代码: /** * 获取屏幕宽度 * * @param context 上下文对象 * @return int */ public static int ge ...
- 史上最完美的Android沉浸式状态导航栏攻略
前言 最近我在小破站开发一款新App,叫高能链.我是一个完美主义者,所以不管对架构还是UI,我都是比较抠细节的,在状态栏和导航栏沉浸式这一块,我还是踩了挺多坑,费了挺多精力的.这次我将我踩坑,适配各机 ...
- Android开发——底部导航栏设计
底部导航栏设计 1.依赖配置 2.tabbar的UI实现 3.tabbar的逻辑绑定 4.tabbar的滑动与点击联动 5.tabbar与文本输入的冲突解决方案 其实,常见的Android和微 ...
- Android自定义View全解
目录 目录.png 1. 自定义View基础 1.1 分类 自定义View的实现方式有以下几种 类型 定义 自定义组合控件 多个控件组合成为一个新的控件,方便多处复用 继承系统View控件 继承自Te ...
- 基于 Kotlin 一行代码实现 android 导航栏 BottomBar
主要功能点 构建者模式链式设置导航栏条目 自定义导航栏的字体大小图片大小 支持纯文字类型 支持底部按钮点击事件 代码简洁不到300行,只有一个类 直接拿来用 看效果是否满意 上代码 直接先贴代码Bot ...
- Android中导航栏之溢出菜单OverflowMenu
Toolbar系列文章导航 Android中导航栏之Toolbar的使用 Android中导航栏之溢出菜单OverflowMenu Android中导航栏之搜索框SearchView Android中 ...
- 21天学习之二(Android 10.0 SystemUI默认去掉底部导航栏的三种方法)
活动地址:CSDN21天学习挑战赛 1.概述 在定制化开发中,在SystemUI的一些定制功能中,针对默认去掉底部导航栏的方法有好几种,StatusBar和DisplayPolicy.java中api ...
- android界面UI美化:沉浸模式、全透明或半透明状态栏及导航栏的实现
android api19开始我们就能对顶部状态栏和底部导航栏进行半透明处理了,而api21开始则可以实现全透明状态栏与导航栏以及开启沉浸模式,至于什么是沉浸模式,大家百度一下应该就都知道了,有一点需 ...
最新文章
- IEnumerable和IQueryable在使用时的区别
- 分享一个帮助用户全屏阅读的jQuery插件 - jQuery fullscreen
- 【图像处理】——Python鼠标框选ROI(感兴趣)区域并且保存(含鼠标事件)
- LeetCode 94. 二叉树的中序遍历(中序遍历)
- 云计算学习路线图课件:云计算和虚拟机有什么区别?
- 2019.7.24循环结构以及昨天的预习题。
- python函数的特性_Python学习(007)-函数的特性
- .NET中Redis安装部署及使用方法
- 程序出错后 程序员给测试人员的20条高频回复
- rowStyle设置Bootstrap Table行样式
- AP聚类算法(Affinity propagation Clustering Algorithm )
- 美女视频都想下载,今天我们就来批量下载它们~
- KMP复习之poj 3461 Oulipo
- Java 处理英文文本标点符号去除
- android studio字体加粗属性,android textview字体加粗 Android studio最新水平居中和垂直居中...
- opcode加密php代码,总结Opcode缓存和PHP代码的加密
- [图文教程]BIOS设置教程
- quartz-深度解析
- 那些辉煌的背后, 不知装载了多少苦涩
- 【转】CVPR2019目标检测汇总
热门文章
- 十年经验讲解功能测试的一些基本操作以及报告编写
- pdf文件里面水印如何全部去掉
- 1024+996=2020,今天注定996,一大波暴击电子美图送给大家!
- 网吧电脑怎么录屏?分享3种便捷方法,一键录屏
- 2020牛客暑期多校训练营(第五场) F、DPS(签到题)
- Winter Is Coming(CF #387 Div. 2)
- 各种python数据类型保存成文件
- 掌门一对一java待遇,算法太TM重要了
- Python之操作Excel、异常处理、网络编程
- php 字体倾斜,CSS中font-style定义字体倾斜体样式的代码示例