之前接到“夜间模式”的需求,因为页面多,所以就打算先简单做。
找的资料基本上都是 recreate() 的形式:先在 SharedPreferences 里面记录一个标识,然后通过 recreate() 重新初始化 activity,在 onCreate() 方法里根据标识通过 setTheme() 去修改主题 Theme(该方法在 setContentView() 之前调用)。
在 attrs.xml 里面声明一套属性(可以是 color,也可以是 reference,前者是颜色,后者是 drawable),其中 styles.xml 中定义了 日间、夜间 两个主题(如果主题文件区分系统版本,那么每个版本下的 styles.xml 都得设置),两个主题都分别对 attrs.xml 里声明的属性进行定义,然后布局 XML 中就通过 ?attr/xxxx 去设置颜色或 drawable,这样切换主题后就能直接切换对应模式下的颜色或 drawable。
下面简单的放下代码:
attrs.xml:
<resources> <attr name="color_text" format="color" /> <attr name="drawable_icon" format="reference" /> </resources> styles.xml: <style name="AppTheme" parent="Theme.AppCompat.Light"> <!-- toolbar(actionbar)颜色 --> <item name="colorPrimary">@color/colorPrimary</item> <!-- 状态栏颜色 --> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <!-- 窗口的背景颜色 --> <item name="android:windowBackground">@color/colorPrimary</item> <item name="color_text">@color/sun_text_color</item> <item name="drawable_icon">@drawable/ic_chevron_right_white_24dp</item> </style> <style name="AppThemeDark" parent="Theme.AppCompat"> <!-- toolbar(actionbar)颜色 --> <item name="colorPrimary">@color/darkcolorPrimary</item> <!-- 状态栏颜色 --> <item name="colorPrimaryDark">@color/darkcolorPrimaryDark</item> <!-- 窗口的背景颜色 --> <item name="android:windowBackground">@color/darkcolorPrimary</item> <item name="color_text">@color/night_text_color</item> <item name="drawable_icon">@drawable/ic_chevron_right_black_24dp</item> </style> 然后布局 xml 文件里面直接用: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/iv_about" android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="?attr/drawable_icon" /> <TextView android:id="@+id/tv_bottom" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Copyright" android:textColor="?attr/color_text" android:textSize="12sp" /> </LinearLayout>到这里应该很多人会问为什么还要说这个,这个方案不是跟标题不符合么?其实这个方案后面还是需要用到的,所以就先贴出来。
下面就来说说标题方案的大概实现思路:
其实就是主动更换 APP 主要首页上各模块的颜色,然后在 SharedPreferences 里面记录一个标识,从首页跳转到另一个 activity 时则在初始化的时候根据标识去设置主题(就是上面贴出的方案),然后一开始进入 APP 时也通过这个标识去设置主题。
主要的工作还是在 APP 首页上,要去改状态栏颜色、列表这些也要处理,甚至一些第三方的控件也要修改颜色(比如底栏什么的,有时候一些库直接改颜色也没用,只能再次初始化,然后恢复到切换时的选中状态...)。
首先我们得考虑切换入口,有两种情况:
1、切换入口在 APP 主要首页上(比如侧滑菜单中),那么这种只需要考虑首页的颜色切换。
2、切换入口在 其它页面上(比如在跳转的设置页面里面),那么这种先得切换当前页面的颜色,然后返回首页的时候在 onResume() 方法里面要进行颜色切换(要实时记录日夜间模式的状态,如果当前已经是对应模式就无需要切换)。
下面我们处理下 SharedPreferences 以及日夜间颜色、drawable 取用工具类:
先自定义 Application,提供一个 context 取用接口:
public class MyApplication extends Application { private static Context context; @Override public void onCreate() { super.onCreate(); context = getApplicationContext(); } public static Context getContext(){ return context; } } 在 AndroidManifest.xml 里面设置 Application: <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..."> <application ... android:name="xxx.MyApplication"> ... </application> </manifest> 接着是处理 SharedPreferences 数据的工具类: public class SharedPreUtil { private SharedPreUtil() {} private static SharedPreUtil sharedpreutil; public static SharedPreUtil getInstance() { if (sharedpreutil == null) { synchronized (SharedPreUtil.class) { sharedpreutil = new SharedPreUtil(); } } return sharedpreutil; } final String DARKMODEL = "darkmodel"; boolean darkmodel; private SharedPreferences getSharedPreferences() { return MyApplication.getContext().getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE); } public void load() { SharedPreferences prefer = getSharedPreferences(); darkmodel = prefer.getBoolean(DARKMODEL, false); } public void save() { SharedPreferences prefer = getSharedPreferences(); SharedPreferences.Editor editor = prefer.edit(); editor.putBoolean(DARKMODEL, darkmodel); } public void clear() { SharedPreferences prefer = getSharedPreferences(); SharedPreferences.Editor editor = prefer.edit(); editor.remove(DARKMODEL); } }这里我们设置了一个夜间模式标识,对外提供了加载、保存、清除的接口。
然后是日夜间颜色、drawable 取用工具类 ThemeResUtil :
public class ThemeResUtil { static boolean darkModel = false; public static void setModel(boolean isdarkmodel) { darkModel = isdarkmodel; } public static boolean isDarkModel() { return darkModel; } public static int getColorPrimary() { if(darkModel) return MyApplication.getContext().getResources().getColor(R.color.darkcolorPrimary); else return MyApplication.getContext().getResources().getColor(R.color.colorPrimary); } public static int getColorPrimaryDark() { if(darkModel) return MyApplication.getContext().getResources().getColor(R.color.darkcolorPrimaryDark); else return MyApplication.getContext().getResources().getColor(R.color.colorPrimaryDark); } public static int getText_color() { if(darkModel) return MyApplication.getContext().getResources().getColor(R.color.night_text_color); else return MyApplication.getContext().getResources().getColor(R.color.sun_text_color); } public static int getIcon_res() { if(darkModel) return R.drawable.ic_chevron_right_black_24dp; else return R.drawable.ic_chevron_right_white_24dp; } }切换主题、第一次进入 APP 首页的时候要对 darkmodel 进行设置。
这里只举例了几个,原理就是根据不同模式去取出对应的颜色或 drawable,并且认真看会发现取的颜色是 colors.xml 里面的颜色,这样我们改颜色就只需要在 colors.xml 里面进行统一修改,有了这个工具类我们就可以直接通过方法来取颜色而不需要再写一大堆 if else 。
接下来在 APP 首页以及从首页跳转的其它 activity 的 onCreate() 方法里面对主题进行处理,保证第一次进入首页时恢复上次手动切换的模式以及从首页跳转的 activity 的模式跟首页一致:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MyApplication.getSharedPreUtil.load(); if(MyApplication.getSharedPreUtil.darkmodel) { setTheme(R.style.AppThemeDark); ThemeResUtil.setModel(true);// APP首页才需要这句,其它跳转activity不需要再次设置 } else { setTheme(R.style.AppTheme); ThemeResUtil.setModel(false);// APP首页才需要这句,其它跳转activity不需要再次设置 } setContentView(...); ... }接下来我们在 APP 首页 activity 里面加入个日夜间模式切换方法:
public void changeTheme(boolean darkmodel)
先处理一些基本的:
1、状态栏相关切换
首先是状态栏的颜色,对应主题里面的 <item name="colorPrimaryDark"></item>。
在 Android 5.0(21)起,可以通过 getWindow().setStatusBarColor(ThemeResUtil.getColorPrimaryDark()); 直接设置,但是5.0以下就无效,所以我们得用到一个第三方库:
SystemBarTint 这个库在4.4下可以设置沉浸式(需要打开 TranslucentStatus)。
下面上简单的代码:
SystemBarTintManager mTintManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SharedPreUtil.getInstance().load(); if(SharedPreUtil.getInstance().darkmodel) { setTheme(R.style.AppThemeDark); ThemeResUtil.setModel(true); // APP首页才需要这句,其它跳转activity不需要再次设置 } else { setTheme(R.style.AppTheme); ThemeResUtil.setModel(false); // APP首页才需要这句,其它跳转activity不需要再次设置 } mTintManager = new SystemBarTintManager(this); //这里根据自己的需要对 mTintManager 进行开关处理。 //通过 mTintManager.setStatusBarTintEnabled(boolean enabled); 进行设置 setContentView(...); ... } public void changeTheme(boolean darkmodel) { ThemeResUtil.setModel(darkmodel); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mTintManager.setStatusBarTintEnabled(false); getWindow().setStatusBarColor(ThemeResUtil.getColorPrimaryDark()); } else { mTintManager.setStatusBarTintEnabled(true); mTintManager.setTintColor(ThemeResUtil.getColorPrimaryDark()); } }其次是 Android 6.0 起的高亮状态栏模式(开启时状态栏图标会变成灰色),下面给出打开和关闭的代码,有需要的可以进行额外设置:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { int vis = getWindow().getDecorView().getSystemUiVisibility(); if(darkmodel) vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; else vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; getWindow().getDecorView().setSystemUiVisibility(vis); }darkmodel 为 true 时关闭高亮模式,恢复白色图标。
2、默认背景颜色,对应主题里面的 <item name="android:windowBackground"></item>。
在开发中为了减少 GPU 过度绘制的问题我们布局最底层的 layout 一般不会设置颜色,而是通过主题中的 android:windowBackground 去设置,如果你设置了这个,并且切换日夜间模式的时候要改其颜色,那么我们就要在代码里面设置了:
getWindow().getDecorView().setBackgroundColor(ThemeResUtil.getColorPrimary());
接下来到各个控件了,需要改颜色或 drawable 的控件都要给个 id ,然后切换模式的时候直接修改背景颜色或颜色或 drawable:
public void changeTheme(boolean darkmodel) { ThemeResUtil.setModel(darkmodel); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mTintManager.setStatusBarTintEnabled(false); getWindow().setStatusBarColor(ThemeResUtil.getColorPrimaryDark()); } else { mTintManager.setStatusBarTintEnabled(true); mTintManager.setTintColor(ThemeResUtil.getColorPrimaryDark()); } getWindow().getDecorView().setBackgroundColor(ThemeResUtil.getColorPrimary()); changeComponent(); } public void changeComponent() { tv_title.setTextColor(ThemeResUtil.getText_color()); ... }(注意如果你的主页有很多层,比如有 fragment,viewpager 等,那么就要分别给这些控件单个页面里面的控件进行主动修改颜色或 drawable)
然后到列表,下面以 recyclerview 进行说明。
实现原理很简单,就是在给 item 设置数据的时候先对 item 里面的控件进行颜色或 drawable 修改(通过 ThemeResUtil 取),然后直接对 recyclerview 的 adapter(设为全局)调用 notifyDataSetChanged,注意列表数据在整个过程不能改变,调用 notifyDataSetChanged 只要数据不变当前滑动进度都会在原来位置。
下面是部分代码:
public class ListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @Override public int getItemCount() { return data.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { View view = View.inflate(viewGroup.getContext(), R.layout.tem, null); ItemHolder holder = new ItemHolder(view, context); return holder; } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if(viewHolder instanceof ItemHolder) { ((ItemHolder)viewHolder).setData(data.get(position)); } } } public class ItemHolder extends RecyclerView.ViewHolder{ TextView tv_title; public ItemHolder(View itemView, Context context) { super(itemView); this.context = context; tv_title = (TextView)itemView.findViewById(R.id.tv_title); } public void checkTheme() { tv_title.setTextColor(ThemeResUtil.getText_color()); } public void setData(...) { checkTheme(); ... } ... }有人可能会问为什么不在 holder 里面声明个变量去记录,然后判断它跟 ThemeResUtil 里面的 darkmodel 不同时才主动去修改颜色或 drawable,其实一开始我是这样做的,但有时候会出现列表同时存在两种模式的 item ,所以后来为了避免这个就强制去修改了(设置颜色对内存开销不会太大,可以忽略)。
然后把 adapter.notifyDataSetChanged(); 放到 changeTheme() 里面:
public void changeTheme(boolean darkmodel) { ThemeResUtil.setModel(darkmodel); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mTintManager.setStatusBarTintEnabled(false); getWindow().setStatusBarColor(ThemeResUtil.getColorPrimaryDark()); } else { mTintManager.setStatusBarTintEnabled(true); mTintManager.setTintColor(ThemeResUtil.getColorPrimaryDark()); } getWindow().getDecorView().setBackgroundColor(ThemeResUtil.getColorPrimary()); changeComponent(); adapter.notifyDataSetChanged(); }当然 listview 、viewpager的原理也差不多。
接下来就是第三方库的修改,这个就不说了,一般都有修改颜色的方法的,只不过有一些可能需要重新初始化,记得一点是重新初始化的话要记得恢复之前的状态(比如选中状态等)。
最后在切换模式时要记录状态,这样跳转到其它 activity 时就能根据这个标识去设置对应主题,然后直接或间接调用 changeTheme():
public void changeModel(boolean darkmodel) { SharedPreUtil.getInstance().load(); SharedPreUtil.getInstance().darkmodel = darkmodel; SharedPreUtil.getInstance().save(); changeTheme(darkmodel); }至于第二种入口的情况,也请参照第一种入口进行设置,只不过你设置切换模式的那个页面得先变换颜色,然后打上标记,再在 APP 首页的 activity 中的 onResume() 方法中去处理,下面给出 APP 首页大概思路的代码:
boolean currentDarkModel = false; //当前是否为夜间模式 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SharedPreUtil.getInstance().load(); if(SharedPreUtil.getInstance().darkmodel) { setTheme(R.style.AppThemeDark); ThemeResUtil.setModel(true); // APP首页才需要这句,其它跳转activity不需要再次设置 } else { setTheme(R.style.AppTheme); ThemeResUtil.setModel(false); // APP首页才需要这句,其它跳转activity不需要再次设置 } currentDarkModel = SharedPreUtil.getInstance().darkmodel; mTintManager = new SystemBarTintManager(this); //这里根据自己的需要对 mTintManager 进行开关处理。 //通过 mTintManager.setStatusBarTintEnabled(boolean enabled); 进行设置 setContentView(...); ... } @Override protected void onResume() { super.onResume(); handleThemeOnResume(); } public void handleThemeOnResume() { SharedPreUtil.getInstance().load(); if(SharedPreUtil.getInstance().darkmodel && !currentDarkModel) { currentDarkModel = true; changeTheme(true); } else if(!SharedPreUtil.getInstance().darkmodel && currentDarkModel){ currentDarkModel = false; changeTheme(false); } }思路大概就是这样,当然如果你的 APP 首页很复杂,层次很多,那么你得一层一层去写接口,然后 APP 首页的 activity 通过一个方法去调用所有层次的接口方法,这样整个首页进行日夜间切换就不需要通过 recreate() 去进行重新初始化,并且保留原来的状态。