日/夜模式切换作为一个App的基本功能经常会被使用到,接下来就举出一些常用的日/夜模式切换的方法
使用UIMode的方法
这种方式操作起来比较简单,就是将不同模式下的资源分开存放,然后调用方法切换资源即可
if (isNight) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
recreate();
其中AppCompatDelegate.MODE_NIGHT_YES代表切换到夜间模式,AppCompatDelegate.MODE_NIGHT_NO代表切换到日间模式
如果是在新的Activity中切换日/夜模式则需要用RxBus(关于RxBus的用法可以参照我上篇博客)通知在后台的Activity调用recreate()重启Activity
由于需要recreate(),会重绘Activity导致屏幕闪烁,重新加载Avtivity时需要注意Activity内元素的保存
使用Theme
通过切换不同的主题来实现切换日/夜模式的效果
<resources>
<attr name="bg" format="color">attr>
<attr name="button_bg" format="color">attr>
<attr name="button_tv" format="color">attr>
resources>
android:backgroundTint="?attr/button_bg"
if(isNight){
setTheme(R.style.Night);
}else{
setTheme(R.style.Day);
}
设置主题需要放在setContentView()之前,所以每次切换完日/夜模式后都需要重新加载Activity
与UIMode方法相同由于需要recreate(),会重绘Activity导致屏幕闪烁,并且在有比较多的属性需要修改时会导致style比较复杂
为了决解改变日/夜模式后屏幕闪烁的问题,我看了不少博客,终于找到了一个比较符合要求的项目,能够实现uiMode方法的不重建Activity切换日/夜模式。
项目地址:https://github.com/geminiwen/SkinSprite
效果:
具体思路是在Activity创建View的过程中注入自己的代码。
接下来分析一下这个lib的具体代码
1)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory(layoutInflater, this);
super.onCreate(savedInstanceState);
}
注入自己的LayoutInflatorFactory,使inflate在这个LayoutInflaterFactory中执行
2)
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (mSkinnableViewInflater == null) {
mSkinnableViewInflater = new SkinnableViewInflater();
}
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mSkinnableViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* 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 */
);
}
这是具体需要创建View的方法,可以看到他将具体的创建逻辑放到了SkinnableViewInflater中,这个类之后在做分析
3)
public void setDayNightMode(@AppCompatDelegate.NightMode int nightMode) {
final boolean isPost21 = Build.VERSION.SDK_INT >= 21;
getDelegate().setLocalNightMode(nightMode);
if (isPost21) {
applyDayNightForStatusBar();
applyDayNightForActionBar();
}
View decorView = getWindow().getDecorView();
applyDayNightForView(decorView);
}
这是一个我们之后需要切换日夜模式需要调用的方法,其中主要调用逻辑有
getDelegate().setLocalNightMode(nightMode);
对系统日/夜模式的资源进行切换
if (isPost21) {
applyDayNightForStatusBar();
applyDayNightForActionBar();
}
如果api等级大于等于21(即5.0及以上版本)则更换状态栏和标题栏资源
applyDayNightForView(decorView);
对于内容中的日/夜资源进行切换,这个方法我们可以看下他的具体实现
private void applyDayNightForView(View view) {
if (view instanceof Skinnable) {
Skinnable skinnable = (Skinnable) view;
if (skinnable.isSkinnable()) {
skinnable.applyDayNight();
}
}
if (view instanceof ViewGroup) {
ViewGroup parent = (ViewGroup)view;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
applyDayNightForView(parent.getChildAt(i));
}
}
}
可以看到这是一个递归的方法,功能是遍历了view下所有的子view,对实现了Skinnable接口并且isSkinnable()返回true的view调用applyDayNight()。可以猜想到这些View就是自定义的View,这个applyDayNight()就是刷新View中资源的方法。
public final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new SkinnableTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new SkinnableButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
case "LinearLayout":
view = new SkinnableLinearLayout(context, attrs);
break;
case "FrameLayout":
view = new SkinnableFrameLayout(context, attrs);
break;
case "RelativeLayout":
view = new SkinnableRelativeLayout(context, attrs);
break;
case "android.support.v7.widget.Toolbar":
view = new SkinnableToolbar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
这个是SkinnableViewInflater中最主要的方法,根据name创建出不同的View,即自定义的View,这里并没有把所有的view都做出来,但是都大同小异,如果不够用还可以自己添加
public SkinnableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mAttrsHelper = new AttrsHelper();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.SkinnableView,
defStyleAttr, 0);
mAttrsHelper.storeAttributeResource(a, R.styleable.SkinnableView);
a.recycle();
}
@Override
public void applyDayNight() {
Context context = getContext();
int key;
key = R.styleable.SkinnableView[R.styleable.SkinnableView_android_background];
int backgroundResource = mAttrsHelper.getAttributeResource(key);
if (backgroundResource > 0) {
Drawable background = ContextCompat.getDrawable(context, backgroundResource);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
setBackgroundDrawable(background);
} else {
setBackground(background);
}
}
}
SkinnableLinearLayout中的思路也比较简单,在构造方法中向mAttrsHelper添加如属性,在需要刷新是再从mAttrsHelper中取出。对于不同的View也只是属性的内容不同而已
至此就是这个项目的大致源码,然后只需要将Activity继承SkinnableActivity,将uiMode中的
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
改为
setDayNightMode(AppCompatDelegate.MODE_NIGHT_YES);
并且不需要调用recreate()方法。需要注意的是还需要在该Activity中添加上android:configChanges=”uiMode”。
结语:这是我第一次写关于阅读源码的博客,尽管选了一个比较简单的lib但还是表达得比较凌乱。我之后还是会多多尝试写这方面的博客,努力提高自己的水平。
参考博客:
android 实现【夜晚模式】的另外一种思路
Android通过改变主题实现夜间模式
Android实现日夜间模式的深入理解