Android换肤功能设计与实现
2013-06-11 19:18
295 查看
MIUI系统最具特色的功能就是系统级的主题换肤,能够更换任何可见的元素。像桌面ICON、桌面文件夹、桌面壁纸、APP中的各种图片资源、字体等等。如果一个ROM想像MIUI一样,支持这种功能的话,那么这个功能是如何实现的那。从功能实现角度划分,可以分成第三方也能换的,还有只有系统能换的。这里主要是Android系统开放的各种服务,实现换肤的功能。比如壁纸,铃声这些,通过系统的相关接口,可以实现对这些功能的更换。MIUI其它的换肤功能,主要是对APP资源的更换,这个功能应该说最具特色。下面主要对这个功能的实现及主题管理APP的开发中遇到的问题,进行一下说明,会持续几篇博客。
从理论上讲,对APP更换资源(桌面也是APP),可以与特定应用定义相关接口,从而有针对性的实现对资源的更换。如桌面,现在市面上的很多桌面都支持自身的换肤功能。可以更换ICON图库、图标背板等等。如果以这种方式实现换肤功能的话,那么换肤模块就要与各个APP定义相关的接口,将对应的资源放于约定好的位置,通过Intent广播方式通知各个APP,使各个APP重新加载。但这样做势必增加各个APP的复杂程度,同时不能更新第三方APP的资源。
如果需要做到对ROM中任何APP都可以进行换肤,那么就需要深入到Android系统对资源的加载、与使用部分去寻找答案了。
简单将Android系统提取资源的相关类关系如上图所示,在我们所使用的Activity的getResource调用的是Context这个虚基类的接口,而如果想对Context的具体实现有一个深入的了解,那么可以去看ContextImpl。简单介绍一下Android系统对资源的提取方式,Android中,对资源的加载,通过Context获取Resources,最后调用的是AssetManager的openNonAsset()加载资源。Android系统通过资源ID来标识不同的资源,ID大于0x01000000为系统资源,否则为app自带资源。通过包名来确定不同的资源包,读取资源文件。
Android系统中所有的资源都是以文件夹压缩包的形式存在的,这就是Android的APP在编译是所做的事,我们知道在Android的APP中,所有的应用代码被编译为dex格式的文件,那res资源那,其实所有的资源文件只是简单地使用zip文件压缩到APK应用包中。在Android手机进行安装时,
其实过程就是 1:解压APK应用包。2.解析文件夹下的xml文件,向系统注册包信息。主要包括启动Activity文件名,应用包名。3.将APK包拷贝到/data/app文件夹,以包名命名。在/data/app下面的就是各个应用所对应的资源包了,其实就是apk包的拷贝。在资源加载过程中,通过应用包名确定资源包的位置。直接到/data/app下面查找相应的资源包。提取对应的资源文件。
特殊的几个资源包:
1.系统资源包,在/system/framework/framework-res
2.系统应用资源包,其实系统应用与普通应用一样,都是通过包名来确定资源包的位置的,所不同的是系统应用是在系统编译时直接编译为Android.mk中指定的包名,并拷贝到/system/app下面的,不存在上面所说的安装应用、解析的部分。其资源包就是/system/app下面的APK本体。包名就是在编译时,在Android.mk中指定的名称。
以上对Android系统资源加载的过程进行了简单地描述,按着这条思路走,基本可以实现对资源选择加载及替换了。
由于本篇涉及到比较底层的东西,部分属于产品级核心,不能详述请见谅,接下来的几篇,主要会说一下主题管理APP的开发,会进行详细介绍。
整体来说,换肤功能的上层APP的主要功能如下:
1.访问网络获取主题列表。
2.下载主题包。
3.在本地管理主题包。
4.应用主题包,触发换肤功能。
下面会重点描述该APP的设计与技术难点,主要以Android4.0系统作为实现目标平台,使用相应SDK。使用MVC典型分层设计,对APP进行大体划分。对于该APP首先需要确定与后台的交互协议,即使是大体上的交互协议。分别对应上述各功能,简单的需求分析后,得到如下简单实现方案。
1.访问网络主题列表,通过主题类型,获取主题缩略图,根据皮肤包编号获取皮肤详细预览图。
2.下载主题包,根据主题URL,使用DownloadManager下载主题包。
3.在本地管理主题包。在下载完后,在本地进行解压,存放在指定目录,并插入对应数据库,提供应用、删除等基本操作。
4.应用主题包,触发换肤功能。应用主题包,需要触发相关的系统换肤模块。
根据上述实现方案,绘制概要设计对应UML图,如下:
根据实现方案,抽象出各类。底层主要抽象:
1.主题数据。2.数据库实现
Control
1.ZIP压缩、解压操作 2.文件(夹)拷贝、删除操作。
3.网络数据访问 4. 与界面的相关交互。
View
数据展示界面。
这一节详细介绍一下Model层的设计,本身并无太多难点,采用标准的Provider结构访问底层数据库。简单UML图如下:
通过ThemeProvider统一访问数据库具体实现ThemeDBHelper。通过向ThemeProvider添加相应的Observer来监听数据库的变化。这里属于标准的Provider操作、及sqlite操作,由于不涉及到多个应用数据共享的操作,只是使用Provider接管、简化对数据库的访问操作,所以实现相对简单,唯一需要注意的是由于需要Observer来监听数据库的变化,所以在Provider的相关操作后,需要通过sendNotification来通知相关监听器。
[java] view
plaincopy
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class ThemeProvider extends ContentProvider {
private final static String TAG = ThemeProvider.class.toString();
public static final String AUTHORITY = "com.tencent.themedatabase";// 对外提供服务接口名
public static final String NOTIFICATION = "notify";// 是否通知提示
private static ThemeDBHelper mThemeDBHelper;
private Context mContext;
// public ThemeProvider(Context context) {
// mContext = context;
// mThemeDBHelper = new ThemeDBHelper(context);
//
// }
private long checkandInsert(SQLiteDatabase db, ContentValues cv,
String table, String nullColumnHack) {
if (cv.get(ThemeDBHelper._ID) == null) {
throw new RuntimeException("Error : the Insert value has no id");
}
return db.insert(table, nullColumnHack, cv);
}
private void sendNotify(Uri uri) {
String notify = uri.getQueryParameter(NOTIFICATION);
if (notify == null || notify.equals("true")) {
mContext.getContentResolver().notifyChange(uri, null);
}
}
public static long generateNewId() {
return mThemeDBHelper.generateNewId();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
/**/
SqlArguments sqlargs = new SqlArguments(uri, selection, selectionArgs);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
int count = db.delete(sqlargs.table, sqlargs.where, sqlargs.selection);
if (count > 0) {
sendNotify(uri);
}
return 0;
}
@Override
public String getType(Uri uri) {
SqlArguments sqlarg = new SqlArguments(uri);
if (TextUtils.isEmpty(sqlarg.where)) {
return "vnd.android.cursor.dir/" + sqlarg.table;
} else {
return "vnd.android.cursor.item/" + sqlarg.table;
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SqlArguments sqlargs = new SqlArguments(uri);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
long id = checkandInsert(db, values, sqlargs.table, null);
if (id < 0) {
return null;
}
uri = ContentUris.withAppendedId(uri, id);
sendNotify(uri);
return uri;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
SqlArguments sqlargs = new SqlArguments(uri);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
db.beginTransaction();
try {
for(int i=0; i<values.length;i++){
if(checkandInsert(db, values[i],sqlargs.table, null) <0){
return 0;
}
}
db.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
}
finally{
db.endTransaction();
}
sendNotify(uri);
return values.length;
}
@Override
public boolean onCreate() {
mContext = this.getContext();
mThemeDBHelper = new ThemeDBHelper(mContext);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SqlArguments sqlargs = new SqlArguments(uri, selection, selectionArgs);
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(sqlargs.table);
SQLiteDatabase db = mThemeDBHelper.getReadableDatabase();
Cursor cr = qb.query(db, projection, sqlargs.where, sqlargs.selection,
null, null, sortOrder);
cr.setNotificationUri(mContext.getContentResolver(), uri);
return cr;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SqlArguments sqlargs = new SqlArguments(uri, selection, selectionArgs);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
int count = db.update(sqlargs.table, values, sqlargs.where,
sqlargs.selection);
Log.e(TAG,"GET WHERE "+sqlargs.where);
if (count > 0) {
sendNotify(uri);
}
return count;
}
static class SqlArguments {
/**
* 处理sql语句,解析各个参数
* */
public final String table;
public final String where;
public final String[] selection;
public SqlArguments(Uri uri, String where, String[] selection) {
int argscount = uri.getPathSegments().size();
if (argscount == 1) {
// uri://host/table
this.table = uri.getPathSegments().get(0);
Log.e("pluszhang","parse uri "+this.table);
this.where = where;
this.selection = selection;
} else if (argscount != 2) {
// uri://host/table/id/...
throw new IllegalArgumentException("Invalidate URI " + uri);
} else if (!TextUtils.isEmpty(where)) {
// uri://host/talbe/id,where应该为空,已经指明id
throw new UnsupportedOperationException(
"WHERE opt not support " + uri);
} else {
// uri://host/table/id
this.table = uri.getPathSegments().get(0);
this.where = "_id=" + uri.getPathSegments().get(1);
this.selection = null;
}
}
public SqlArguments(Uri uri) {
if (uri.getPathSegments().size() == 1) {
this.table = uri.getPathSegments().get(0);
this.where = null;
this.selection = null;
} else {
throw new IllegalArgumentException("Invalidate URI " + uri);
}
}
}
}
/article/2808033.html
从理论上讲,对APP更换资源(桌面也是APP),可以与特定应用定义相关接口,从而有针对性的实现对资源的更换。如桌面,现在市面上的很多桌面都支持自身的换肤功能。可以更换ICON图库、图标背板等等。如果以这种方式实现换肤功能的话,那么换肤模块就要与各个APP定义相关的接口,将对应的资源放于约定好的位置,通过Intent广播方式通知各个APP,使各个APP重新加载。但这样做势必增加各个APP的复杂程度,同时不能更新第三方APP的资源。
如果需要做到对ROM中任何APP都可以进行换肤,那么就需要深入到Android系统对资源的加载、与使用部分去寻找答案了。
简单将Android系统提取资源的相关类关系如上图所示,在我们所使用的Activity的getResource调用的是Context这个虚基类的接口,而如果想对Context的具体实现有一个深入的了解,那么可以去看ContextImpl。简单介绍一下Android系统对资源的提取方式,Android中,对资源的加载,通过Context获取Resources,最后调用的是AssetManager的openNonAsset()加载资源。Android系统通过资源ID来标识不同的资源,ID大于0x01000000为系统资源,否则为app自带资源。通过包名来确定不同的资源包,读取资源文件。
Android系统中所有的资源都是以文件夹压缩包的形式存在的,这就是Android的APP在编译是所做的事,我们知道在Android的APP中,所有的应用代码被编译为dex格式的文件,那res资源那,其实所有的资源文件只是简单地使用zip文件压缩到APK应用包中。在Android手机进行安装时,
其实过程就是 1:解压APK应用包。2.解析文件夹下的xml文件,向系统注册包信息。主要包括启动Activity文件名,应用包名。3.将APK包拷贝到/data/app文件夹,以包名命名。在/data/app下面的就是各个应用所对应的资源包了,其实就是apk包的拷贝。在资源加载过程中,通过应用包名确定资源包的位置。直接到/data/app下面查找相应的资源包。提取对应的资源文件。
特殊的几个资源包:
1.系统资源包,在/system/framework/framework-res
2.系统应用资源包,其实系统应用与普通应用一样,都是通过包名来确定资源包的位置的,所不同的是系统应用是在系统编译时直接编译为Android.mk中指定的包名,并拷贝到/system/app下面的,不存在上面所说的安装应用、解析的部分。其资源包就是/system/app下面的APK本体。包名就是在编译时,在Android.mk中指定的名称。
以上对Android系统资源加载的过程进行了简单地描述,按着这条思路走,基本可以实现对资源选择加载及替换了。
由于本篇涉及到比较底层的东西,部分属于产品级核心,不能详述请见谅,接下来的几篇,主要会说一下主题管理APP的开发,会进行详细介绍。
整体来说,换肤功能的上层APP的主要功能如下:
1.访问网络获取主题列表。
2.下载主题包。
3.在本地管理主题包。
4.应用主题包,触发换肤功能。
下面会重点描述该APP的设计与技术难点,主要以Android4.0系统作为实现目标平台,使用相应SDK。使用MVC典型分层设计,对APP进行大体划分。对于该APP首先需要确定与后台的交互协议,即使是大体上的交互协议。分别对应上述各功能,简单的需求分析后,得到如下简单实现方案。
1.访问网络主题列表,通过主题类型,获取主题缩略图,根据皮肤包编号获取皮肤详细预览图。
2.下载主题包,根据主题URL,使用DownloadManager下载主题包。
3.在本地管理主题包。在下载完后,在本地进行解压,存放在指定目录,并插入对应数据库,提供应用、删除等基本操作。
4.应用主题包,触发换肤功能。应用主题包,需要触发相关的系统换肤模块。
根据上述实现方案,绘制概要设计对应UML图,如下:
根据实现方案,抽象出各类。底层主要抽象:
1.主题数据。2.数据库实现
Control
1.ZIP压缩、解压操作 2.文件(夹)拷贝、删除操作。
3.网络数据访问 4. 与界面的相关交互。
View
数据展示界面。
这一节详细介绍一下Model层的设计,本身并无太多难点,采用标准的Provider结构访问底层数据库。简单UML图如下:
通过ThemeProvider统一访问数据库具体实现ThemeDBHelper。通过向ThemeProvider添加相应的Observer来监听数据库的变化。这里属于标准的Provider操作、及sqlite操作,由于不涉及到多个应用数据共享的操作,只是使用Provider接管、简化对数据库的访问操作,所以实现相对简单,唯一需要注意的是由于需要Observer来监听数据库的变化,所以在Provider的相关操作后,需要通过sendNotification来通知相关监听器。
[java] view
plaincopy
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class ThemeProvider extends ContentProvider {
private final static String TAG = ThemeProvider.class.toString();
public static final String AUTHORITY = "com.tencent.themedatabase";// 对外提供服务接口名
public static final String NOTIFICATION = "notify";// 是否通知提示
private static ThemeDBHelper mThemeDBHelper;
private Context mContext;
// public ThemeProvider(Context context) {
// mContext = context;
// mThemeDBHelper = new ThemeDBHelper(context);
//
// }
private long checkandInsert(SQLiteDatabase db, ContentValues cv,
String table, String nullColumnHack) {
if (cv.get(ThemeDBHelper._ID) == null) {
throw new RuntimeException("Error : the Insert value has no id");
}
return db.insert(table, nullColumnHack, cv);
}
private void sendNotify(Uri uri) {
String notify = uri.getQueryParameter(NOTIFICATION);
if (notify == null || notify.equals("true")) {
mContext.getContentResolver().notifyChange(uri, null);
}
}
public static long generateNewId() {
return mThemeDBHelper.generateNewId();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
/**/
SqlArguments sqlargs = new SqlArguments(uri, selection, selectionArgs);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
int count = db.delete(sqlargs.table, sqlargs.where, sqlargs.selection);
if (count > 0) {
sendNotify(uri);
}
return 0;
}
@Override
public String getType(Uri uri) {
SqlArguments sqlarg = new SqlArguments(uri);
if (TextUtils.isEmpty(sqlarg.where)) {
return "vnd.android.cursor.dir/" + sqlarg.table;
} else {
return "vnd.android.cursor.item/" + sqlarg.table;
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SqlArguments sqlargs = new SqlArguments(uri);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
long id = checkandInsert(db, values, sqlargs.table, null);
if (id < 0) {
return null;
}
uri = ContentUris.withAppendedId(uri, id);
sendNotify(uri);
return uri;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
SqlArguments sqlargs = new SqlArguments(uri);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
db.beginTransaction();
try {
for(int i=0; i<values.length;i++){
if(checkandInsert(db, values[i],sqlargs.table, null) <0){
return 0;
}
}
db.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
}
finally{
db.endTransaction();
}
sendNotify(uri);
return values.length;
}
@Override
public boolean onCreate() {
mContext = this.getContext();
mThemeDBHelper = new ThemeDBHelper(mContext);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SqlArguments sqlargs = new SqlArguments(uri, selection, selectionArgs);
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(sqlargs.table);
SQLiteDatabase db = mThemeDBHelper.getReadableDatabase();
Cursor cr = qb.query(db, projection, sqlargs.where, sqlargs.selection,
null, null, sortOrder);
cr.setNotificationUri(mContext.getContentResolver(), uri);
return cr;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SqlArguments sqlargs = new SqlArguments(uri, selection, selectionArgs);
SQLiteDatabase db = mThemeDBHelper.getWritableDatabase();
int count = db.update(sqlargs.table, values, sqlargs.where,
sqlargs.selection);
Log.e(TAG,"GET WHERE "+sqlargs.where);
if (count > 0) {
sendNotify(uri);
}
return count;
}
static class SqlArguments {
/**
* 处理sql语句,解析各个参数
* */
public final String table;
public final String where;
public final String[] selection;
public SqlArguments(Uri uri, String where, String[] selection) {
int argscount = uri.getPathSegments().size();
if (argscount == 1) {
// uri://host/table
this.table = uri.getPathSegments().get(0);
Log.e("pluszhang","parse uri "+this.table);
this.where = where;
this.selection = selection;
} else if (argscount != 2) {
// uri://host/table/id/...
throw new IllegalArgumentException("Invalidate URI " + uri);
} else if (!TextUtils.isEmpty(where)) {
// uri://host/talbe/id,where应该为空,已经指明id
throw new UnsupportedOperationException(
"WHERE opt not support " + uri);
} else {
// uri://host/table/id
this.table = uri.getPathSegments().get(0);
this.where = "_id=" + uri.getPathSegments().get(1);
this.selection = null;
}
}
public SqlArguments(Uri uri) {
if (uri.getPathSegments().size() == 1) {
this.table = uri.getPathSegments().get(0);
this.where = null;
this.selection = null;
} else {
throw new IllegalArgumentException("Invalidate URI " + uri);
}
}
}
}
/article/2808033.html
相关文章推荐
- Android换肤功能实现与换肤框架QSkinLoader使用方式介绍
- android换肤功能实现
- Android实现换肤功能(二)
- Android应用如何实现换肤功能
- android应用开发-从设计到实现 1-2 功能的确定
- 分析Android App中内置换肤功能的实现方式
- Android学习之 换肤功能模块的实现<二>
- Android应用如何实现换肤功能
- Android实现换肤功能(二)
- #Android笔记#基于popupwindow的底部菜单栏设计与功能实现
- Android应用如何实现换肤功能
- android应用换肤功能的实现
- Android换肤功能实现(白天、黑夜)
- Android 打造自己的个性化应用(二):应用程序内置资源实现换肤功能
- Android应用如何实现换肤功能
- Android 换肤功能实现
- Android 打造自己的个性化应用(二):应用程序内置资源实现换肤功能
- Android日志收集上报功能设计与实现(总)
- Android一键换肤功能:一种简单的实现
- Android日志收集上报功能设计与实现(总)