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

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

作者: 瑜小贤 | 来源:发表于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进行换肤)

相关文章

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

    一、夜间模式的实现方式夜间模式的实现方式有两种,一种是本地替换,一种是用动态换肤的方案替换。但是两中方案的思想大致...

  • iOS关于换肤和夜间模式的一些思考

    iOS关于换肤和夜间模式的一些思考 iOS关于换肤和夜间模式的一些思考

  • 夜间模式实践

    现状 夜间模式是android换肤的一种,关于换肤的相关知识总结,大家可以参考这篇文章Android换肤技术总结-...

  • 夜间模式实现方式

    iOS 夜间模式的应用场景在浏览器,小说类产品上应用较多。 实现方式也多样化。 方案一 比如小说类,总共没有几个页...

  • 夜间模式

    标签(空格分隔): Android 1、通过切换theme来实现夜间模式。2、通过资源id映射的方式来实现夜间模式...

  • Android 夜间模式 换肤

    本文为原创,转载请注明出处。文章标题为夜间模式,其实我这里想说的不是什么夜间模式,而是Skin库,有了Skin库,...

  • iOS-夜间模式(换肤设置)

    iOS 开发中有时候会有夜间模式(换肤设置)的需求, 其实主要是更改相关颜色操作! 思路:每次切换夜间/白天模式...

  • iOS 换肤-黑夜、黑暗、夜间模式

    iOS 开发中有时候会有夜间模式(换肤设置)的需求, 其实主要是更改相关颜色操作! 思路:每次切换夜间/白天模式...

  • Android一键换肤功能实现

    市面上对数的App都提供换肤功能,这里暂且不讲白天和夜间模式 下图是网易云音乐的换肤功能 换肤其实就是替换资源(文...

  • android 夜间模式(换肤)总结

    一、android 平台常见的换肤方案. Android 平台常见的额换肤方式总结起来有如下三种: 1.设置set...

网友评论

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

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