文章目录

  • 1 采集需要换肤的控件
  • 1.1 SkinManager换肤管理类
  • 1.2 SkinActivityLifecycle
  • 1.3 SkinLayoutFactory
  • 1.4 SkinAttribute
  • 2 加载皮肤包并换肤
  • 2.1 下载apk并加载
  • 2.2 SkinActivity
  • 2.3 SkinManager
  • 2.4 SkinLayoutFactory
  • 2.5 SkinActivityLifecycle
  • 2.6 SkinAttribute
  • 3 总结


代码下载:


Github:
https://github.com/345166018/WangyiSkin/tree/master/HxSkin

setContentView源码分析可以查看:


参考:
https://github.com/fengjundev/Android-Skin-Loaderhttps://github.com/ximsfei/Android-skin-supporthttps://www.jianshu.com/p/af7c0585dd5b

这里只实现了背景的替换,后续会添加 字体,状态栏换肤,自定义控件,fragment换肤。


换肤模式:

模式

案例

内置换肤

在Apk包中存在多种资源(图片、颜色值)用于换肤时候切换。自由度低,apk文件大。

一般用于没有其他需求的日间/夜间模式app(高德地图)

动态换肤

通过运行时动态加载皮肤包。

网易云音乐

android 更换背景图 安卓换背景_Android


如上图所示,换肤主要包括三个步骤:

  1. 采集需要换肤的控件
  2. 加载皮肤包
  3. 控件换肤

先实现一个背景图片的替换,其他功能之后再慢慢完善。

看效果:

android 更换背景图 安卓换背景_动态换肤_02

1 采集需要换肤的控件

新建MyApplication,在onCreate方法中实现SkinManager初始化操作,代码如下:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        SkinManager.init(this);
    }
}

1.1 SkinManager换肤管理类

public class SkinManager{

    private static SkinManager instance;
    private Application application;
    
    public static void init(Application application){
        synchronized (SkinManager.class) {
            if(null == instance){
                instance = new SkinManager(application);
            }
        }
    }
    public static SkinManager getInstance() {
        return instance;
    }

    private SkinManager(Application application) {
        this.application = application;
        /**
         * 提供了一个应用生命周期回调的注册方法,用来对应用的生命周期进行集中管理,
         * 这个接口叫registerActivityLifecycleCallbacks,可以通过它注册
         * 自己的ActivityLifeCycleCallback,每一个Activity的生命周期都会回调到这里的对应方法。
         */
         application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());//3
    }

}



1.2 SkinActivityLifecycle

使用ActivityLifecycleCallbacks对应用的生命周期进行集中管理。

public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
    HashMap<Activity , SkinLayoutFactory> factoryHashMap = new HashMap<>();

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

        LayoutInflater layoutInflater = LayoutInflater.from(activity);
        try {
            Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
            mFactorySet.setAccessible(true);
            mFactorySet.setBoolean(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //添加自定义创建View 工厂
        SkinLayoutFactory factory = new SkinLayoutFactory(activity);
        layoutInflater.setFactory2(factory);
    }

    @Override
    public void onActivityStarted(Activity activity) {
    }

    @Override
    public void onActivityResumed(Activity activity) {
    }

    @Override
    public void onActivityPaused(Activity activity) {
    }

    @Override
    public void onActivityStopped(Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    }
}

1.3 SkinLayoutFactory

SkinLayoutFactory 实现了 LayoutInflater.Factory2接口。在SkinLayoutFactory中去采集需要换肤的控件和控件中的属性。

public class SkinLayoutFactory implements LayoutInflater.Factory2{

	//控件包名的前缀
    private static final String[] mClassPrefixlist = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };

	//
    private static final Class[] mConstructorSignature =
            new Class[]{Context.class, AttributeSet.class};

    private static final HashMap<String, Constructor<? extends View>> mConstructor =
            new HashMap<String, Constructor<? extends View>>();

    //属性处理类
    private SkinAttribute skinAttribute;
    private Activity activity;

    public SkinLayoutFactory(Activity activity) {
        this.activity = activity;
        skinAttribute = new SkinAttribute();
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // 反射 classLoader
        View view = createViewFromTag(name, context, attrs);//1
        // 自定义View
        if(null ==  view){
            view = createView(name, context, attrs);//2
        }
        //筛选符合属性View
        skinAttribute.load(view, attrs);//3
        return view;
    }

    private View createViewFromTag(String name, Context context, AttributeSet attrs) {
        //包含自定义控件
        if (-1 != name.indexOf(".")) {
            return null;
        }
        //
        View view = null;
        for (int i = 0; i < mClassPrefixlist.length; i++) {
            view = createView(mClassPrefixlist[i] + name, context, attrs);
            if(null != view){
                break;
            }
        }
        return view;
    }

    private View createView(String name, Context context, AttributeSet attrs) {
        Constructor<? extends View> constructor = mConstructor.get(name);
        if (constructor == null) {
            try {
                //通过全类名获取class
                Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
                //获取构造方法
                constructor = aClass.getConstructor(mConstructorSignature);
                mConstructor.put(name, constructor);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (null != constructor) {
            try {
                return constructor.newInstance(context, attrs);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

}

注释1:通过反射获取到系统提供的View
注释2:获取到自定义的View
注释3:调用SkinAttribute的load方法,将获取到的所有View以及View中的属性传递给SkinAttribute类中进行筛选和保存

1.4 SkinAttribute

load方法用于筛选和保存需要换肤的控件和控件中的属性。

public class SkinAttribute {
    private static final List<String> mAttributes = new ArrayList<>();

    static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

    private List<SkinView> skinViews = new ArrayList<>();

    public void load(View view, AttributeSet attrs) {
        List<SkinPain> skinPains = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            //获取属性名字
            String attributeName = attrs.getAttributeName(i);
            if (mAttributes.contains(attributeName)) {
                //获取属性对应的值
                String attributeValue = attrs.getAttributeValue(i);
                if (attributeValue.startsWith("#")) {
                    continue;
                }

                int resId;
                //判断前缀字符串 是否是"?"
                //attributeValue  = "?2130903043"
                if (attributeValue.startsWith("?")) {  //系统属性值
                    //字符串的子字符串  从下标 1 位置开始
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {
                    //@1234564
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                if (resId != 0) {
                    SkinPain skinPain = new SkinPain(attributeName, resId);
                    skinPains.add(skinPain);
                }
            }
        }
        if (!skinPains.isEmpty()) {
            SkinView skinView = new SkinView(view, skinPains);
            skinView.applySkin();
            skinViews.add(skinView);
        }
    }

    static class SkinView {
        View view;
        List<SkinPain> skinPains;
        public SkinView(View view, List<SkinPain> skinPains) {
            this.view = view;
            this.skinPains = skinPains;
        }
      }

    static class SkinPain {
        String attributeName;
        int resId;
        public SkinPain(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }

}

以上就是完整的采集需要换肤控件的过程。所有需要换肤的View和对应的属性都保存在了SkinAttribute的skinViews当中。


2 加载皮肤包并换肤

新建一个Phone Module,在drawable中添加一张换肤背景图,make project后生成apk,将生成的apk复制到 app中的assets中。

如下图所示:

android 更换背景图 安卓换背景_android 更换背景图_03

app和和皮肤包app_skin都有一张叫t_window_bg的背景图。
查看app-intermediates-javac下面的R.class 和skin-intermediates-javac下面的R.class ,会发现里面的t_window_bg的值相同,如下:

public static final int t_window_bg = 2131099747;

android 更换背景图 安卓换背景_控件_04


获取到app中t_window_bg的值,就可以通过这个值去app_skin中查找了。

2.1 下载apk并加载

在MainActivity中模拟apk的下载(这里是复制assets中的apk到本地),并保存apk下载后所在的路径。

public class MainActivity extends AppCompatActivity {

    String apkName = "app_skin-debug.apk";

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        try {
            Utils.extractAssets(newBase, apkName);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File extractFile = this.getFileStreamPath(apkName);
        String apkPath = extractFile.getAbsolutePath();
        //保存apk路径
        MyApplication.getApplication().setApkPath(apkPath);

    }

    /**
     * 进入换肤
     */
    public void skinSelect(View view) {
        startActivity(new Intent(this, SkinActivity.class));
    }
    
}

具体下载的方法如下代码所示:

public class Utils {
    /**
     * 把Assets里面得文件复制到 /data/data/files 目录下
     */
    public static void extractAssets(Context context, String sourceName) {
        AssetManager am = context.getAssets();
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = am.open(sourceName);
            File extractFile = context.getFileStreamPath(sourceName);
            fos = new FileOutputStream(extractFile);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer)) > 0) {
                fos.write(buffer, 0, count);
            }
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            closeSilently(is);
            closeSilently(fos);
        }
    }
    private static void closeSilently(Closeable closeable) {
        if (closeable == null) {
            return;
        }
        try {
            closeable.close();
        } catch (Throwable e) {
            // ignore
        }
    }
}

2.2 SkinActivity

SkinActivity为换肤页面,点击换肤按钮进行换肤,点击还原恢复原始状态。

public class SkinActivity extends Activity {
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_skin);
    }

    public void change(View view) {
        String path = MyApplication.getApplication().getApkPath();
        SkinManager.getInstance().loadSkin(path);
    }

    public void restore(View view) {
        SkinManager.getInstance().loadSkin(null);
    }
}

android 更换背景图 安卓换背景_动态换肤_05

2.3 SkinManager

又回到SkinManager类中。换肤操作的方法为loadSkin方法。在实现换肤之前,需要获取到换肤包中的资源。要获取换肤包的资源,除了需要包的路径,还需要包对应Resources。

public class SkinManager extends Observable {

    private static SkinManager instance;
    private Application application;

    public static void init(Application application){
        synchronized (SkinManager.class) {
            if(null == instance){
                instance = new SkinManager(application);
            }
        }
    }
    public static SkinManager getInstance() {
        return instance;
    }

    private SkinManager(Application application) {
        this.application = application;
        //共享首选项 用于记录当前使用的皮肤
        SkinPreference.init(application);
        //资源管理类 用于从app/皮肤 中加载资源
        SkinResources.init(application);
        application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
        loadSkin(SkinPreference.getInstance().getSkin());
    }

    public void loadSkin(String path) {
        if(TextUtils.isEmpty(path)){
            // 记录使用默认皮肤
            SkinPreference.getInstance().setSkin("");
            //清空资源管理器, 皮肤资源属性等
            SkinResources.getInstance().reset();
        } else {
            try {
                //反射创建AssetManager
                AssetManager manager = AssetManager.class.newInstance();
                // 资料路径设置 目录或者压缩包
                Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
                addAssetPath.invoke(manager, path);

                Resources appResources = this.application.getResources();//1
                Resources skinResources = new Resources(manager, 
                        appResources.getDisplayMetrics(), appResources.getConfiguration());//2

                //记录
                SkinPreference.getInstance().setSkin(path);
                //获取外部Apk(皮肤薄) 包名
                PackageManager packageManager = this.application.getPackageManager();
                PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
                String packageName = packageArchiveInfo.packageName;//3

                SkinResources.getInstance().applySkin(skinResources,packageName);//4
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //采集的view   皮肤包
        setChanged();
        //通知观者者
        notifyObservers();
    }
}

注释1:获取app的Resources
注释2:获取皮肤包的Resources
注释3:获取皮肤包的应用包名
注释4:将皮肤包的Resources和应用包名添加到SkinResources中

SkinResources中的applySkin方法

public void applySkin(Resources resources, String pkgName) {
        mSkinResources = resources;
        mSkinPkgName = pkgName;
        //是否使用默认皮肤
        isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
    }

SkinManager是一个被观察者,在保存了皮肤包的Resources和pkgName后,通知观察者(SkinLayoutFactory)去更换皮肤

2.4 SkinLayoutFactory

public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {

   ...
	
   @Override
    public void update(Observable o, Object arg) {
        //更换皮肤
        skinAttribute.applySkin();
    }

}

2.5 SkinActivityLifecycle

需要在SkinActivityLifecycle中注册和删除观察者

public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
    HashMap<Activity , SkinLayoutFactory> factoryHashMap = new HashMap<>();

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        /**
         *  更新状态栏
         */
        SkinThemeUtils.updataStatusBarColor(activity);

        LayoutInflater layoutInflater = LayoutInflater.from(activity);
        try {
            Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
            mFactorySet.setAccessible(true);
            mFactorySet.setBoolean(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //添加自定义创建View 工厂
        SkinLayoutFactory factory = new SkinLayoutFactory(activity);
        layoutInflater.setFactory2(factory);

        //注册观察者
        SkinManager.getInstance().addObserver(factory);
        factoryHashMap.put(activity, factory);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
        //删除观察者
        SkinLayoutFactory remove = factoryHashMap.remove(activity);
        SkinManager.getInstance().deleteObserver(remove);
    }
}

2.6 SkinAttribute

public class SkinAttribute {
    private static final List<String> mAttributes = new ArrayList<>();

    static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

    private List<SkinView> skinViews = new ArrayList<>();

    public void load(View view, AttributeSet attrs) {
        List<SkinPain> skinPains = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            //获取属性名字
            String attributeName = attrs.getAttributeName(i);
            if (mAttributes.contains(attributeName)) {
                //获取属性对应的值
                String attributeValue = attrs.getAttributeValue(i);
                if (attributeValue.startsWith("#")) {
                    continue;
                }

                int resId;
                //判断前缀字符串 是否是"?"
                //attributeValue  = "?2130903043"
                if (attributeValue.startsWith("?")) {  //系统属性值
                    //字符串的子字符串  从下标 1 位置开始
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {
                    //@1234564
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                if (resId != 0) {
                    SkinPain skinPain = new SkinPain(attributeName, resId);
                    skinPains.add(skinPain);
                }
            }
        }
        if (!skinPains.isEmpty()) {
            SkinView skinView = new SkinView(view, skinPains);
            skinView.applySkin();
            skinViews.add(skinView);
        }
    }

    static class SkinView {
        View view;
        List<SkinPain> skinPains;

        public SkinView(View view, List<SkinPain> skinPains) {
            this.view = view;
            this.skinPains = skinPains;
        }

		//1
        public void applySkin() {
            for (SkinPain skinPair : skinPains) {//2
                Drawable left = null, top = null, right = null, bottom = null;
                switch (skinPair.attributeName) {
                    case "background":
                        Object background = SkinResources.getInstance().getBackground(
                                skinPair.resId);
                        //Color
                        if (background instanceof Integer) {
                            view.setBackgroundColor((Integer) background);//3
                        } else {
                            ViewCompat.setBackground(view, (Drawable) background);//4
                        }
                        break;
                    case "src":
                        background = SkinResources.getInstance().getBackground(skinPair
                                .resId);
                        if (background instanceof Integer) {
                            ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
                                    background));
                        } else {
                            ((ImageView) view).setImageDrawable((Drawable) background);
                        }
                        break;
                    case "textColor":
                        ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                                (skinPair.resId));
                        break;
                    case "drawableLeft":
                        left = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableTop":
                        top = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableRight":
                        right = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableBottom":
                        bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    default:
                        break;
                }
                if (null != left || null != right || null != top || null != bottom) {
                    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                            bottom);
                }
            }
        }
    }

    static class SkinPain {
        String attributeName;
        int resId;

        public SkinPain(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }

    /**
     * 换皮肤
     */
    public void applySkin() {
        for (SkinView mSkinView : skinViews) {
            mSkinView.applySkin();
        }
    }
}

注释1:内部类SkinView的applySkin为真正执行换肤操作的方法。
注释2:skinPains保存了所有需要换肤的View及对应的属性。
注释3:背景是颜色
注释4:背景是图片



3 总结

  1. 在SkinManager中注册SkinActivityLifecycle。
  2. SkinActivityLifecycle中创建SkinLayoutFactory,并将工厂添加到LayoutInflater中。需要注意的是LayoutInflater的setFactory2方法只能使用一次,所以需要先对mFactorySet值进行修改。
  3. 在SkinLayoutFactory中通过反射获取到所有需要换肤的控件及属性。
  4. 获取控件的属性具体是在SkinAttribute的load()方法中实现的,并将所有控件和属性保存在skinViews当中。
  5. 下载皮肤apk到本地。
  6. SkinActivity中点击按钮调用SkinManager的loadSkin方法,方法中需传入apk路径。
  7. 反射创建AssetManager,通过AssetManager和app的Resources(appResources)获取到皮肤包的Resources(skinResources)。
  8. 通过PackageManager获取到皮肤包的packageName。
  9. 再将skinResources和packageName通过SkinResources的applySkin方法添加到SkinResources中。
  10. 此时就可以通知SkinLayoutFactory去更换皮肤,具体换肤在SkinAttribute的applySkin()中完成。所有需要换肤的控件和属性都保存在SkinAttribute的skinViews中。循环skinViews,拿到resId(app的资源的值),使用resId去皮肤包中查找对应的资源(查找皮肤资源在SkinResources实现),然后将皮肤包中的资源替换掉app的资源并给对应的属性赋值。