我们先分析一下源码:
1. 思考xml布局文件是如何parse解析成控件加载到根布局的
用到的类有:
分析点有2条:
protected void onCreate(Bundle savedInstanceState) {
//分析1: super.onCreate(savedInstanceState)最终会走到AppCompatActivity.onCreate方法,
//分析截止是在AppDelegateImpl.installViewFactory
super.onCreate(savedInstanceState);
//分析2:分析xml布局是如何经过parse解析添加到content布局中
// 最终会调用到PhoneWindow.setContentView方法
setContentView(R.layout.activity_main);
}
MainActivity:
/**
* Api28源码分析
*/
public class MainActivity extends SkinActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//分析1: super.onCreate(savedInstanceState)最终会走到AppCompatActivity.onCreate方法,
//分析截止是在AppDelegateImp.installViewFactory
super.onCreate(savedInstanceState);
//分析2:分析xml布局是如何经过parse解析添加到content布局中
// 最终会调用到PhoneWindow.setContentView方法
//分析截止是在LayoutInflate.createViewFromTag,并最终回调到 mFactory2.onCreateView-->AppDelegateImpl.onCreateView中
setContentView(R.layout.activity_main);
}
}
super.onCreate(savedInstanceState);方法会走到AppCompatActivity方法中:
AppCompatActivity:
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
//注意这句话,最终会调用到AppCompatDelegateImpl.installViewFactory()中去
//作用就是设置Factory2,当调用Factory2.onCreateView方法的时候,会走到AppCompatDelegateImpl的onCreateView里
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
}
delegate.installViewFactory();会走到AppCompatDelegateImpl的installViewFactory方法中
AppCompatDelegateImpl:
/**
* @author Eason
* @createtime 2020/3/19
* @desc 注意实现了LayoutInflater.Factory2接口
*/
class AppCompatDelegateImpl extends AppCompatDelegate implements MenuBuilder.Callback, LayoutInflater.Factory2 {
/**
* 是在LayoutInflater这个类中的inflate方法中
* 会被view = mFactory2.onCreateView(parent, name, context, attrs);调用到这个方法里面来
*/
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return createView(parent, name, context, attrs);
}
public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
//下面的if语句就是想方设法创建出来mAppCompatViewInflater
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName = a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if ((viewInflaterClassName == null) || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
Log.i(TAG, "Failed to instantiate custom view inflater "
+ viewInflaterClassName + ". Falling back to default.", t);
mAppCompatViewInflater = new AppCompatViewInflater();
}
}
}
//最终会调用到AppCompatViewInflater.createView方法中去
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
/**
* 这个方法是在 delegate.installViewFactory()中调用到
* 作用:设置setFactory2,把自己作为 LayoutInflaterCompat.setFactory2的第二个参数传进去
* Factory2是LayoutInflater的内部类,里面只有一个方法public View onCreateView()
* 隐藏当调用到mFactory2.onCreateView的时候实际就会调到自己这个类AppCompatDelegateImpl的onCreateView方法
*/
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
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");
}
}
}
}
以上就是super.onCreate(savedInstanceState);的分析,其作用就是设置Factory2
接下来分析setContentView(R.layout.activity_main):
会走到PhoneWindow的setContent方法中
PhoneWindow:
public class PhoneWindow extends Window implements MenuBuilder.Callback {
/**
* MainActivity.setContentView-->Activity.setContentView-->PhoneWindow.setContentView
*
* @param layoutResID 传入的布局id,此处可以理解为R.layout.activity_main
*/
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
/**
* 这个方法作用:
* 1.生成DecorView对象(DecorView是继承FrameLayout)
* 2.初始化mContentParent对象,就是R.layout.screen_simple布局中@android:id/content的FragmeLayout
*/
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
/**
*这个方法作用:
* 将传进来的布局加载到mContentParent容器中[在讲换肤的时候会详细讲这个方法]
*/
//会走到LayoutInflate这个类中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
/**
* 里面有两个重要的方法:
* 1. generateDecor:如果mDecor为空,则生成DecorView
* 2. generateLayout:
*/
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//生成DecorView,DecorView是继承FrameLayout
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//
mContentParent = generateLayout(mDecor);
}
}
/**
* 生成DecorView 对象
*/
protected DecorView generateDecor(int featureId) {
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
/**
*找相应的加载布局,加载到DecorView中,然后返回布局中FragmeLayout对象
* @param decor
* @return 布局中id为ID_ANDROID_CONTENT的容器
*/
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
//省略...
//解析属性值
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
requestFeature(FEATURE_ACTION_BAR);
}
if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
// 根据features和属性的不同,来给layoutResource赋值,实际就是找相应的加载布局
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if {
//省略...
}else{
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//将找到的布局layoutResource加入到DecorView中去
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//找到layoutResource布局中id为ID_ANDROID_CONTENT的控件,然后返回
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
//省略...
return contentParent;
}
}
mLayoutInflater.inflate(layoutResID, mContentParent);会走到LayoutInflate这个类中
LayoutInflate:
public abstract class LayoutInflater {
/**
* 是被PhoneWindow.setContentView方法调用到
*
* @param resource 实际要加载的布局,如R.layout.activity_main
* @param root 容器布局id为content的那个
* @return 返回布局
*/
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
//注意这句话
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
View result = root;
//...
if (TAG_MERGE.equals(name)) {
//如果含有merge标签
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//root就是R.layout.screen_simple布局中@android:id/content的FragmeLayout
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
root.addView(temp, params);
if (root == null || !attachToRoot) {
result = temp;
}
}
return result;
}
}
}
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
View view;
//由于在MainActivity的super.onCreate(savedInstanceState);方法中设置了Factory2,并将AppCompatDelegateImpl设置为第二个参数
if (mFactory2 != null) {
//如果自己设置了mFactory2,则会走到mFactory2.onCreateView方法里面,通过debug发现mFactory2就是AppCompatDelegateImpl
//而实际就是调用AppCompatDelegateImpl方法中去
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//判断是否是自定义View
if (-1 == name.indexOf('.')) {
//最终也是会走到createView
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
protected View onCreateView(View parent, String name, AttributeSet attrs)
throws ClassNotFoundException {
return onCreateView(name, attrs);
}
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
/**
* 真正的通过反射来创建View
*/
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
//...
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
}
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
}
以上就是针对源码的分析。
那么回到主题如何根据理解的源码进行系统内换肤呢?
方案:将布局中的常规控件进行包装成可换肤的控件,如TextView-->SkinTextView,其中SkinTextView实现了changeSkin接口。
当用户点击切换的时候,修改系统配置为夜间模式或日间模式,然后执行代码:获取根布局DecorView,遍历控件取得实现changeSkin接口的布局然后调用改变布局的方法。
先看效果:
日间模式:
夜间模式:
第二个界面:
注意,改变第一个界面第二个界面也会被改变。
接下来实现:
新建lib_skin包:
ISkinable:
/**
* @author Eason
* @createtime 2020/3/19
* @desc 皮肤切换的接口,所有需要修改日/夜间模式的都要实现这个接口
*/
public interface ISkinable {
/**
* 切换模式
*/
void changeSkin();
}
MyAppCompatViewInflater:
/**
* @author Eason
* @createtime 2020/3/19
* @desc 实现自定义的AppCompatViewInflater,主要功能就是对各种View进行匹配封装成自己的SkinXxView
*/
public class MyAppCompatViewInflater extends AppCompatViewInflater {
private final String TAG = this.getClass().getSimpleName();
private Context mContext;
public MyAppCompatViewInflater(Context context) {
this.mContext = context;
}
public View generateView(String name, AttributeSet attrs) {
Log.d(TAG, "generateView-->" + name);
View view;
switch (name) {
case "TextView":
view = new SkinTextView(mContext, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = new SkinButton(mContext, attrs);
verifyNotNull(view, name);
break;
case "androidx.constraintlayout.widget.ConstraintLayout":
view = new SkinConstraintLayout(mContext, attrs);
verifyNotNull(view, name);
break;
//ImageView、LinearLayout、RelativeLayout、自定义View...
default:
view = super.createView(mContext, name, attrs);
}
return view;
}
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");
}
}
}
SkinConstraintLayout:
public class SkinConstraintLayout extends ConstraintLayout implements ISkinable {
private final String TAG = this.getClass().getSimpleName();
private int[] mAttrs = R.styleable.SkinConstraintLayout;
private SparseIntArray mResourceMap = new SparseIntArray();
public SkinConstraintLayout(Context context) {
this(context, null);
}
public SkinConstraintLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SkinConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, mAttrs, defStyleAttr, 0);
for (int i = 0; i < a.length(); i++) {
mResourceMap.put(mAttrs[i], a.getResourceId(i, -1));
}
a.recycle();
}
@Override
public void changeSkin() {
Log.d(TAG, "changeSkin--->" + TAG);
//设置background
int backgroundKey = mAttrs[R.styleable.SkinConstraintLayout_android_background];
int backgroundId = mResourceMap.get(backgroundKey);
if (backgroundId > 0) {
setBackground(ContextCompat.getDrawable(getContext(), backgroundId));
}
}
}
SkinTextView:
/**
* 继承TextView兼容包,9.0源码中也是如此
* 参考:AppCompatViewInflater.java
* 86行 + 138行 + 206行
*/
public class SkinTextView extends AppCompatTextView implements ISkinable {
private final String TAG = this.getClass().getSimpleName();
private int[] mAttrs = R.styleable.SkinTextView;
private SparseIntArray mResourceMap = new SparseIntArray();
public SkinTextView(Context context) {
this(context, null);
}
public SkinTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public SkinTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 根据自定义属性,匹配控件属性的类型集合,如:background + textColor
TypedArray a = context.obtainStyledAttributes(attrs, mAttrs, defStyleAttr, 0);
//R.styleable.SkinTextView 为在attrs定义的属性值数组
for (int i = 0; i < mAttrs.length; i++) {
mResourceMap.put(mAttrs[i], a.getResourceId(i, -1));
}
a.recycle();
}
@Override
public void changeSkin() {
Log.d(TAG, "changeSkin--->" + TAG);
//设置background
// 根据自定义属性,获取styleable中的background属性
int backgroundKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_background];
int backgroundId = mResourceMap.get(backgroundKey);
if (backgroundId > 0) {
setBackgroundDrawable(ContextCompat.getDrawable(getContext(), backgroundId));
}
//设置textColor
int textColorKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_textColor];
int textColorId = mResourceMap.get(textColorKey);
if (textColorId > 0) {
setTextColor(ContextCompat.getColorStateList(getContext(), textColorId));
}
}
}
Const:
public class Const {
public static final String SP_NAME = "sp_lib_skin";
public static final String KEY_NIGHT_MODE = "isNight";
}
SkinActivity:
/**
* 如果要实现换肤功能,所有Activity要继承SkinActivity,并复写isOpenSkin方法
* 因为默认是关闭的
*/
public class SkinActivity extends AppCompatActivity {
private final String TAG = this.getClass().getSimpleName();
private MyAppCompatViewInflater mAppCompatViewInflater;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (isOpenSkin()) {
boolean isNight = getSharedPreferences(Const.SP_NAME, MODE_PRIVATE).getBoolean(Const.KEY_NIGHT_MODE, false);
getDelegate().setLocalNightMode(isNight ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), this);
}
super.onCreate(savedInstanceState);
}
/**
* 是否开启换肤功能
*
* @return true:开启换肤功能,子类可以复用
*/
protected boolean isOpenSkin() {
return false;
}
/**
* 此方法会被多次调用,界面只有一个 ImageView 和 Button时候的日志打印如下
* onCreateView-->LinearLayout
* onCreateView-->LinearLayout
* onCreateView-->ViewStub
* onCreateView-->ViewStub
* onCreateView-->FrameLayout
* onCreateView-->FrameLayout
* onCreateView-->androidx.appcompat.widget.ActionBarOverlayLayout
* onCreateView-->androidx.appcompat.widget.ActionBarOverlayLayout
* onCreateView-->androidx.appcompat.widget.ContentFrameLayout
* onCreateView-->androidx.appcompat.widget.ContentFrameLayout
* onCreateView-->androidx.appcompat.widget.ActionBarContainer
* onCreateView-->androidx.appcompat.widget.ActionBarContainer
* onCreateView-->androidx.appcompat.widget.Toolbar
* onCreateView-->androidx.appcompat.widget.Toolbar
* onCreateView-->androidx.appcompat.widget.ActionBarContextView
* onCreateView-->androidx.appcompat.widget.ActionBarContextView
* onCreateView-->androidx.constraintlayout.widget.ConstraintLayout
* onCreateView-->androidx.constraintlayout.widget.ConstraintLayout
* onCreateView-->ImageView
* onCreateView-->ImageView
* onCreateView-->Button
* onCreateView-->Button
*/
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (isOpenSkin()) {
//防止重复创建
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new MyAppCompatViewInflater(context);
}
return mAppCompatViewInflater.generateView(name, attrs);
} else {
return super.onCreateView(name, context, attrs);
}
}
/**
* 设置显示模式(夜间/日间)
*
* @param isNight true:夜间模式
*/
protected void setNightMode(boolean isNight) {
if (isOpenSkin()) {
//int uiMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
getSharedPreferences(Const.SP_NAME, MODE_PRIVATE).edit().putBoolean(Const.KEY_NIGHT_MODE, isNight).apply();
getDelegate().setLocalNightMode(isNight ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
View decorView = getWindow().getDecorView();
setNightMode(decorView);
}
}
/**
* 给具体View设置夜间模式
*
* @param view 具体控件
*/
private void setNightMode(View view) {
if (view instanceof ISkinable) {
//如果实现了ISkinable接口,则调用接口changeSkin方法改变皮肤
((ISkinable) view).changeSkin();
}
if (view instanceof ViewGroup) {
//继续遍历子节点
int childCount = ((ViewGroup) view).getChildCount();
for (int i = 0; i < childCount; i++) {
View child = ((ViewGroup) view).getChildAt(i);
setNightMode(child);
}
}
}
}
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- SkinXx的统一控件属性 -->
<declare-styleable name="SkinCommonAttr">
<attr name="android:background" />
<attr name="android:textColor" />
</declare-styleable>
<!-- SkinConstraintLayout控件属性 -->
<declare-styleable name="SkinConstraintLayout">
<attr name="android:background" />
</declare-styleable>
<!-- TextView控件属性 -->
<declare-styleable name="SkinTextView">
<attr name="android:background" />
<attr name="android:textColor" />
<attr name="android:textSize" />
</declare-styleable>
<!-- SkinButton控件属性 -->
<declare-styleable name="SkinButton">
<attr name="android:background" />
<attr name="android:textColor" />
</declare-styleable>
</resources>
以上就是lib_skin的编写。
接下来我们看看该如何在app内使用:
1. 首先在app的build.gradle引入lib_skin依赖
2. MainAtivity继承SkinActivity并开启isOpenSkin标志
调用方法
setNightMode(true);开启夜间模式
setNightMode(false);关闭夜间模式
public class MainActivity extends SkinActivity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
textView.setText("当前模式" + getDelegate().getLocalNightMode());
}
public void jump(View view) {
startActivity(new Intent(this, SecondActivity.class));
}
public void nightMode(View view) {
setNightMode(true);
textView.setText("当前模式" + getDelegate().getLocalNightMode());
}
public void dayMode(View view) {
setNightMode(false);
textView.setText("当前模式" + getDelegate().getLocalNightMode());
}
@Override
protected boolean isOpenSkin() {
return true;
}
}
为了实现这个效果需要用两套color.xml文件
values-colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
<color name="color_group_bg">#FF6600</color>
<color name="color_txt">#f00</color>
<color name="color_bg">#0f0</color>
</resources>
values-night-colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#1D8F43</color>
<color name="colorPrimaryDark">#4DC67A</color>
<color name="colorAccent">#262E2D</color>
<color name="color_group_bg">#252424</color>
<color name="color_txt">#000000</color>
<color name="color_bg">#FFFFFF</color>
</resources>
运行发现功能实现了:
但是在切换的时候会出现卡顿一下的效果。
为了解决这个问题,需要在manifest中Activity配置android:configChanges="uiMode":
这样就可以了。
但是加上这个句话的时候,toolbar和状态栏已经导航栏都没有切换成功,模拟器6.0.1版本。
如下图:
接下来我们引入几个工具类针对性处理他们:
StatusBarUtils :
public class StatusBarUtils {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void forStatusBar(Activity activity) {
TypedArray a = activity.getTheme().obtainStyledAttributes(0, new int[]{
android.R.attr.statusBarColor
});
int color = a.getColor(0, 0);
activity.getWindow().setStatusBarColor(color);
a.recycle();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public static void forStatusBar(Activity activity, int skinColor) {
activity.getWindow().setStatusBarColor(skinColor);
}
}
NavigationUtils:
public class NavigationUtils {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void forNavigation(Activity activity) {
TypedArray a = activity.getTheme().obtainStyledAttributes(0, new int[] {
android.R.attr.statusBarColor
});
int color = a.getColor(0, 0);
activity.getWindow().setNavigationBarColor(color);
a.recycle();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void forNavigation(Activity activity, int skinColor) {
activity.getWindow().setNavigationBarColor(skinColor);
}
}
ActionBarUtils:
public class ActionBarUtils {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void forActionBar(AppCompatActivity activity) {
TypedArray a = activity.getTheme().obtainStyledAttributes(0, new int[]{
android.R.attr.colorPrimary
});
int color = a.getColor(0, 0);
a.recycle();
ActionBar actionBar = activity.getSupportActionBar();
if (actionBar != null) {
actionBar.setBackgroundDrawable(new ColorDrawable(color));
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static void forActionBar(AppCompatActivity activity, int skinColor) {
ActionBar actionBar = activity.getSupportActionBar();
if (actionBar != null) {
actionBar.setBackgroundDrawable(new ColorDrawable(skinColor));
}
}
}
拓展:实际上是用到了Android系统自带的模式切换,但同样也要两套color:
END.