日/夜模式切换

日/夜模式切换作为一个App的基本功能经常会被使用到,接下来就举出一些常用的日/夜模式切换的方法

使用UIMode的方法
这种方式操作起来比较简单,就是将不同模式下的资源分开存放,然后调用方法切换资源即可

  • 资源存放的路径

pic

pic

  • 切换资源的方法
1
2
3
4
5
6
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

  • 最终效果

gif

  • 存在问题

由于需要recreate(),会重绘Activity导致屏幕闪烁,重新加载Avtivity时需要注意Activity内元素的保存

使用Theme
通过切换不同的主题来实现切换日/夜模式的效果

  • 在attrs.xml中设置主题中需要替换资源
1
2
3
4
5
<resources>
<attr name="bg" format="color"></attr>
<attr name="button_bg" format="color"></attr>
<attr name="button_tv" format="color"></attr>
</resources>
  • 设置日/夜模式主题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <style name="Day" parent="AppTheme">
<item name="bg">#FFFFFF</item>
<item name="button_bg">#A9A9A9</item>
<item name="button_tv">#000000</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
</style>

<style name="Night" parent="AppTheme">
<item name="bg">#4F4F4F</item>
<item name="button_bg">#000000</item>
<item name="button_tv">#FFFFFF</item>
<item name="colorPrimary">#828282</item>
<item name="colorPrimaryDark">#828282</item>
</style>
  • 在layout文件中使用arrts中的资源属性
1
android:backgroundTint="?attr/button_bg"
  • 在Activity中切换主题
1
2
3
4
5
 if(isNight){
setTheme(R.style.Night);
}else{
setTheme(R.style.Day);
}

设置主题需要放在setContentView()之前,所以每次切换完日/夜模式后都需要重新加载Activity

  • 存在问题

与UIMode方法相同由于需要recreate(),会重绘Activity导致屏幕闪烁,并且在有比较多的属性需要修改时会导致style比较复杂

为了决解改变日/夜模式后屏幕闪烁的问题,我看了不少博客,终于找到了一个比较符合要求的项目,能够实现uiMode方法的不重建Activity切换日/夜模式。
项目地址:https://github.com/geminiwen/SkinSprite

效果:
gif

具体思路是在Activity创建View的过程中注入自己的代码。
接下来分析一下这个lib的具体代码

  • 首先是SkinnableActivity,继承自AppCompatActivity,在这个Activity中对View的创建进行拦截,我们主要关注三个方法

1)

1
2
3
4
5
6
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory(layoutInflater, this);
super.onCreate(savedInstanceState);
}

注入自己的LayoutInflatorFactory,使inflate在这个LayoutInflaterFactory中执行

2)

1
2
3
4
5
6
7
8
9
10
11
12
13
@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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);

}

这是一个我们之后需要切换日夜模式需要调用的方法,其中主要调用逻辑有

1
getDelegate().setLocalNightMode(nightMode);

对系统日/夜模式的资源进行切换

1
2
3
4
if (isPost21) {
applyDayNightForStatusBar();
applyDayNightForActionBar();
}

如果api等级大于等于21(即5.0及以上版本)则更换状态栏和标题栏资源

1
applyDayNightForView(decorView);

对于内容中的日/夜资源进行切换,这个方法我们可以看下他的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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中资源的方法。

  • 接下来可以看下上面的SkinnableViewInflater这个类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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都做出来,但是都大同小异,如果不够用还可以自己添加

  • 最后只剩下View的没有看了,由于View比较多而且其中的内容都相似,我们拿SkinnableLinearLayout作为案例进行研究
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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实现日夜间模式的深入理解

猜你喜欢

转载自www.cnblogs.com/dajunjun/p/11724409.html
今日推荐