Android主题切换日夜间模式与换肤框架小结
1、Android主题切换日夜间模式
1、Google提供了夜间模式方案
-
首先你需要准备两套资源,一套日见一套夜间,以color为例:在res中新建values-night,然后新建color.xml,日间夜间资源命名要相同,但颜色不同。
-
第一步准备好,就可以这样使用,配合SharedPreferences,记录我们是开夜间还是日间,在Application这样写:
SharedPreferences sp = getSharedPreferences("theme_mode",MODE_PRIVATE); boolean nightMode = sp.getBoolean("is_night_mode", false); AppCompatDelegate.setDefaultNightMode(nightMode ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
-
对于上面的,介绍下 AppCompatDelegate.setDefaultNightMode几种模式:
MODE_NIGHT_NO: 亮色(light)主题,不使用夜间模式 MODE_NIGHT_YES:暗色(dark)主题,使用夜间模式 MODE_NIGHT_AUTO:根据当前时间自动切换 亮色( light )/暗色( dark )主题(22:00-07:00时间段内自动切换为夜间模式) MODE_NIGHT_FOLLOW_SYSTEM(默认选项):设置为跟随系统,通常为MODE_NIGHT_NO
-
在Activity中切换:
// 获取当前模式 int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; // 将为夜间模式保存到SharedPreferences SharedPreferences sp = getSharedPreferences("theme_mode",MODE_PRIVATE); sp.edit().putBoolean("is_night_mode",currentNightMode == Configuration.UI_MODE_NIGHT_NO).commit(); // 切换模式 getDelegate().setDefaultNightMode(currentNightMode == Configuration.UI_MODE_NIGHT_NO ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO); // 重启Activity //recreate(); recreate存在一些问题 startActivity(new Intent(this,MainActivity.class)); finish();
2、Android-skin-support
1、使用:
github写的很详细
https://github.com/ximsfei/Android-skin-support#%E5%AF%BC%E5%85%A5
2、原理分析:
LayoutInflater有一个内部接口Factory,系统会使用它去做XML到View的转换,而系统也提供了setFactory的方法,用户设置了,则用我们设置的,这样系统就会走我们Factory的onCreateView,他会返回一个我们定制化的View。
-
首先,Activity会在onCreate中初始化Factory,我们没有设置就返回系统默认的:
@Override public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory(layoutInflater, this); //系统默认的 } else { if (!(LayoutInflaterCompat.getFactory(layoutInflater) instanceof AppCompatDelegateImplV9)) { Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed" + " so we can not install AppCompat's"); } } }
-
Factory会在layoutInflater.inflate,最终会走Factory.createView,看看系统的:
@Override public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (mAppCompatViewInflater == null) { mAppCompatViewInflater = new AppCompatViewInflater(); } boolean inheritContext = false; if (IS_PRE_LOLLIPOP) { inheritContext = (attrs instanceof XmlPullParser) // If we have a XmlPullParser, we can detect where we are in the layout ? ((XmlPullParser) attrs).getDepth() > 1 // Otherwise we have to use the old heuristic : shouldInheritContext((ViewParent) parent); } 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 */ ); }
-
再看下去:
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 AppCompatTextView(context, attrs); break; case "ImageView": view = new AppCompatImageView(context, attrs); break; case "Button": view = new AppCompatButton(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; } 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; }
-
可以看到系统也是根据XML解析的名字new出控件,但上面都是AppCompat类型,其他类型其实的通过类加载器创建出来的,那么XML使用TextView其实是AppCompatTextView,利用这一个原理,看看Android-skin-support:
private View createViewFromFV(Context context, String name, AttributeSet attrs) { View view = null; if (name.contains(".")) { return null; } switch (name) { case "View": view = new SkinCompatView(context, attrs); break; case "LinearLayout": view = new SkinCompatLinearLayout(context, attrs); break; case "RelativeLayout": view = new SkinCompatRelativeLayout(context, attrs); break; case "FrameLayout": view = new SkinCompatFrameLayout(context, attrs); break; case "TextView": view = new SkinCompatTextView(context, attrs); break; case "ImageView": view = new SkinCompatImageView(context, attrs); break; case "Button": view = new SkinCompatButton(context, attrs); break; case "EditText": view = new SkinCompatEditText(context, attrs); break; case "Spinner": view = new SkinCompatSpinner(context, attrs); break; case "ImageButton": view = new SkinCompatImageButton(context, attrs); break; case "CheckBox": view = new SkinCompatCheckBox(context, attrs); break; case "RadioButton": view = new SkinCompatRadioButton(context, attrs); break; case "RadioGroup": view = new SkinCompatRadioGroup(context, attrs); break; case "CheckedTextView": view = new SkinCompatCheckedTextView(context, attrs); break; case "AutoCompleteTextView": view = new SkinCompatAutoCompleteTextView(context, attrs); break; case "MultiAutoCompleteTextView": view = new SkinCompatMultiAutoCompleteTextView(context, attrs); break; case "RatingBar": view = new SkinCompatRatingBar(context, attrs); break; case "SeekBar": view = new SkinCompatSeekBar(context, attrs); break; case "ProgressBar": view = new SkinCompatProgressBar(context, attrs); break; case "ScrollView": view = new SkinCompatScrollView(context, attrs); break; } return view; }
-
这就返回了自己定制好的View了,那么回到前面,我们需要在super.onCreate前设置自己的Factory,不可能每个Activity都设置一遍,那么利用Application.ActivityLifecycleCallbacks统一设置:
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { if (activity instanceof AppCompatActivity) { LayoutInflater layoutInflater = activity.getLayoutInflater(); try { Field field = LayoutInflater.class.getDeclaredField("mFactorySet"); field.setAccessible(true); field.setBoolean(layoutInflater, false); LayoutInflaterCompat.setFactory(activity.getLayoutInflater(), getSkinDelegate((AppCompatActivity) activity)); } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } updateStatusBarColor(activity); updateWindowBackground(activity); } }