市面上的app大部分都有换肤功能,今天我来给大家介绍一下实现换肤功能的三种实现方案.
一. 使用系统默认的主题Theme.AppCompat.DayNight
app主题必须是Theme.AppCompat.DayNight的子类
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<!--colorPrimaryDark对应状态栏的颜色-->
<item name="colorPrimary">@color/colorPrimaryDark</item>
<!--colorPrimary 对应ToolBar的颜色-->
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<!--colorAccent 对应EditText编辑时、RadioButton选中、CheckBox等选中时的颜色。-->
<item name="colorAccent">@color/colorAccent</item>
<!--<item name="android:windowIsTranslucent">true</item>-->
<!--可以在这里设置更多的主题样式,不一定在xml布局中定义,设置最主要的主题样式-->
</style>
</resources>
在资源文件中需添加一个夜间皮肤资源的文件,文件名及里面的图片名称需保持一致
这就简单的完成了基本的配置了, 当设置为夜间模式是, app会使用nigth相关的资源文件 ,接下来就需要通过代码设置主题,主题样式分别为三种:
MODE_NIGHT_NO // 白天主题
MODE_NIGHT_YES // 夜晚主题
MODE_NIGHT_AUTO // 根据时间自动
public static void appTheme() {
SharedPreferences sharedPreferences = App.getContext().getSharedPreferences(AppTheme,0);
int appThemeMode = sharedPreferences.getInt(AppThemeMode, 0);
switch (appThemeMode) {
case AppThemeConstant.DEFALUT: {
} break;
case AppThemeConstant.NIGHT_AUTO: {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);
} break;
case AppThemeConstant.NIGHT_DAY: {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
} break;
case AppThemeConstant.NIGHT_NIGHT: {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} break;
}
}
public static int getAppTheme() {
SharedPreferences sharedPreferences = App.getContext().getSharedPreferences(AppTheme,0);
return sharedPreferences.getInt(AppThemeMode, 0);
}
public static void setAppTheme(@AppThemeConstant.Mode int mode) {
/**保存主题*/
SharedPreferences sharedPreferences = App.getContext().getSharedPreferences(AppTheme,0);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt(AppThemeMode, mode);
editor.commit();
/**刷新activity*/
appTheme();
for (Activity activity: App.getActivitys()) {
activity.recreate();
Log.e("activity", "acitivit");
}
}
注意 : 更换主图完后需要recreate().
该实现方法有个致命的缺点就是要刷新界面体验效果不好.项目依赖appcompat库,最低版本为23.2.0. activity必须是继承AppCompatActivity.使用该接口才有效.
二. 皮肤包更换主题
皮肤包的资源也就是打包一个相关资源的apk, 资源名需要与项目中的资源保存一致.
将皮肤包安装到本地后,并安装,即可通过该皮肤包的包名获取到该项目的context,在通过context和资源名获取相关的资源,否则会发现样式对不上到不到自己想要的效果,如果皮肤资源和应用中的资源完全一样(数量和种类)才能直接使用应用中的R文件应用资源.
在此我就不展示如何下载皮肤包了,跳过这一步,将皮肤包放到asset文件夹下,在拷贝到本地,然后通过隐式安装apk将,资源包下载到手机中.
/**
* 从assets目录中复制整个文件夹内容
* @param context Context 使用CopyFiles类的Activity
* @param oldPath String 原文件路径 如:/aa
* @param newPath String 复制后路径 如:xx:/bb/cc
*/
public static void copyFilesForAssets(Context context, String oldPath, String newPath) {
try {
String fileNames[] = context.getAssets().list(oldPath);//获取assets目录下的所有文件及目录名
if (fileNames.length > 0) {//如果是目录
File file = new File(newPath);
file.mkdirs();//如果文件夹不存在,则递归
for (String fileName : fileNames) {
copyFilesForAssets(context,oldPath + "/" + fileName,newPath+"/"+fileName);
}
} else {//如果是文件
InputStream is = context.getAssets().open(oldPath);
FileOutputStream fos = new FileOutputStream(new File(newPath));
byte[] buffer = new byte[1024];
int byteCount=0;
while((byteCount=is.read(buffer))!=-1) {//循环从输入流读取 buffer字节
fos.write(buffer, 0, byteCount);//将读取的输入流写入到输出流
}
fos.flush();//刷新缓冲区
is.close();
fos.close();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 静默安装App
* <p>非root需添加权限 {@code <uses-permission android:name="android.permission.INSTALL_PACKAGES" />}</p>
*
* @param filePath 文件路径
* @return {@code true}: 安装成功<br>{@code false}: 安装失败
*/
public static boolean installAppSilent(final String filePath) {
File file = FileUtils.getFileByPath(filePath);
if (!FileUtils.isFileExists(file)) {
return false;
}
String command = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm install " + filePath;
ShellUtils.CommandResult commandResult = ShellUtils.execCmd(command, !isSystemApp(), true);
return commandResult.successMsg != null && commandResult.successMsg.toLowerCase().contains("success");
}
通过上面的代码是不是发现换肤的功能实现十分简单>.< 到这里还差最后一步了那就是更换资源,话不多多说接着上代码
Context context = createPackageContext(SKIN_PACK_NAME, Context.CONTEXT_INCLUDE_CODE
| Context.CONTEXT_IGNORE_SECURITY);
//此处应该根据皮肤apk中的资源ID找到皮肤资源,而不能直接用本应用中的R
//如果皮肤资源和应用中的资源完全一样(数量和种类)才能直接使用应用中的R
int imageRes = context.getResources().getIdentifier(RES_IMAGE, "drawable", context.getPackageName());
int textColor = context.getResources().getIdentifier(RES_TEXTCOLOR, "color", context.getPackageName());
int textBackground = context.getResources().getIdentifier(RES_TEXTBACKGROUND, "color", context.getPackageName());
if(imageRes>0){
img_icon.setBackground(context.getResources().getDrawable(imageRes));
mTextView.setTextColor(context.getResources().getColor(textColor));
mTextView.setBackgroundColor(context.getResources().getColor(textBackground));
}
以上实现方法及步骤合适很通俗易懂的, 但要真正使用第二种方法更换主题资源那肯定得累死自己, 把所有资源都通过代码替换一遍. 所以在此推荐一个换肤框架集成简单, 能大大的减少工作量.
三. android-skin-loader实现换肤功能
框架地址: android-skin-loader
这个框架的换肤机制是使用动态加载的机制去加载皮肤包里面的内容,无需Acitvity重启即可实现皮肤的实时更换,皮肤包是可以与原安装包相分离的,需要自己定做(这个皮肤包其实也就是一个普通的Android项目,只是只有资源文件没有类文件而已),这样做的好处就是可以在线提供皮肤包供用户去下载,也可以大大的减少安装包的体积,同时也很好的实现了插件化。
- 在Application中进行初始化
public class SkinApplication extends Application {
public void onCreate() {
super.onCreate();
// Must call init first
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
}
}
- 在布局文件中标识需要换肤的View
xmlns:skin="http://schemas.android.com/android/skin"
<TextView
skin:enable="true"
... />
继承BaseActivity或者BaseFragmentActivity作为BaseActivity进行开发
从.skin文件中设置皮肤
String SKIN_NAME = "BlackFantacy.skin";
String SKIN_DIR = Environment.getExternalStorageDirectory() + File.separator + SKIN_NAME;
File skin = new File(SKIN_DIR);
SkinManager.getInstance().load(skin.getAbsolutePath(),
new ILoaderListener() {
@Override
public void onStart() {
}
@Override
public void onSuccess() {
}
@Override
public void onFailed() {
}
});
- 重设默认皮肤
SkinManager.getInstance().restoreDefaultTheme();
- 对代码中创建的View的换肤支持
主要由IDynamicNewView接口实现该功能,在BaseActivity,BaseFragmentActivity和BaseFragment中已经实现该接口.
public interface IDynamicNewView {
void dynamicAddView(View view, List<DynamicAttr> pDAttrs);
}
用法:动态创建View后,调用dynamicAddView方法注册该View至皮肤映射表即可(如下).详见sample工程
private void dynamicAddTitleView() { TextView textView = new
TextView(getActivity()); textView.setText(“Small Article
(动态new的View)”); RelativeLayout.LayoutParams param = new
RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
param.addRule(RelativeLayout.CENTER_IN_PARENT);
textView.setLayoutParams(param);
textView.setTextColor(getActivity().getResources().getColor(R.color.color_title_bar_text));
textView.setTextSize(20); titleBarLayout.addView(textView);
List mDynamicAttr = new ArrayList(); mDynamicAttr.add(new DynamicAttr(AttrFactory.TEXT_COLOR,
R.color.color_title_bar_text)); dynamicAddView(textView,
mDynamicAttr); }
好了,本文到此结束。很感谢你的耐心看完!希望能帮助到你.
源码传送:Skin.demo