Android客户端主题切换 - LifengMr/ThemeChange GitHub Wiki

  Android客户端主题切换主要通过更改图片、颜色值等来改变UI效果,典型场景就是夜间模式的使用。本文也将以夜间模式为切入点,分析目前主要的几种实现方式。Support Lib 23.2.0提供了Theme.AppCompat.DayNight主题,但只适配API14及以上系统,这里不做过多介绍。

| 实现方式 | 应用app | Demo ----|------------- | -------------|----- 1 | 设置主题style | 内涵段子 | demo1 2 | 设置res-night资源 | XX | demo2 3 | themeloader库(LayoutInflater.Factory) | 暂无 | demo4 4 | java代码手动切换资源 | 今日头条 | demo3

#####一、设置主题style 1、创建Day和Night两种主题style,分别对应日间和夜间样式

<style name="DayNightTheme" parent="AppTheme" >
    <item name="s0">@color/s0</item>
    <item name="s1">@color/s1</item>
    <item name="s2">@color/s2</item>
    <item name="s3">@color/s3</item>
    <item name="s4">@color/s4</item>

    <item name="mainBackground">@color/s0</item>
    <item name="btnBackground">@drawable/bg_btn</item>
    <item name="image">@mipmap/image</item>
    <item name="btnStr">@string/btn_text</item>
</style>

<style name="DayNightTheme.Night">
    <item name="s1">@color/s1_night</item>
    <item name="s2">@color/s2_night</item>
    <item name="s3">@color/s3_night</item>
    <item name="s4">@color/s4_night</item>

    <item name="mainBackground">@color/s0_night</item>
    <item name="btnBackground">@drawable/bg_btn_night</item>
    <item name="image">@mipmap/image_night</item>
    <item name="btnStr">@string/btn_text_night</item>
</style>

2、java代码设置theme切换夜间模式

public void apply(Context context) {
    if (mIsNight) {
        context.setTheme(R.style.DayNightTheme_Night);
    } else{
        context.setTheme(R.style.DayNightTheme);
    }
}

#####二、设置res-night资源 1、在res资源中配置night资源 mipmap-night values-night etc. 2、java代码清除缓存资源并切夜间模式

public void toggle() {
    if (sUiNightMode == Configuration.UI_MODE_NIGHT_YES) {
        notNight();
    } else {
        night();
    }
}
public void night() {
    updateConfig(Configuration.UI_MODE_NIGHT_YES);
    System.gc();
    System.runFinalization(); 
    System.gc();
    mActivity.get().recreate();
}

#####三、设置主题style 1、配置主题资源

static {
    sMap.put(R.color.s0, R.color.s0_night);
    sMap.put(R.color.s1, R.color.s1_night);
    sMap.put(R.color.s2, R.color.s2_night);
    sMap.put(R.color.s3, R.color.s3_night);
    sMap.put(R.color.s4, R.color.s4_night);

    sMap.put(R.string.btn_text, R.string.btn_text_night);
    sMap.put(R.mipmap.image, R.mipmap.image_night);
    sMap.put(R.drawable.bg_btn, R.drawable.bg_btn_night);
}

2、根据主题获取资源

public static int getId(int resId, boolean night) {
    if (!night) {
        return resId;
    }
    if (sMap.isEmpty()) {
        return resId;
    }

    Integer value = sMap.get(resId);
    if (value != null) {
        resId = value.intValue();
    }
    return resId;
}
public static int getColor(Context context, int id, boolean night) {
    return context.getResources().getColor(getId(id, night));
}

3、手动切换资源

@Override
public void onThemeChanged() {
    super.onThemeChanged();

    drawViews();
}
private void drawViews() {
    boolean isNight = ThemeManger.ins().isNight();
    mLayout.setBackgroundColor(ThemeRes.getColor(this, R.color.s0, isNight));
    mBtn.setTextColor(ThemeRes.getColor(this, R.color.s4, isNight));
    mBtn.setText(ThemeRes.getString(this, R.string.btn_text, isNight));
    mBtn.setBackground(ThemeRes.getDrawable(this, R.drawable.bg_btn, isNight));
    mImg.setImageResource(ThemeRes.getId(R.mipmap.image, isNight));
}

#####四、设置主题style 1、自定义LayoutInflater.Factory,实现onCreateView方法

@Override
public View onCreateView(String name, Context context, AttributeSet attributeSet) {
    View view = null;
    try {
        view = createView(name, context, attributeSet);
    } catch (Exception e) {
    }

    //解析view的所有attr属性,并将theme loader库支持的主题替换属性保存到List
    parseAttrs(context, view, attributeSet);

    return view;
}
private View createView(String name, Context context, AttributeSet attributeSet)
        throws ClassNotFoundException {
    View view = null;
    LayoutInflater layoutInflater = LayoutInflater.from(context);
    if (-1 == name.indexOf(".")) {
        if(name.equals("Surface") || name.equals("SurfaceHolder") || name.equals("SurfaceView")
                || name.equals("TextureView") || name.equals("View")){
            view = layoutInflater.createView(name, "android.view.", attributeSet);
        } else if (name.equals("WebView")) {
            view = layoutInflater.createView(name, "android.webkit.", attributeSet);
        } else {
            view = layoutInflater.createView(name, "android.widget.", attributeSet);
        }
    } else {
        view = layoutInflater.createView(name, null, attributeSet);
    }

    return view;
}

2、为Activity设置以上自定义LayoutInflater.Factory

public ThemeLayoutFactory buildThemeFactory(Activity activity) {
    LayoutInflater inflater = activity.getLayoutInflater();
    ThemeLayoutInflaterCompat.setFactorySet(inflater, false);
    ThemeLayoutFactory factory = new ThemeLayoutFactory();
    inflater.setFactory(factory);
    return factory;
}

3、创建主题Resources

AssetManager assetManager = AssetManagerCompat.newInstance();
int cookie = AssetManagerCompat.addAssetPath(assetManager, themePath);
if (cookie == 0) {
    if (mOnThemeLoadListener != null) {
        mOnThemeLoadListener.onFailed();
    }
    return;
}
Resources resources = context.getResources();
mThemeResources = new Resources(assetManager, resources.getDisplayMetrics(),
        resources.getConfiguration());

4、从主题包中获取资源

public int getColor(int resId) {
    Resources hostResources = mContext.getResources();
    int hostColor = hostResources.getColor(resId);
    if (mThemeResources == null) {
        return hostColor;
    }

    String resName = hostResources.getResourceEntryName(resId);
    int themeResId = mThemeResources.getIdentifier(resName, "color", mThemePackageName);
    int themeColor = 0;
    try {
        themeColor = mThemeResources.getColor(themeResId);
    } catch (Exception e) {
        themeColor = hostColor;
    }
    return themeColor;
}

5、通知activity做主题更改

@Override
public void onThemeChanged() {
    if (mThemeLayoutFactory != null) {
        mThemeLayoutFactory.apply();
    }
}
//TextColorAttr
@Override
public void apply(View view) {
    if (view == null) {
        return;
    }
    if (view instanceof TextView) {
        TextView textView = (TextView)view;
        if(ATTR_TYPE_NAME_COLOR.equals(attrValueTypeName)) {
            textView.setTextColor(ThemeLoader.inst().getColorStateList(attrValueRefId));
        }
    }
}

备注:themeloader库目前暂不支持style、shape等属性,而且主题版本控制和签名校验也未添加,后续将持续维护并做支持。 #####四种实现方式优缺点对比:

实现方式 即时生效 支持两种以上主题 支持在线拉取
设置主题style x x x
设置res-night资源 x x x
java代码手动切换资源 x x
themeloader库(LayoutInflater.Factory)
⚠️ **GitHub.com Fallback** ⚠️