【android 换肤模式总结】将相本无种,男儿当自强。这篇文章主要讲述android 换肤模式总结相关的知识,希望能为你提供帮助。
由于android的设置中并没有夜间模式的选项,对于喜欢睡前玩手机的用户,只能简单的调节手机屏幕亮度来改善体验。目前越来越多的应用开始把夜间模式加到自家应用中,没准不久google也会把这项功能添加到Android系统中吧。
业内关于夜间模式的实现,有两种主流方案,各有其利弊,我较为推崇第三种方案:
1、通过切换theme来实现夜间模式。
2、通过修改uiMode来切换夜间模式。
3、通过插件方式切换夜间模式。
值得一提的是,上面提到的几种方案,都是资源内嵌在Apk中的方案,像新浪微博那种需要通过下载方式实现的夜间模式方案,网上有很多介绍,这里不去讨论。
下面简要描述下几种方案的实现原理:
1、通过切换theme来实现夜间模式。
首先在attrs.xml中,为需要随theme变化的内容定义属性
<
?xml
version="1.0"
encoding="utf-8"?>
<
resources>
<
attr
name="colorValue"
format="color"
/>
<
attr
name="floatValue"
format="float"
/>
<
attr
name="integerValue"
format="integer"
/>
<
attr
name="booleanValue"
format="boolean"
/>
<
attr
name="dimensionValue"
format="dimension"
/>
<
attr
name="stringValue"
format="string"
/>
<
attr
name="referenceValue"
format="color|reference"
/>
<
attr
name="imageValue"
format="reference"/>
<
attr
name="curVisibility">
<
enum
name="show"
value="https://www.songbingjia.com/android/0"
/>
<
!--
Not
displayed,
but
taken
into
account
during
layout
(space
is
left
for
it).
-->
<
enum
name="inshow"
value="https://www.songbingjia.com/android/1"
/>
<
!--
Completely
hidden,
as
if
the
view
had
not
been
added.
-->
<
enum
name="hide"
value="https://www.songbingjia.com/android/2"
/>
<
/attr>
<
/resources>
从上面的xml文件的内容可以看到,attr里可以定义各种属性类型,如color、float、integer、boolean、dimension(sp、dp/dip、px、pt...)、reference(指向本地资源),还有curVisibility是枚举属性,对应view的invisibility、visibility、gone。
其次在不同的theme中,对属性设置不同的值,在styles.xml中定义theme如下
<
style
name="DayTheme"
parent="Theme.Sherlock.Light">
>
<
item
name="colorValue">
@color/title<
/item>
<
item
name="floatValue">
0.35<
/item>
<
item
name="integerValue">
33<
/item>
<
item
name="booleanValue">
true<
/item>
<
item
name="dimensionValue">
16dp<
/item>
<
!--
如果string类型不是填的引用而是直接放一个字符串,在布局文件中使用正常,但代码里获取的就有问题
-->
<
item
name="stringValue">
@string/action_settings<
/item>
<
item
name="referenceValue">
@drawable/bg<
/item>
<
item
name="imageValue">
@drawable/launcher_icon<
/item>
<
item
name="curVisibility">
show<
/item>
<
/style>
<
style
name="NightTheme"
parent="Theme.Sherlock.Light">
<
item
name="colorValue">
@color/night_title<
/item>
<
item
name="floatValue">
1.44<
/item>
<
item
name="integerValue">
55<
/item>
<
item
name="booleanValue">
false<
/item>
<
item
name="dimensionValue">
18sp<
/item>
<
item
name="stringValue">
@string/night_action_settings<
/item>
<
item
name="referenceValue">
@drawable/night_bg<
/item>
<
item
name="imageValue">
@drawable/night_launcher_icon<
/item>
<
item
name="curVisibility">
hide<
/item>
<
/style>
在布局文件中使用对应的值,通过?attr/属性名,来获取不同theme对应的值。
?xml
version="1.0"
encoding="utf-8"?>
<
LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/referenceValue"
android:orientation="vertical"
>
<
TextView
android:id="@+id/setting_Color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textColor="?attr/colorValue"
/>
<
CheckBox
android:id="@+id/setting_show_answer_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="?attr/booleanValue"/>
<
TextView
android:id="@+id/setting_Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="?attr/dimensionValue"
android:text="@string/text_title"
android:textColor="?attr/colorValue"
/>
<
TextView
android:id="@+id/setting_Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="?attr/stringValue"
/>
<
ImageView
android:id="@+id/setting_Image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="http://img.readke.com/220413/14134J023-0.jpg"
/>
<
View
android:id="@+id/setting_line"
android:layout_width="match_parent"
android:layout_height="1dp"
android:visibility="?attr/curVisibility"
/>
<
/LinearLayout>
在Activity中调用如下changeTheme方法,其中isNightMode为一个全局变量用来标记当前是否为夜间模式,在设置完theme后,还需要调用restartActivity或者setContentView重新刷新UI。
@Override
protected
void
onCreate(Bundle
savedInstanceState)
{
super.onCreate(savedInstanceState);
if(AppThemeManager.isLightMode()){
this.setTheme(R.style.NightTheme);
}else{
this.setTheme(R.style.DayTheme);
}
setContentView(R.layout.setting);
}
到此即完成了一个夜间模式的简单实现,包括Google自家在内的很多应用都是采用此种方式实现夜间模式的,这应该也是Android官方推荐的方式。
但这种方式有一些不足,规模较大的应用,需要随theme变化的属性会很多,都需要逐一定义,有点麻烦,另外一个缺点是要使得新theme生效,一般需要restartActivity来切换UI,会导致切换主题时界面闪烁。
不过也可以通过调用自定义的updateTheme方法,重启Activity即可
public
static
void
updateTheme(Activity
activity,isNight)
{
AppThemeManager.setNightMode(isNight);
activity.recreate();
/
*
*
activity.finish();
*
Intent
intent=new
Intent();
*
intent.setClass(context,
MainActivity.class);
*
context.startActivity(intent);
*/
}
当然,潜在的问题也是存在的,比如,我们动态获取资源Resource,那么遇到这种情况的解决办法是自定义资源获取规则,并且在资源名称上下功夫
public
static
Drawable
getDrawable(Context
context,String
resName,boolean
isForce)
{
int
resId;
if(AppThemeManager.isLightMode()
&
&
isForce)
//这里使用isForce参数主要是为了一些主题切换时共用的图片被匹配
{
//约定,黑夜图片带_night
resId
=
context.getResources().getIdentifier(resName+"_night",
"drawable",
context.getPackageName());
}else{
resId
=
context.getResources().getIdentifier(resName,
"drawable",
context.getPackageName());
}
return
context.getResources().getDrawable(resId);
}public
static
Drawable
getDrawable(Context
context,int
resid,boolean
isForce)
{
String
resName
=
context.getResources().getResourceEntryName(resid);
if(AppThemeManager.isLightMode()
&
&
isForce)
{
resName
=
resName+"_night";
}
int
resId
=
context.getResources().getIdentifier(resName,
"drawable",
context.getPackageName());
return
context.getResources().getDrawable(resId);
}//当然,获取string,dimens等资源也是这种方式,这里就不再论述
优点:可以匹配多套主题,并不局限于黑白模式
缺点:需要大量定义主题
2、通过修改uiMode来切换夜间模式。
修改uimode是修改Configuration,这种主题切换只限于黑白模式,没有其他模式,核心代码如下
Configuration
newConfig
=
new
Configuration(activity.getResources().getConfiguration());
newConfig.uiMode
&
=
~Configuration.UI_MODE_NIGHT_MASK;
newConfig.uiMode
|=
uiNightMode;
activity.getResources().updateConfiguration(newConfig,
null);
activity.recreate();
但这种切换的前提是,我们的资源目录必须具备切换-night后缀,类似国际化语言的切换,如:
values-night/
drawable-night/
drawable-night-xxdpi/
.....
下面来一个开源的Helper
package
com.example.androidtestcase;
import
android.app.Activity;
import
android.content.SharedPreferences;
import
android.content.res.Configuration;
import
android.preference.PreferenceManager;
import
java.lang.ref.WeakReference;
public
class
NightModeHelper
{
private
static
final
String
PREF_KEY
=
"nightModeState";
private
static
int
sUiNightMode
=
Configuration.UI_MODE_NIGHT_UNDEFINED;
private
WeakReference<
Activity>
mActivity;
private
SharedPreferences
mPrefs;
public
NightModeHelper(Activity
activity)
{
int
currentMode
=
(activity.getResources().getConfiguration()
.uiMode
&
Configuration.UI_MODE_NIGHT_MASK);
mPrefs
=
PreferenceManager.getDefaultSharedPreferences(activity);
init(activity,
-1,
mPrefs.getInt(PREF_KEY,
currentMode));
}
public
NightModeHelper(Activity
activity,
int
theme)
{
int
currentMode
=
(activity.getResources().getConfiguration()
.uiMode
&
Configuration.UI_MODE_NIGHT_MASK);
mPrefs
=
PreferenceManager.getDefaultSharedPreferences(activity);
init(activity,
theme,
mPrefs.getInt(PREF_KEY,
currentMode));
}
public
NightModeHelper(Activity
activity,
int
theme,
int
defaultUiMode)
{
init(activity,
theme,
defaultUiMode);
}
private
void
init(Activity
activity,
int
theme,
int
defaultUiMode)
{
mActivity
=
new
WeakReference<
Activity>
(activity);
if
(sUiNightMode
==
Configuration.UI_MODE_NIGHT_UNDEFINED)
{
sUiNightMode
=
defaultUiMode;
}
updateConfig(sUiNightMode);
if
(theme
!=
-1)
{
activity.setTheme(theme);
}
}
private
void
updateConfig(int
uiNightMode)
{
Activity
activity
=
mActivity.get();
if
(activity
==
null)
{
throw
new
IllegalStateException("Activity
went
away?");
}
Configuration
newConfig
=
new
Configuration(activity.getResources().getConfiguration());
newConfig.uiMode
&
=
~Configuration.UI_MODE_NIGHT_MASK;
newConfig.uiMode
|=
uiNightMode;
activity.getResources().updateConfiguration(newConfig,
null);
sUiNightMode
=
uiNightMode;
if
(mPrefs
!=
null)
{
mPrefs.edit()
.putInt(PREF_KEY,
sUiNightMode)
.apply();
}
}
public
static
int
getUiNightMode()
{
return
sUiNightMode;
}
public
void
toggle()
{
if
(sUiNightMode
==
Configuration.UI_MODE_NIGHT_YES)
{
notNight();
}
else
{
night();
}
}
public
void
notNight()
{
updateConfig(Configuration.UI_MODE_NIGHT_NO);
System.gc();
System.runFinalization();
System.gc();
mActivity.get().recreate();
}
public
void
night()
{
updateConfig(Configuration.UI_MODE_NIGHT_YES);
System.gc();
System.runFinalization();
//
added
in
https://github.com/android/platform_frameworks_base/commit/6f3a38f3afd79ed6dddcef5c83cb442d6749e2ff
System.gc();
mActivity.get().recreate();
}
}
当然,Android也为这种过于冗杂的模式提供了UIModeManager,优点是我们再也不需要使用Perference手动保存并管理一些信息了。
UiModeManager
umm
=
(UiModeManager
)context.getSystemService(Context.UI_MODE_SERVICE);
umm.getNightMode(UI_MODE_NIGHT_YES);
对于第二种方案,优缺点如下:
优点:
/res/xxx-night形式避免了切换中需要手动管理资源的问题,避免了代码手动管理夜间模式配置
缺点:
只能局限于2种主题。
3、通过插件方式切换夜间模式。
插件换肤具体请参考如下博客:
Android更换皮肤解决方案
参考 http://www.2cto.com/kf/201501/366859.html
本项目是以插件化开发思想进行的,主要工作和代码如下
资源文件,这里以color资源为例
1、首先我们需要准备一个皮肤包,这个皮肤包里面不会包含任何Activity,里面只有资源文件,这里我为了简单,仅仅加入一个color.xml(其实就相当于Android系统中的framework_res.apk)
<
!--?xml
version="1.0"
encoding="utf-8"?-->
<
resources>
<
color
name="main_btn_color">
#E61ABD<
/color>
<
color
name="main_background">
#38F709<
/color>
<
color
name="second_btn_color">
#000000<
/color>
<
color
name="second_background">
#FFFFFF<
/color>
<
/resources>
2、将该资源打包成apk文件,放入sd卡中(实际项目你可以从我网络下载)
3、将需要换肤的Activity实现ISkinUpdate(这个可以自己随便定义名称)接口
public
class
MainActivity
extends
Activity
implements
ISkinUpdate,OnClickListener
{
private
Button
btn_main;
private
View
main_view;
@Override
protected
void
onCreate(Bundle
savedInstanceState)
{
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_main);
SkinApplication.getInstance().mActivitys.add(this);
btn_main=(Button)this.findViewById(R.id.btn_main);
btn_main.setOnClickListener(this);
main_view=this.findViewById(R.id.main_view);
}
@Override
protected
void
onResume()
{
super.onResume();
if(SkinPackageManager.getInstance(this).mResources!=null)
{
updateTheme();
Log.d("yzy",
"onResume-->
updateTheme");
}
}
@Override
public
boolean
onCreateOptionsMenu(Menu
menu)
{
//
Inflate
the
menu;
this
adds
items
to
the
action
bar
if
it
is
present.
getMenuInflater().inflate(R.menu.main,
menu);
return
true;
}
@Override
public
boolean
onOptionsItemSelected(MenuItem
item)
{
int
id
=
item.getItemId();
if
(id
==
R.id.action_settings)
{
//Toast.makeText(this,
"change
skin",
1000).show();
File
dir=new
File(Environment.getExternalStorageDirectory(),"plugins");
File
skin=new
File(dir,"SkinPlugin.apk");
if(skin.exists())
{
SkinPackageManager.getInstance(MainActivity.this).loadSkinAsync(skin.getAbsolutePath(),
new
loadSkinCallBack()
{
@Override
public
void
startloadSkin()
{
Log.d("yzy",
"startloadSkin");
}
@Override
public
void
loadSkinSuccess()
{
Log.d("yzy",
"loadSkinSuccess");
MainActivity.this.sendBroadcast(new
Intent(SkinBroadCastReceiver.SKIN_ACTION));
}
@Override
public
void
loadSkinFail()
{
Log.d("yzy",
"loadSkinFail");
}
});
}
return
true;
}
return
super.onOptionsItemSelected(item);
}
@Override
public
void
updateTheme()
{
//
TODO
Auto-generated
method
stub
if(btn_main!=null)
{
try
{
Resources
mResource=SkinPackageManager.getInstance(this).mResources;
Log.d("yzy",
"start
and
mResource
is
null-->
"+(mResource==null));
int
id1=mResource.getIdentifier("main_btn_color",
"color",
"com.skin.plugin");
btn_main.setBackgroundColor(mResource.getColor(id1));
int
id2=mResource.getIdentifier("main_background",
"color","com.skin.plugin");
main_view.setBackgroundColor(mResource.getColor(id2));
//img_skin.setImageDrawable(mResource.getDrawable(mResource.getIdentifier("skin",
"drawable","com.skin.plugin")));
}
catch
(Exception
e)
{
//
TODO
Auto-generated
catch
block
e.printStackTrace();
}
}
}
@Override
protected
void
onDestroy()
{
//
TODO
Auto-generated
method
stub
SkinApplication.getInstance().mActivitys.remove(this);
super.onDestroy();
}
@Override
public
void
onClick(View
v)
{
//
TODO
Auto-generated
method
stub
if(v.getId()==R.id.btn_main)
{
Intent
intent=new
Intent(this,SecondActivity.class);
this.startActivity(intent);
}
}
}
这段代码里面主要看onOptionsItemSelected,这个方法里面,通过资源apk路径,拿到该资源apk对应Resources对象。我们直接看看SkinPacakgeManager里面做了什么吧
/**
*
解析皮肤资源包
*
com.skin.demo.SkinPackageManager
*
@author
yuanzeyao
<
br>
*
create
at
2015年1月3日
下午3:24:16
*/
public
class
SkinPackageManager
{
private
static
SkinPackageManager
mInstance;
private
Context
mContext;
/**
*
当前资源包名
*/
public
String
mPackageName;
/**
*
皮肤资源
*/
public
Resources
mResources;
private
SkinPackageManager(Context
mContext)
{
this.mContext=mContext;
}
public
static
SkinPackageManager
getInstance(Context
mContext)
{
if(mInstance==null)
{
mInstance=new
SkinPackageManager(mContext);
}
return
mInstance;
}
/**
*
异步加载皮肤资源
*
@param
dexPath
*
需要加载的皮肤资源
*
@param
callback
*
回调接口
*/
public
void
loadSkinAsync(String
dexPath,final
loadSkinCallBack
callback)
{
new
AsyncTask<
string,void,resources>
()
{
protected
void
onPreExecute()
{
if(callback!=null)
{
callback.startloadSkin();
}
};
@Override
protected
Resources
doInBackground(String...
params)
{
try
{
if(params.length==1)
{
String
dexPath_tmp=params[0];
PackageManager
mPm=mContext.getPackageManager();
PackageInfo
mInfo=mPm.getPackageArchiveInfo(dexPath_tmp,PackageManager.GET_ACTIVITIES);
mPackageName=mInfo.packageName;
AssetManager
assetManager
=
AssetManager.class.newInstance();
Method
addAssetPath
=
assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager,
dexPath_tmp);
Resources
superRes
=
mContext.getResources();
Resources
skinResource=new
Resources(assetManager,
superRes.getDisplayMetrics(),
superRes.getConfiguration());
SkinConfig.getInstance(mContext).setSkinResourcePath(dexPath_tmp);
return
skinResource;
}
return
null;
}
catch
(Exception
e)
{
return
null;
}
};
protected
void
onPostExecute(Resources
result)
{
mResources=result;
if(callback!=null)
{
if(mResources!=null)
{
callback.loadSkinSuccess();
}else
{
callback.loadSkinFail();
}
}
};
}.execute(dexPath);
}
/**
*
加载资源的回调接口
*
com.skin.demo.loadSkinCallBack
*
@author
yuanzeyao
<
br>
*
create
at
2015年1月4日
下午1:45:48
*/
public
static
interface
loadSkinCallBack
{
public
void
startloadSkin();
public
void
loadSkinSuccess();
public
void
loadSkinFail();
}
}
调用loadSkinAsync后,如果成功,就会发送一个换肤广播,并将当前皮肤apk的路径保存到sp中,便于下次启动app是直接加载该皮肤资源。接受换肤广播是在SkinApplication中注册的,当接收到此广播后,随即调用所有已经启动,并且需要换肤的Activity的updateTheme方法,从而实现换肤。
public
class
SkinApplication
extends
Application
{
private
static
SkinApplication
mInstance=null;
public
ArrayList<
iskinupdate>
mActivitys=new
ArrayList<
iskinupdate>
();
@Override
public
void
onCreate()
{
//
TODO
Auto-generated
method
stub
super.onCreate();
mInstance=this;
String
skinPath=SkinConfig.getInstance(this).getSkinResourcePath();
if(!TextUtils.isEmpty(skinPath))
{
//如果已经换皮肤,那么第二次进来时,需要加载该皮肤
SkinPackageManager.getInstance(this).loadSkinAsync(skinPath,
null);
}
SkinBroadCastReceiver.registerBroadCastReceiver(this);
}
public
static
SkinApplication
getInstance()
{
return
mInstance;
}
@Override
public
void
onTerminate()
{
//
TODO
Auto-generated
method
stub
SkinBroadCastReceiver.unregisterBroadCastReceiver(this);
super.onTerminate();
}
public
void
changeSkin()
{
for(ISkinUpdate
skin:mActivitys)
{
skin.updateTheme();
}
}
}
推荐阅读
- Android主题换肤 无缝切换
- android保存照片到相册的一些事
- Android 蓝牙开发之搜索配对连接通信大全
- 第24章OnLongClickListener长按事件(从零开始学Android)
- android app开发
- Androidhttp请求加密机制详解
- 安卓开发错误:The type android.support.v4.app.TaskStackBuilder$SupportParentable cannot be resolved.
- word2007中页眉页脚7个运用妙招_Word专区
- Word2007:页码样式的自制_Word专区