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

采集

大致流程

  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 {
    @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;
        }
    }
}

制作

  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;
                }
                //反射创建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的四种模式

  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 {
    @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替代

换肤Demo地址

发布了113 篇原创文章 · 获赞 48 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/yehui928186846/article/details/101266170