因为项目须要,近段时间开发的夜间模式功能。主流的方案以下:android
一、经过切换theme实现 二、经过resource id映射实现 三、经过Android Support Library的实现
切换theme实现夜间模式
采用这种实现方式的表明是简书和知乎~实现策略以下:缓存
1)在xml中定义两套theme,差异仅仅是颜色不一样
<!--白天主题--> <style name="DayTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <item name="clockBackground">@android:color/white</item> <item name="clockTextColor">@android:color/black</item> </style> <!--夜间主题--> <style name="NightTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="colorPrimary">@color/color3F3F3F</item> <item name="colorPrimaryDark">@color/color3A3A3A</item> <item name="colorAccent">@color/color868686</item> <item name="clockBackground">@color/color3F3F3F</item> <item name="clockTextColor">@color/color8A9599</item> </style>
自定义颜色:app
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="clockBackground" format="color" /> <attr name="clockTextColor" format="color" /> </resources>
在layout布局文件中,如 TextView 里的 android:textColor="?attr/clockTextColor" 是让其字体颜色跟随所设置的 Theme。
2)Java代码相关实现:ide
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); initData(); initTheme(); setContentView(R.layout.activity_day_night); initView(); }
在每次setContentView以前必须调用initTheme方法,由于当 View 建立成功后 ,再去 setTheme 是没法对 View 的 UI 效果产生影响的。函数
/** * 刷新UI界面 */ private void refreshUI() { TypedValue background = new TypedValue();//背景色 TypedValue textColor = new TypedValue();//字体颜色 Resources.Theme theme = getTheme(); theme.resolveAttribute(R.attr.clockBackground, background, true); theme.resolveAttribute(R.attr.clockTextColor, textColor, true); mHeaderLayout.setBackgroundResource(background.resourceId); for (RelativeLayout layout : mLayoutList) { layout.setBackgroundResource(background.resourceId); } for (CheckBox checkBox : mCheckBoxList) { checkBox.setBackgroundResource(background.resourceId); } for (TextView textView : mTextViewList) { textView.setBackgroundResource(background.resourceId); } Resources resources = getResources(); for (TextView textView : mTextViewList) { textView.setTextColor(resources.getColor(textColor.resourceId)); } int childCount = mRecyclerView.getChildCount(); for (int childIndex = 0; childIndex < childCount; childIndex++) { ViewGroup childView = (ViewGroup) mRecyclerView.getChildAt(childIndex); childView.setBackgroundResource(background.resourceId); View infoLayout = childView.findViewById(R.id.info_layout); infoLayout.setBackgroundResource(background.resourceId); TextView nickName = (TextView) childView.findViewById(R.id.tv_nickname); nickName.setBackgroundResource(background.resourceId); nickName.setTextColor(resources.getColor(textColor.resourceId)); TextView motto = (TextView) childView.findViewById(R.id.tv_motto); motto.setBackgroundResource(background.resourceId); motto.setTextColor(resources.getColor(textColor.resourceId)); } //让 RecyclerView 缓存在 Pool 中的 Item 失效 //那么,若是是ListView,要怎么作呢?这里的思路是经过反射拿到 AbsListView 类中的 RecycleBin 对象,而后一样再用反射去调用 clear 方法 Class<RecyclerView> recyclerViewClass = RecyclerView.class; try { Field declaredField = recyclerViewClass.getDeclaredField("mRecycler"); declaredField.setAccessible(true); Method declaredMethod = Class.forName(RecyclerView.Recycler.class.getName()).getDeclaredMethod("clear", (Class<?>[]) new Class[0]); declaredMethod.setAccessible(true); declaredMethod.invoke(declaredField.get(mRecyclerView), new Object[0]); RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool(); recycledViewPool.clear(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } refreshStatusBar(); } /** * 刷新 StatusBar */ private void refreshStatusBar() { if (Build.VERSION.SDK_INT >= 21) { TypedValue typedValue = new TypedValue(); Resources.Theme theme = getTheme(); theme.resolveAttribute(R.attr.colorPrimary, typedValue, true); getWindow().setStatusBarColor(getResources().getColor(typedValue.resourceId)); } }
refreshUI函数起到模式切换的做用。经过 TypedValue 和 Theme.resolveAttribute 在代码中获取 Theme 中设置的颜色,来从新设置控件的背景色或者字体颜色等等。refreshStatusBar刷新状态栏。布局
/** * 展现一个切换动画 */ private void showAnimation() { final View decorView = getWindow().getDecorView(); Bitmap cacheBitmap = getCacheBitmapFromView(decorView); if (decorView instanceof ViewGroup && cacheBitmap != null) { final View view = new View(this); view.setBackgroundDrawable(new BitmapDrawable(getResources(), cacheBitmap)); ViewGroup.LayoutParams layoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); ((ViewGroup) decorView).addView(view, layoutParam); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f); objectAnimator.setDuration(300); objectAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); ((ViewGroup) decorView).removeView(view); } }); objectAnimator.start(); } } /** * 获取一个 View 的缓存视图 * * @param view * @return */ private Bitmap getCacheBitmapFromView(View view) { final boolean drawingCacheEnabled = true; view.setDrawingCacheEnabled(drawingCacheEnabled); view.buildDrawingCache(drawingCacheEnabled); final Bitmap drawingCache = view.getDrawingCache(); Bitmap bitmap; if (drawingCache != null) { bitmap = Bitmap.createBitmap(drawingCache); view.setDrawingCacheEnabled(false); } else { bitmap = null; } return bitmap; }
showAnimation 是用于展现一个渐隐效果的属性动画,这个属性做用在哪一个对象上呢?是一个 View ,一个在代码中动态填充到 DecorView 中的 View。
知乎之因此在夜间模式切换过程当中会有渐隐效果,是由于在切换前进行了截屏,同时将截屏拿到的 Bitmap 设置到动态填充到 DecorView 中的 View 上,并对这个 View 执行一个渐隐的属性动画,因此使得咱们可以看到一个漂亮的渐隐过渡的动画效果。并且在动画结束的时候再把这个动态添加的 View 给 remove 了,避免了 Bitmap 形成内存飙升问题。测试
resource id映射实现夜间模式
经过id获取资源时,先将其转换为夜间模式对应id,再经过Resources来获取对应的资源。字体
public static Drawable getDrawable(Context context, int id) { return context.getResources().getDrawable(getResId(id)); } public static int getResId(int defaultResId) { if (!isNightMode()) { return defaultResId; } if (sResourceMap == null) { buildResourceMap(); } int themedResId = sResourceMap.get(defaultResId); return themedResId == 0 ? defaultResId : themedResId; } private static void buildResourceMap() { sResourceMap = new SparseIntArray(); sResourceMap.put(R.drawable.common_background, R.drawable.common_background_night); // ... }
这个方案简单粗暴,麻烦的地方和第一种方案同样:每次添加资源都须要创建映射关系,刷新UI的方式也与第一种方案相似,貌似今日头条,网易新闻客户端等主流新闻阅读应用都是经过这种方式实现的夜间模式。动画
经过Android Support Library实现
1)在res目录中为夜间模式配置专门的目录,以-night为后缀ui
2)在Application中设置夜间模式
3)夜间模式切换
三种方案比较,第二种太暴力,不适合项目后期开发;第一种方法须要作配置的地方比第三种方法多。整体来讲,第三种方法最简单,相似整个app内有一个夜间模式的总开关,切换了之后就不用管了。最后采用第三种方案!
经过Android Support Library实现夜间模式虽然简单,可是当中也碰到了一些坑。现作一下记录:
一、 横屏切换的时候,夜间模式混乱
基于Android Support Library的夜间模式,至关因而support库来帮忙关键相关的资源,有时候会出现错误的状况。好比说app横竖屏切换以后!!经测试发现,每次调起一个横屏的Activity,而后退出,整个app的夜间模式就乱了,部分的UI调用的是日间模式的资源~~~
这里认为的加了一个多余的设定:
/** * 刷新UI_MODE模式 */ public void refreshResources(Activity activity) { if (Prop.isNightMode.getBoolean()) { updateConfig(activity, Configuration.UI_MODE_NIGHT_YES); } else { updateConfig(activity, Configuration.UI_MODE_NIGHT_NO); } } /** * google官方bug,暂时解决方案 * 手机切屏后从新设置UI_MODE 模式(由于在DayNight主题下,切换横屏后UI_MODE会出错,会致使 资源获取出错,须要从新设置回来) */ private void updateConfig(Activity activity, int uiNightMode) { Configuration newConfig = new Configuration(activity.getResources().getConfiguration()); newConfig.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK; newConfig.uiMode |= uiNightMode; activity.getResources().updateConfiguration(newConfig, null); }
在每次退出横屏的时候,调用这个方法,强制刷新一次config
二、 drawable xml文件中部分颜色值 日间/夜间 弄反了
Android Support Library实现的夜间模式,资源的获取碰到了一些坑。咱们常常会在drawable文件夹中定义一些xml来作背景形状、背景颜色。
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="false" android:state_enabled="true"> <shape android:shape="rectangle"> <solid android:color="@color/app_textbook_bg_color"/> <corners android:radius="5dp" /> </shape> </item> </selector>
按照Android Support Library的介绍,须要为夜间模式建立一个为night借个人目录,那么这里就能够有两种理解:
1)在values/color.xml 和values-night/color.xml分别为app_textbook_bg_color定义不一样的色值
2)在values/color.xml 和values-night/color.xml分别为app_textbook_bg_color定义不一样的色值;此外,须要分别定义drawable/bg_textbook.xml和drawable-night/bg_textbook.xml,两个文件的内容能够同样。
这里碰到了一些坑。原先采用的是第一种方法,这样代码改动少,看起来一目了然。可是,,不一样厂商的手机会有不同的表现!!部分手机,在夜间模式的时候仍是用的日间的资源;杀了app重进才会好。
个人理解是Android会对资源作缓存~ 缓存的时候会将app_textbook_color解析出来并缓存;假设日间模式app_textbook_color为#FFFFFF,咱们设置夜间模式切换,这时候不一样手机厂商的策略不同,有些厂商会把缓存清除,因此切成夜间模式的时候app_textbook_color的色值会改变,夜间模式正常;可是有些厂商应该不会清理缓存,夜间模式切换以后,拿的是日间模式缓存下的色值,也就是#FFFFFF,这样就出问题了~~
以上为我的看法,建议碰到这种状况,多在drawable下写一个xml防止个别手机出错。
三、切换夜间模式须要restartActivity,会闪一下
这也是一个比较坑的地方。夜间模式切换之后,须要从新获取一遍资源,最简单的方法是restart一下。如今我采用的就是这种简单粗暴的方法,用户体验比较不友好,后期须要参考知乎的实现,改进实现。