分析Android App中內(nèi)置換膚功能的實(shí)現(xiàn)方式
Android平臺api沒有特意為換膚提供一套簡便的機(jī)制,這可能是外國的軟件更注重功能和易用,不流行換膚。系統(tǒng)不提供直接支持,只能自行研究。
換膚,可以認(rèn)為是動(dòng)態(tài)替換資源(文字、顏色、字體大小、圖片、布局文件……)。這個(gè)使用編程語言來動(dòng)態(tài)設(shè)置是可以做到的,例如使用View的setBackgroundResource、setTextSize、setTextColor等函數(shù)。但我們不可能在每個(gè)activity里對頁面里的所有控件都通過調(diào)用這些函數(shù)來換膚,這樣的程序代碼難以維護(hù)、擴(kuò)展,也違背了UI和代碼分離的原則(android開發(fā)中UI以xml文件的方式布局)。 通常,皮膚資源會(huì)在主程序apk之外提供,以減少主程序的大小,以及方便隨時(shí)提供新的皮膚擴(kuò)展。
簡單的來說,軟件皮膚包括圖標(biāo)、字體、布局、交互風(fēng)格等,換膚就是換掉皮膚包括的部分或所有資源。
主流應(yīng)用程序換膚方式:
國內(nèi)有很多的軟件都支持皮膚定制,這也是與國外軟件重大不同之一,國外用戶注重社交、郵件等功能,國內(nèi)用戶則重視音樂、小說、皮膚等功能. 在寫這個(gè)換膚系列之前,我也參考了其他人的一些總結(jié). 知識點(diǎn)比較散,因此我對其進(jìn)行了整理和進(jìn)一步的優(yōu)化和擴(kuò)展. 當(dāng)然,由于精力有限,部分換膚方式我也只寫了主體功能的實(shí)現(xiàn). 如果出現(xiàn)有誤或者不夠詳細(xì)的地方,希望大家提出意見或者自行進(jìn)行擴(kuò)展.
關(guān)于其中提到的幾種主流實(shí)現(xiàn)方式,接下來的文章里我會(huì)具體提供代碼進(jìn)行解釋, 此次先做一個(gè)整體的概述.
目前主流的換膚從功能上可以劃分幾種實(shí)現(xiàn)方式,
1) 軟件內(nèi)置多個(gè)皮膚,不可由用戶增加或修改:
最低的自由度,軟件實(shí)現(xiàn)相對于后面的幾種相對簡單.
如果你的程序和資源都很小,可以在主程序apk中放入足夠的皮膚資源.
典型應(yīng)用:平板電腦Apad上QQ空間的換膚功能,實(shí)際上只是改變了Activity的背景,或這部分的資源.
2) 官方提供皮膚供下載,用戶可以使用下載的皮膚:
用戶可選擇下載自己喜歡的皮膚,有些玩家會(huì)破解皮膚的定制方法,自己做皮膚使用,或者傳到網(wǎng)上給大家用。
典型應(yīng)用: 墨跡天氣下載的皮膚就是一個(gè)zip格式的壓縮包,在應(yīng)用的時(shí)候把皮膚資源釋放到墨跡天氣應(yīng)用的目錄下,更換皮膚時(shí)新的皮膚資源會(huì)替換掉老的皮膚資源每次加載的時(shí)候就是從手機(jī)硬盤上讀取圖片,這些圖片資源的命名和程序中的資源的命名保持一致,一旦找不到這些資源,可以選擇到系統(tǒng)默認(rèn)中查找。
這種實(shí)現(xiàn)是直接讀取了外部資源文件,在程序運(yùn)行時(shí)通過代碼顯示的替換界面的背景資源。
這種方式的優(yōu)點(diǎn)是:皮膚資源的格式定義很隨意可以是zip也可以是自定義的格式,只要程序中能夠解析到資源就行,缺點(diǎn)是需要讀取并解析文件,導(dǎo)致效率上會(huì)比較差.
3) 皮膚資源存在于主程序之類的APK中,即實(shí)現(xiàn)了APK的拆分. 這里類似于瀏覽器與插件的關(guān)系. 當(dāng)考慮應(yīng)用程序需要擴(kuò)展時(shí),則需要采用本方式實(shí)現(xiàn).,這不僅僅體現(xiàn)在換膚的APK中.
典型應(yīng)用: 手機(jī)QQ換膚的實(shí)現(xiàn)方式
Q的皮膚是一個(gè)無界面APK應(yīng)用,這個(gè)皮膚應(yīng)用中的資源和主程序的資源命名一致,通過主程序和皮膚程序共享進(jìn)程實(shí)現(xiàn)主程序?qū)ζつw程序中資源的訪問,在程序運(yùn)行時(shí)通過代碼顯示指定皮膚資源,缺點(diǎn)是在主程序中每個(gè)activity要增加復(fù)雜的使用哪種皮膚邏輯,優(yōu)點(diǎn)是效率比較快,且使應(yīng)用程序具有了良好的擴(kuò)展性,降低了程序的耦合性. 包括其他類似的擴(kuò)展功能,都可以利用此方式實(shí)現(xiàn).
4) 官方提供皮膚制作工具或方法,用戶可自制皮膚:
大致分為兩種情況:
a. 應(yīng)用程序主列表為一個(gè)GridView,用戶可通過在設(shè)置中選擇背景的顏色和按鈕的風(fēng)格. 直接進(jìn)行替換即可.
b. 另外一種是有可視化帶向?qū)У墓ぞ?。用戶只要自己找一些圖片、修改文字的字體替換就可以了。用戶可以上傳自制的皮膚,提供其他用戶下載. 一般都是打包為.zip格式的,擴(kuò)展名可由公司需求自定義. 例如墨跡天氣皮膚擴(kuò)展名是mja,搜狗輸入法的皮膚擴(kuò)展名是sga,它們的文件格式實(shí)際上都是zip。之后的應(yīng)用就和第二種換膚方式類似了.
這種方式優(yōu)點(diǎn)是:使用戶有參與感,自由度較高。用戶可根據(jù)自己的喜好定制軟件的皮膚。
5) 改寫SDK的Resource類:
此處提供一些思路:
在Android系統(tǒng)中,資源主要指圖片和MP3類型的文件,也是用戶UI包含的所有元素。谷歌在設(shè)計(jì)Android系統(tǒng)時(shí),將UI界面和邏輯代碼分開組建:界面設(shè)計(jì)通過XML的形式描述,具體的程序和應(yīng)用邏輯則通過代碼來實(shí)現(xiàn);前端工程師只負(fù)責(zé)HTML和CSS的設(shè)計(jì)與架構(gòu),后端工程師則專門考慮JSP和Java的代碼執(zhí)行.
資源訪問在Android性能架構(gòu)中處于何種地位?在進(jìn)行Android開發(fā)時(shí),開發(fā)者經(jīng)常用到Framework提供的資源包Framework.jar與Framework-res.apk,以及與核心資源相關(guān)的組件“Resource Manager”文件系統(tǒng)。
APK本身是一個(gè)簡單的文件格式,也是一個(gè)壓縮文件包。通過解壓文件包可以釋放APK文件:首先需要APK的原數(shù)據(jù)Meta INF、Manifest以及RES目錄。一部分包含圖片資源的應(yīng)用,在資源釋放時(shí)也會(huì)用到Layout。
在安裝文件時(shí),系統(tǒng)會(huì)將文件取出、解壓后放在Dalvik Cache中。該緩存下有許多dex文件,當(dāng)用戶打開應(yīng)用時(shí)系統(tǒng)會(huì)自動(dòng)加載相應(yīng)的類。在加載過程中,系統(tǒng)如需訪問APK,則需對其進(jìn)行解壓,這樣通常導(dǎo)致效率較為低下。而如果將dex文件放入Dalvik Cache中,則能夠令加載的效率大大提升。
每個(gè)進(jìn)程都有一份關(guān)于Framework的共享類和共享資源,但物理內(nèi)存空間中的系統(tǒng)級別資源只有一份。Framework類和資源是只讀的,而Android操作系統(tǒng)設(shè)計(jì)之初并沒有硬盤的虛擬內(nèi)存和換進(jìn)換出機(jī)制,所以節(jié)省內(nèi)存空間是非常重要的工作。
應(yīng)用程序內(nèi)置資源實(shí)現(xiàn)換膚功能
通過應(yīng)用程序內(nèi)置資源實(shí)現(xiàn)換膚,典型的應(yīng)用為QQ空間中換膚的實(shí)現(xiàn). 應(yīng)用場景為: 應(yīng)用一般不大,且頁面較少,風(fēng)格相對簡單,一般只用實(shí)現(xiàn)部分資源或者只用實(shí)現(xiàn)背景的更換.
此種換膚方式實(shí)現(xiàn)的思路:
1. 把幾套皮膚放在res/drawable目錄里,然后用SharedPreferences來記錄當(dāng)前皮膚的資源id.然后在程序啟動(dòng)時(shí)加載Activity背景。
2. 主要的實(shí)現(xiàn)在皮膚管理器SkinManager類中. 將皮膚資源的ID加入集合中. 由該類同一調(diào)度皮膚更換,如初始化皮膚,獲取當(dāng)前皮膚符號以及具體的對應(yīng)資源的更換皮膚.
接下來看一下效果圖:


內(nèi)置皮膚的實(shí)現(xiàn)相對比較簡單,下面直接上代碼:
AndroidMainfest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tony.skindemo"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:label="@string/app_name"
android:name="com.tony.skindemo.SkinDemoActivity" >
<intent-filter >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
布局文件:
main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:textColor="#ff00ff" android:text="程序皮膚更換" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <RadioGroup android:id="@+id/skin_options" android:layout_width="fill_parent" android:layout_height="wrap_content" > <RadioButton android:layout_weight="1" android:id="@+id/radioButton1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="風(fēng)格1" /> <RadioButton android:layout_weight="1" android:id="@+id/radioButton2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="風(fēng)格2" /> <RadioButton android:layout_weight="1" android:id="@+id/radioButton3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="風(fēng)格3" /> <RadioButton android:layout_weight="1" android:id="@+id/radioButton4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="風(fēng)格4" /> <RadioButton android:layout_weight="1" android:id="@+id/radioButton5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="風(fēng)格5" /> </RadioGroup> </LinearLayout>
程序主Activity
package com.tony.skindemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.Window;
import android.view.WindowManager;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RadioGroup.OnCheckedChangeListener;
public class SkinDemoActivity extends Activity {
private SkinSettingManager mSettingManager;
private RadioButton radioButton1;
private RadioButton radioButton2;
private RadioButton radioButton3;
private RadioButton radioButton4;
private RadioButton radioButton5;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 取消標(biāo)題欄
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 完成窗體的全屏顯示 // 取消掉狀態(tài)欄
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.main);
// 初始化皮膚
mSettingManager = new SkinSettingManager(this);
mSettingManager.initSkins();
//通過單選按鈕設(shè)置皮膚(可自定義更換的方式,如導(dǎo)航欄,也可以加上預(yù)覽功能,此處不再實(shí)現(xiàn))
radioButton1 = (RadioButton) findViewById(R.id.radioButton1);
radioButton2 = (RadioButton) findViewById(R.id.radioButton2);
radioButton3 = (RadioButton) findViewById(R.id.radioButton3);
radioButton4 = (RadioButton) findViewById(R.id.radioButton4);
radioButton5 = (RadioButton) findViewById(R.id.radioButton5);
RadioGroup radioGroup = (RadioGroup) findViewById(R.id.skin_options);
radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.radioButton1:
mSettingManager.changeSkin(1);
break;
case R.id.radioButton2:
mSettingManager.changeSkin(2);
break;
case R.id.radioButton3:
mSettingManager.changeSkin(3);
break;
case R.id.radioButton4:
mSettingManager.changeSkin(4);
break;
case R.id.radioButton5:
mSettingManager.changeSkin(5);
break;
default:
break;
}
}
});
}
// 這里為了簡單實(shí)現(xiàn),實(shí)現(xiàn)換膚
public boolean onTouchEvent(MotionEvent event) {
mSettingManager.toggleSkins();
return super.onTouchEvent(event);
}
}
皮膚管理器:
import android.app.Activity;
import android.content.SharedPreferences;
/**
* 皮膚管理器
* @author tony
*
*/
public class SkinSettingManager {
public final static String SKIN_PREF = "skinSetting";
public SharedPreferences skinSettingPreference;
private int[] skinResources = { R.drawable.default_wallpaper,
R.drawable.wallpaper_c,R.drawable.wallpaper_d,R.drawable.wallpaper_f,
R.drawable.wallpaper_g
};
private Activity mActivity;
public SkinSettingManager(Activity activity) {
this.mActivity = activity;
skinSettingPreference = mActivity.getSharedPreferences(SKIN_PREF, 3);
}
/**
* 獲取當(dāng)前程序的皮膚序號
*
* @return
*/
public int getSkinType() {
String key = "skin_type";
return skinSettingPreference.getInt(key, 0);
}
/**
* 把皮膚序號寫到全局設(shè)置里去
*
* @param j
*/
public void setSkinType(int j) {
SharedPreferences.Editor editor = skinSettingPreference.edit();
String key = "skin_type";
editor.putInt(key, j);
editor.commit();
}
/**
* 獲取當(dāng)前皮膚的背景圖資源id
*
* @return
*/
public int getCurrentSkinRes() {
int skinLen = skinResources.length;
int getSkinLen = getSkinType();
if(getSkinLen >= skinLen){
getSkinLen = 0;
}
return skinResources[getSkinLen];
}
public void toggleSkins(){
int skinType = getSkinType();
if(skinType == skinResources.length - 1){
skinType = 0;
}else{
skinType ++;
}
setSkinType(skinType);
mActivity.getWindow().setBackgroundDrawable(null);
try {
mActivity.getWindow().setBackgroundDrawableResource(getCurrentSkinRes());
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 用于初始化皮膚
*/
public void initSkins(){
mActivity.getWindow().setBackgroundDrawableResource(getCurrentSkinRes());
}
/**
* 隨即切換一個(gè)背景皮膚
*/
public void changeSkin(int id) {
setSkinType(id);
mActivity.getWindow().setBackgroundDrawable(null);
try {
mActivity.getWindow().setBackgroundDrawableResource(getCurrentSkinRes());
} catch (Throwable e) {
e.printStackTrace();
}
}
}
就這樣,通過程序內(nèi)置皮膚的基本功能完成了.若想在自己的應(yīng)用中實(shí)現(xiàn),仍需注意以下幾點(diǎn)(實(shí)現(xiàn)起來并不復(fù)雜,此處不再寫具體實(shí)現(xiàn)):
1. 實(shí)現(xiàn)多個(gè)activity的更換皮膚. 需要利用自定義MyApplication類,繼承自Application. 并加入activity的集合屬性.用于存儲應(yīng)用所有的activity。修改SkinManager,在更換皮膚時(shí),從application中取出該集合,進(jìn)行遍歷并更換皮膚。
2.可以優(yōu)化用戶體驗(yàn),通過導(dǎo)航欄方式進(jìn)入更換皮膚界面,并可以加入預(yù)覽功能,當(dāng)確定修改配置后,才完成更換皮膚功能.
3. 加入style.theme等資源,實(shí)現(xiàn)更加復(fù)雜的皮膚更換. 具體實(shí)現(xiàn)同更換背景.
- Android實(shí)現(xiàn)apk插件方式換膚的實(shí)例講解
- Android開發(fā)實(shí)現(xiàn)切換主題及換膚功能示例
- android使用SkinManager實(shí)現(xiàn)換膚功能的示例
- Android 換膚技術(shù)資料整理
- android換膚功能 如何動(dòng)態(tài)獲取控件中背景圖片的資源id?
- Android應(yīng)用開發(fā)中實(shí)現(xiàn)apk皮膚文件換膚的思路分析
- Android編程實(shí)現(xiàn)換膚功能實(shí)例
- Android實(shí)現(xiàn)換膚的兩種思路分析
- 基于Android-Skin-Loader實(shí)現(xiàn)換膚效果
相關(guān)文章
ToolBar中menu無法同時(shí)顯示圖標(biāo)和文字問題的解決方法
這篇文章主要為大家詳細(xì)介紹了ToolBar中menu無法同時(shí)顯示圖標(biāo)和文字問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09
Android中SurfaceView和view畫出觸摸軌跡
這篇文章主要介紹了Android中SurfaceView和view畫出觸摸軌跡的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-03-03
Android應(yīng)用借助LinearLayout實(shí)現(xiàn)垂直水平居中布局
這篇文章主要介紹了Android應(yīng)用借助LinearLayout實(shí)現(xiàn)垂直水平居中布局的方法,文中列舉了LinearLayout線性布局下居中相關(guān)的幾個(gè)重要參數(shù),需要的朋友可以參考下2016-04-04
Android系統(tǒng)中使用shareuserid獲取系統(tǒng)權(quán)限的教程
這篇文章主要介紹了Android系統(tǒng)中使用shareuserid獲取系統(tǒng)權(quán)限的教程,這樣以來不同的apk就可以互相訪問對應(yīng)的app文件夾,需要的朋友可以參考下2016-04-04
Android連接MySQL數(shù)據(jù)庫實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Android連接MySQL數(shù)據(jù)庫實(shí)現(xiàn)方法,在Android應(yīng)用程序中連接MySQL數(shù)據(jù)庫可以幫助開發(fā)人員實(shí)現(xiàn)更豐富的數(shù)據(jù)管理功能,而且在Android中操作數(shù)據(jù)庫真的太智能了,需要的朋友可以參考下2024-02-02
Android開發(fā)中編寫藍(lán)牙相關(guān)功能的核心代碼講解
這篇文章主要介紹了Android開發(fā)中編寫藍(lán)牙功能的核心部分講解,包括掃描和配對以及修改藍(lán)牙設(shè)備可見性等操作,需要的朋友可以參考下2016-02-02
Android Build Variants 為項(xiàng)目設(shè)置變種版本的方法
下面小編就為大家分享一篇Android Build Variants 為項(xiàng)目設(shè)置變種版本的方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01

