您的位置:首页 > 移动开发 > Android开发

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: