美文网首页
换肤方式实现日/夜间模式

换肤方式实现日/夜间模式

作者: 瑜小贤 | 来源:发表于2020-03-30 15:17 被阅读0次

    一、夜间模式的实现方式
    夜间模式的实现方式有两种,一种是本地替换,一种是用动态换肤的方案替换。
    但是两中方案的思想大致是一样的,就是收集页面View,在页面初始化时替换为自定义的相关view,在自定义view实现的接口方法里进行日夜间的判断及不同资源的加载。

    二、具体的实现

    /**
     * 换肤Activity父类
     *
     * 用法:
     * 1、继承此类
     * 2、重写openChangeSkin()方法
     */
    public class SkinActivity extends AppCompatActivity {
    
        private CustomAppCompatViewInflater viewInflater;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
            super.onCreate(savedInstanceState);
        }
    
        // TODO 第三大步骤,收集布局里面所有的View
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            if (openChangeSkin()) {
                if (viewInflater == null) {
                    viewInflater = new CustomAppCompatViewInflater(context);
                }
                viewInflater.setName(name);
                viewInflater.setAttrs(attrs);
                return viewInflater.autoMatch(); // TODO 第三大步:第二小步
            }
            return super.onCreateView(parent, name, context, attrs);
        }
    
        /**
         * @return 是否开启换肤,增加此开关是为了避免开发者误继承此父类,导致未知bug
         */
        protected boolean openChangeSkin() {
            return false;
        }
    
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        protected void defaultSkin(int themeColorId) {
            this.skinDynamic(null, themeColorId);
        }
    
        /**
         * 动态换肤(api限制:5.0版本)
         */
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        protected void skinDynamic(String skinPath, int themeColorId) {
    
            // TODO 第一大步:收集皮肤包里面的所有信息
            SkinManager.getInstance().loaderSkinResources(skinPath);
    
            // TODO 第二大步:调用了系统的 进行改变
            if (themeColorId != 0) {
                // ActionBus 状态栏 + 底部栏
                int themeColor = SkinManager.getInstance().getColor(themeColorId);
                StatusBarUtils.forStatusBar(this, themeColor);
                NavigationUtils.forNavigation(this, themeColor);
                ActionBarUtils.forActionBar(this, themeColor);
            }
    
            // TODO 第四大步骤:真正的换肤
            applyViews(getWindow().getDecorView());
        }
    
        /**
         * 控件回调监听,匹配上则给控件执行换肤方法
         */
        protected void applyViews(View view) {
            if (view instanceof ViewsMatch) {
                ViewsMatch viewsMatch = (ViewsMatch) view;
                viewsMatch.skinnableView();
            }
    
            if (view instanceof ViewGroup) {
                ViewGroup parent = (ViewGroup) view;
                int childCount = parent.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    applyViews(parent.getChildAt(i));
                }
            }
        }
    }
    

    这就是所需换肤的Activity父类,所有需要换肤的Activity需要继承自此Activity。

    1. 首先注意到我重写了onCreate方法,并在其中设置了setFactory2
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
            super.onCreate(savedInstanceState);
    }
    

    这是因为我们想要设置自定义的LayoutInflater,而AppCompatActivity内部也调用了setFactory,所以如果我们再调用setFactory就会打印出信息如下(不是报错,只是自定义的Inflater无效):

    The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's
    

    源码追踪:

    AppCompatActivity-> AppCompatDelegate ->AppCompatDelegateImpl

    AppCompatActivity.onCreate()

    @Override
       protected void onCreate(@Nullable Bundle savedInstanceState) {
           getDelegate().installViewFactory();
           getDelegate().onCreate(savedInstanceState);
           super.onCreate(savedInstanceState);
       }
    

    可以看出AppCompatDelegateImpl实现了LayoutInflater.Factory2接口;

    class AppCompatDelegateImpl extends AppCompatDelegate
            implements MenuBuilder.Callback, LayoutInflater.Factory2 {
    

    AppCompatDelegateImpl.installViewFactory()

      @Override
        public void installViewFactory() {
            LayoutInflater layoutInflater = LayoutInflater.from(mContext);
            //注意这里,如果我们在super.onCreate之前设置了自定义的Factory,
            //这里就会走 != null,系统的setFactory2不会被调用;
            //从而AppCompatDelegateImpl.onCreateView方法也不会被调用;
            if (layoutInflater.getFactory() == null) {
                LayoutInflaterCompat.setFactory2(layoutInflater, this);
            } else {
                if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                    Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                            + " so we can not install AppCompat's");
                }
            }
        }
    

    AppCompatDelegateImpl Factory2

     public interface Factory2 extends Factory {
            @Nullable
            View onCreateView(@Nullable View parent, @NonNull String name,
                    @NonNull Context context, @NonNull AttributeSet attrs);
        }
    

    可以看出,Factory2是一个接口,onCreateViewFactory2的接口方法,如果我们在系统调用之前,也就是super.onCreate()之前设置Factory,就会 就不会走系统的Factory的接口方法,而是走我们自己的onCreateView

    然后我们看到自己重写的 onCreateView方法,方法中的参数name就是页面控件的名称(自定义的就是包名+类名,系统的就直接是类名),而attrs就是控件属性了,我们使用了自定义的CustomAppCompatViewInflater对其进行了收集,并执行了自定义AppCompatViewInflater中的autoMatch方法。

    下面看CustomAppCompatViewInflater

    /**
     * TODO 第三大步骤 一个小步
     * 自定义控件加载器(可以考虑该类不被继承)
     */
    public final class CustomAppCompatViewInflater
            extends AppCompatViewInflater
    {
    
        private String name; // 控件名
        private Context context; // 上下文
        private AttributeSet attrs; // 某控件对应所有属性
    
        public CustomAppCompatViewInflater(@NonNull Context context) {
            this.context = context;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setAttrs(AttributeSet attrs) {
            this.attrs = attrs;
        }
    
        /**
         * @return 自动匹配控件名,并初始化控件对象
         */
        public View autoMatch() {
            View view = null;
            switch (name) {
                case "LinearLayout":
                    // view = super.createTextView(context, attrs); // 源码写法
                    view = new SkinnableLinearLayout(context, attrs);
                    this.verifyNotNull(view, name);
                    break;
                case "RelativeLayout":
                    view = new SkinnableRelativeLayout(context, attrs);
                    this.verifyNotNull(view, name);
                    break;
                case "TextView":
                    view = new SkinnableTextView(context, attrs);
                    this.verifyNotNull(view, name);
                    break;
                case "ImageView":
                    view = new SkinnableImageView(context, attrs);
                    this.verifyNotNull(view, name);
                    break;
                case "Button":
                    // button = new Button(); 虽然能实现,不能这样干,无法商用
                    view = new SkinnableButton(context, attrs);
                    this.verifyNotNull(view, name);
                    break;
            }
    
            return view;
        }
    
        /**
         * 校验控件不为空(源码方法,由于private修饰,只能复制过来了。为了代码健壮,可有可无)
         *
         * @param view 被校验控件,如:AppCompatTextView extends TextView(v7兼容包,兼容是重点!!!)
         * @param name 控件名,如:"ImageView"
         */
        private void verifyNotNull(View view, String name) {
            if (view == null) {
                throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
            }
        }
    }
    

    在这个Inflater中替换资源,使用我们自定义的Skinnable的View,即实现了换肤接口的View,这其中分为两种:

    1. 原生控件:如果需要实现换肤功能,建议统一在换肤module里定义,因为会利用自定义Inflater在autoMatch方法中替换,所以布局中还是正常写成原生控件。
    2. 自定义控件:定义在主工程中,可以直接实现换肤接口即可,在换肤接口方法中利用SkinManager来判断和获取日夜间模式。
      下面举例原生控件、自定义控件的代码示例
    /**
     * 继承TextView兼容包,9.0源码中也是如此
     * 参考:AppCompatViewInflater.java
     * 86行 + 138行 + 206行
     */
    public class SkinnableTextView extends AppCompatTextView implements ViewsMatch {
    
        private AttrsBean attrsBean;
    
        public SkinnableTextView(Context context) {
            this(context, null);
        }
    
        public SkinnableTextView(Context context, AttributeSet attrs) {
            this(context, attrs, android.R.attr.textViewStyle);
        }
    
        public SkinnableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            attrsBean = new AttrsBean();
    
            // 根据自定义属性,匹配控件属性的类型集合,如:background + textColor
            TypedArray typedArray = context.obtainStyledAttributes(attrs,
                    R.styleable.SkinnableTextView,
                    defStyleAttr, 0);
            // 存储到临时JavaBean对象
            attrsBean.saveViewResource(typedArray, R.styleable.SkinnableTextView);
            // 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
            typedArray.recycle();
        }
    
        @Override
        public void skinnableView() {
            // 根据自定义属性,获取styleable中的background属性
            int key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_android_background];
            // 根据styleable获取控件某属性的resourceId
            int backgroundResourceId = attrsBean.getViewResource(key);
            if (backgroundResourceId > 0) {
                // 是否默认皮肤
                if (SkinManager.getInstance().isDefaultSkin()) {
                    // 兼容包转换
                    Drawable drawable = ContextCompat.getDrawable(getContext(), backgroundResourceId);
                    // 控件自带api,这里不用setBackgroundColor()因为在9.0测试不通过
                    // setBackgroundDrawable本来过时了,但是兼容包重写了方法
                    setBackgroundDrawable(drawable);
                } else {
                    // 获取皮肤包资源
                    Object skinResourceId = SkinManager.getInstance().getBackgroundOrSrc(backgroundResourceId);
                    // 兼容包转换
                    if (skinResourceId instanceof Integer) {
                        int color = (int) skinResourceId;
                        setBackgroundColor(color);
                        // setBackgroundResource(color); // 未做兼容测试
                    } else {
                        Drawable drawable = (Drawable) skinResourceId;
                        setBackgroundDrawable(drawable);
                    }
                }
            }
    
            // 根据自定义属性,获取styleable中的textColor属性
            key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_android_textColor];
            int textColorResourceId = attrsBean.getViewResource(key);
            if (textColorResourceId > 0) {
                if (SkinManager.getInstance().isDefaultSkin()) {
                    ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
                    setTextColor(color);
                } else {
                    ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
                    setTextColor(color);
                }
            }
    
            // 根据自定义属性,获取styleable中的字体 custom_typeface 属性
            key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_custom_typeface];
            int textTypefaceResourceId = attrsBean.getViewResource(key);
            if (textTypefaceResourceId > 0) {
                if (SkinManager.getInstance().isDefaultSkin()) {
                    setTypeface(Typeface.DEFAULT);
                } else {
                    setTypeface(SkinManager.getInstance().getTypeface(textTypefaceResourceId));
                }
            }
        }
    }
    
    
    // 自定义控件
    public class CustomCircleView extends View implements ViewsMatch {
    
        private Paint mTextPain;
        private AttrsBean attrsBean;
    
        public CustomCircleView(Context context) {
            this(context, null);
        }
    
        public CustomCircleView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public CustomCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            attrsBean = new AttrsBean();
    
            // 根据自定义属性,匹配控件属性的类型集合,如:circleColor
            TypedArray typedArray = context.obtainStyledAttributes(attrs,
                    R.styleable.CustomCircleView,
                    defStyleAttr, 0);
    
            int corcleColorResId = typedArray.getResourceId(R.styleable.CustomCircleView_circleColor, 0);
    
            // 存储到临时JavaBean对象
            attrsBean.saveViewResource(typedArray, R.styleable.CustomCircleView);
            // 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
            typedArray.recycle();
    
            mTextPain = new Paint();
            mTextPain.setColor(getResources().getColor(corcleColorResId));
            //开启抗锯齿,平滑文字和圆弧的边缘
            mTextPain.setAntiAlias(true);
            //设置文本位于相对于原点的中间
            mTextPain.setTextAlign(Paint.Align.CENTER);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            // 获取宽度一半
            int width = getWidth() / 2;
            // 获取高度一半
            int height = getHeight() / 2;
            // 设置半径为宽或者高的最小值(半径)
            int radius = Math.min(width, height);
            // 利用canvas画一个圆
            canvas.drawCircle(width, height, radius, mTextPain);
        }
    
        @Override
        public void skinnableView() {
            // 根据自定义属性,获取styleable中的circleColor属性
            int key = R.styleable.CustomCircleView[R.styleable.CustomCircleView_circleColor]; 
            int resourceId = attrsBean.getViewResource(key);
            if (resourceId > 0) {
                if (SkinManager.getInstance().isDefaultSkin()) {
                    int color = ContextCompat.getColor(getContext(), resourceId);
                    mTextPain.setColor(color);
                } else {
                    int color = SkinManager.getInstance().getColor(resourceId);
                    mTextPain.setColor(color);
                }
            }
    
            // 根据自定义属性,获取styleable中的textColor属性
            key = R.styleable.CustomCircleView[R.styleable.CustomCircleView_android_background];
            int backgroundResourceId = attrsBean.getViewResource(key);
            if (backgroundResourceId > 0) {
                if (SkinManager.getInstance().isDefaultSkin()) {
                    Drawable drawable = ContextCompat.getDrawable(getContext(), backgroundResourceId);
                    // 控件自带api,这里不用setBackgroundColor()因为在9.0测试不通过
                    // setBackgroundDrawable本来过时了,但是兼容包重写了方法
                    setBackgroundDrawable(drawable);
                } else {
                    // 获取皮肤包资源
                    Object skinResourceId = SkinManager.getInstance().getBackgroundOrSrc(backgroundResourceId);
                    // 兼容包转换
                    if (skinResourceId instanceof Integer) {
                        int color = (int) skinResourceId;
                        setBackgroundColor(color);
                        // setBackgroundResource(color); // 未做兼容测试
                    } else {
                        Drawable drawable = (Drawable) skinResourceId;
                        setBackgroundDrawable(drawable);
                    }
                }
            }
            invalidate();
        }
    }
    

    可以看出,ViewsMatch就是前文那个换肤接口,实现skinnableView的换肤回调方法,要注意的是需要换肤的属性都要自定义在attrs中,无论是自定义控件还是系统控件,也无论自定义的还是系统View自带的属性,因为只有在attrs中定义了,attrsBean才能收集到这个属性,以提供给控件换肤时使用。

    <!-- 自定义控件属性 -->
        <declare-styleable name="CustomCircleView">
            <attr name="circleColor" format="color" />
            <attr name="android:background" />
        </declare-styleable>
    

    再来看一个关键类SkinManager

    /**
     * 皮肤管理器
     * 加载应用资源(app内置:res/xxx) or 存储资源(下载皮肤包:net163.skin)
     */
    public class SkinManager {
    
        private static SkinManager instance; // 单例
        private Application application; // 拥有用户App环境,所以需要 application
        private Resources appResources; // 用于加载app内置资源 【本地Resources】
        private Resources skinResources; // 用于加载皮肤包资源  【外置Resources】
        private String skinPackageName; // 皮肤包资源所在包名(注:皮肤包不在app内,也不限包名)【外置皮肤包详细信息】
        private boolean isDefaultSkin = true; // 应用默认皮肤(app内置)【标记 换肤 和 默认】
        private static final String ADD_ASSET_PATH = "addAssetPath"; // 方法名
        private Map<String, SkinCache> cacheSkin; // 任何时候尽量用缓存
    
        private SkinManager(Application application) {
            this.application = application;
            appResources = application.getResources();
            cacheSkin = new HashMap<>();
        }
    
        /**
         * 单例方法,目的是初始化app内置资源(越早越好,用户的操作可能是:换肤后的第2次冷启动)
         */
        public static void init(Application application) {
            if (instance == null) {
                synchronized (SkinManager.class) {
                    if (instance == null) {
                        instance = new SkinManager(application);
                    }
                }
            }
        }
    
        public static SkinManager getInstance() {
            return instance;
        }
    
        /**
         * 加载皮肤包资源
         *
         * 收集皮肤包里面的所有信息
         *
         * @param skinPath 皮肤包路径,为空则加载app内置资源
         */
        public void loaderSkinResources(String skinPath) {
            // 优化:如果没有皮肤包或者没做换肤动作,方法不执行直接返回!
            if (TextUtils.isEmpty(skinPath)) {
                isDefaultSkin = true;
                return;
            }else{
                File file = new File(skinPath);
                Log.e("loaderSkinResources","file.exists()="+file.exists());
                if(!file.exists()){
                    isDefaultSkin = true;
                    return;
                }
            }
    
            // 优化:app冷启动、热启动可以取缓存对象
            if (cacheSkin.containsKey(skinPath)) {
                isDefaultSkin = false;
                SkinCache skinCache = cacheSkin.get(skinPath);
                if (null != skinCache) {
                    // 皮肤包资源 --- 图片 颜色 ... 信息集
                    skinResources = skinCache.getSkinResources();
    
                    // 皮肤薄路径---native层换肤需要的信息
                    skinPackageName = skinCache.getSkinPackageName();
                    return;
                }
            }
    
            // 开始真正的换肤 AssetManager.java  -----> AssetManager.cpp {换肤 皮肤包是否合格 AndroidManifest.xml}
            try {
                // 创建资源管理器(此处不能用:application.getAssets())
                AssetManager assetManager = AssetManager.class.newInstance();
                // 由于AssetManager中的addAssetPath和setApkAssets方法都被@hide,目前只能通过反射去执行方法
                Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
                // 设置私有方法可访问
                addAssetPath.setAccessible(true);
                // 执行addAssetPath方法
                addAssetPath.invoke(assetManager, skinPath);
                //==============================================================================
                // 如果还是担心@hide限制,可以反射addAssetPathInternal()方法,参考源码366行 + 387行
                //==============================================================================
    
                // 创建加载外部的皮肤包(net163.skin)文件Resources(注:依然是本应用加载)
                skinResources = new Resources(assetManager,
                        appResources.getDisplayMetrics(), appResources.getConfiguration());
    
                // 如果拿不到 packageName 证明 不是皮肤包 ,因为 皮肤包 有清单文件 和 资源映射表
                // 根据apk文件路径(皮肤包也是apk文件),获取该应用的包名。兼容5.0 - 9.0(亲测)
                skinPackageName = application.getPackageManager()
                        .getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
    
                // 无法获取皮肤包应用的包名,则加载app内置资源
                isDefaultSkin = TextUtils.isEmpty(skinPackageName);
                if (!isDefaultSkin) {
                    // 没有问题 就存储
                    cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
                }
    
                Log.e("skinPackageName >>> ", skinPackageName);
    
            } catch (Exception e) {
                e.printStackTrace();
                // 发生异常,预判:通过skinPath获取skinPacakageName失败!
                isDefaultSkin = true;
            }
        }
    
        /**
         * 参考:resources.arsc资源映射表
         * 通过ID值获取资源 Name 和 Type
         *
         * 能够 自动 区分  本地app资源加载  还是  皮肤包资源加载
         * resourceId == ghsy
         *
         * @param resourceId 资源ID值
         * @return 如果没有皮肤包则加载app内置资源ID,反之加载皮肤包指定资源ID
         */
        private int getSkinResourceIds(int resourceId) {
            // 优化:如果没有皮肤包或者没做换肤动作,直接返回app内置资源!
            if (isDefaultSkin) return resourceId;  // 本地app资源加载
    
    
            // TODO 加载 皮肤包里面的资源
    
            // 使用app内置资源加载,是因为内置资源与皮肤包资源一一对应(“netease_bg”, “drawable”)
            String resourceName = appResources.getResourceEntryName(resourceId);
            String resourceType = appResources.getResourceTypeName(resourceId);
    
            // 动态获取皮肤包内的指定资源ID
            // getResources().getIdentifier(“netease_bg”, “drawable”, “com.netease.skin.packages”);
            int skinResourceId = skinResources.getIdentifier(resourceName, resourceType, skinPackageName);
    
            // 源码1924行:(0 is not a valid resource ID.)
            isDefaultSkin = skinResourceId == 0;
            return skinResourceId == 0 ? resourceId : skinResourceId;
        }
    
        public boolean isDefaultSkin() {
            return isDefaultSkin;
        }
    
        //==============================================================================================
    
        public int getColor(int resourceId) {
            int ids = getSkinResourceIds(resourceId);
            return isDefaultSkin ? appResources.getColor(ids) : skinResources.getColor(ids);
        }
    
        public ColorStateList getColorStateList(int resourceId) {
            int ids = getSkinResourceIds(resourceId);
            return isDefaultSkin ? appResources.getColorStateList(ids) : skinResources.getColorStateList(ids);
        }
    
        // mipmap和drawable统一用法(待测)
        public Drawable getDrawableOrMipMap(int resourceId) {
            int ids = getSkinResourceIds(resourceId);
            return isDefaultSkin ? appResources.getDrawable(ids) : skinResources.getDrawable(ids);
        }
    
        public String getString(int resourceId) {
            int ids = getSkinResourceIds(resourceId);
            return isDefaultSkin ? appResources.getString(ids) : skinResources.getString(ids);
        }
    
        // 返回值特殊情况:可能是color / drawable / mipmap
        public Object getBackgroundOrSrc(int resourceId) {
            // 需要获取当前属性的类型名Resources.getResourceTypeName(resourceId)再判断
            String resourceTypeName = appResources.getResourceTypeName(resourceId);
    
            switch (resourceTypeName) {
                case "color":
                    return getColor(resourceId);
    
                case "mipmap": // drawable / mipmap
                case "drawable":
                    return getDrawableOrMipMap(resourceId);
            }
            return null;
        }
    
        // 获得字体
        public Typeface getTypeface(int resourceId) {
            // 通过资源ID获取资源path,参考:resources.arsc资源映射表
            String skinTypefacePath = getString(resourceId);
            // 路径为空,使用系统默认字体
            if (TextUtils.isEmpty(skinTypefacePath)) return Typeface.DEFAULT;
            return isDefaultSkin ? Typeface.createFromAsset(appResources.getAssets(), skinTypefacePath)
                    : Typeface.createFromAsset(skinResources.getAssets(), skinTypefacePath);
        }
    }
    
    

    有点长,但是并不复杂,这个类的作用

    1. 利用反射AssetManager调用addAssetPath方法加载皮肤包
    2. 提供了一些列getColor getDrawableOrMipMap等方法,供用户在换肤方法中调用,因为换肤方法中要根据条件(比如是日间还是夜间)来获取不同的资源,主工程中的资源直接可以通过context获取,而资源包中的资源则得通过自建的assetManager来获取。

    总结

    1. 换肤Activity继承SkinActivity
    2. SkinActivity中,在super.onCreate系统设置之前设置Factory2
    3. SkinActivity中,在onCreateView中使用自定义控件加载器(CustomAppCompatViewInflater),记录控件属性,将需要替换的控件替换成实现了换肤接口的自定义控件。(需换肤的属性问题:通过attrs解决;不同皮肤包的资源获取问题:通过SkinManager.getColor等解决)
    4. SkinActivity中,在skinDynamic中进行真正的换肤流程(对getWindow().getDecorView()进行迭代循环,使用接口方法skinnableView进行换肤)

    相关文章

      网友评论

          本文标题:换肤方式实现日/夜间模式

          本文链接:https://www.haomeiwen.com/subject/eipmuhtx.html