在這裡談一下墨跡天氣的換膚實現方式,不過首先聲明我只是通過反編譯以及參考了一些網上其他資料的方式推測出的換膚原理, 在這裡只供參考. 若大家有更好的方式, 歡迎交流.
墨跡天氣下載的皮膚就是一個zip格式的壓縮包,在應用的時候把皮膚資源釋放到墨跡天氣應用的目錄下,更換皮膚時新的皮膚資源會替換掉老的皮膚資源每次載入的時候就是從手機硬碟上讀取圖片,這些圖片資源的命名和程式中的資源的命名保持一致,一旦找不到這些資源,可以選擇到系統預設中尋找。這種實現是直接讀取了外部資源檔,在程式運行時通過代碼顯示的替換介面的背景資源。這種方式的優點是:皮膚資源的格式定義很隨意可以是zip也可以是自訂的格式,只要程式中能夠解析到資源就行,缺點是效率上的問題.
這裡需要注意的一點是,再這裡對壓縮包的解壓,藉助了第三方工具: ant. jar進行解壓和壓縮檔. 關於ant工具的使用,我在稍後的文章中會具體介紹.
主要技術點:
如何去讀取zip檔案中的資源以及皮膚檔案存放方式
實現方案:如果軟體每次啟動都去讀取SD卡上的皮膚檔案,速度會比較慢。較好的做法是提供一個皮膚設定的介面,使用者選擇了哪一個皮膚,就把那個皮膚檔案解壓縮到”/data/data/[package name]/skin”路徑下(讀取的快速及安全性),這樣不需要跨儲存空間讀取,速度較快,而且不需要每次都去zip壓縮包中讀取,不依賴SD卡中的檔案,即使皮膚壓縮包檔案被刪除了也沒有關係。
實現方法:
1. 在軟體的協助或者官網的協助中提示使用者將皮膚檔案拷貝到SD卡指定路徑下。
2. 在軟體中提供皮膚設定介面。可以在菜單或者在設定中。可參考墨跡、搜狗IME、QQ等支援換膚的軟體。
3. 載入指定路徑下的皮膚檔案,讀取其中的縮圖,在皮膚設定介面中顯示,將使用者選中的皮膚檔案解壓縮到”/data/data/[package name]/skin”路徑下。
4. 軟體中優先讀取”/data/data/[package name]/skin/”路徑下的資源。如果沒有則使用apk中的資源。
:
具體代碼:
1. AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.tony.skin" android:versionCode="1" android:versionName="1.0"><uses-sdk android:minSdkVersion="7" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".Re_Skin2Activity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>
2.布局檔案main.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#d2d2d2" android:id="@+id/layout"> <Button android:text="匯入皮膚" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button><Button android:text="換膚" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button><TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="請先點擊“匯入皮膚”,會將/sdcard/skin.zip匯入到/sdcard/Skin_kris目錄下,然後點擊‘換膚’會將sdcard裡面的素材用作皮膚" android:textColor="#000"></TextView></LinearLayout>
3. Re_Skin2Activity:
package com.tony.skin;import android.app.Activity;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.drawable.BitmapDrawable;import android.os.Bundle;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.LinearLayout;import android.widget.Toast;import com.tony.skin.utils.ZipUtil;/** * * @author Tony * */public class Re_Skin2Activity extends Activity implements OnClickListener{private ButtonbtnSet;private ButtonbtnImport;private LinearLayout layout; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); btnSet = (Button)findViewById(R.id.button1); btnSet.setOnClickListener(this); btnImport = (Button)findViewById(R.id.button2); btnImport.setOnClickListener(this); layout = (LinearLayout)findViewById(R.id.layout); }@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.button1:Bitmap bitmap= BitmapFactory.decodeFile("/sdcard/Skin_kris/skin/google.png"); BitmapDrawable bd=new BitmapDrawable(bitmap);btnSet.setBackgroundDrawable(bd);layout.setBackgroundDrawable(new BitmapDrawable(BitmapFactory.decodeFile("/sdcard/Skin_kris/skin/bg/bg.png")));break;case R.id.button2:ZipUtil zipp = new ZipUtil(2049);System.out.println("begin do zip");zipp.unZip("/sdcard/skin.zip","/sdcard/Skin_kris");Toast.makeText(this, "匯入成功", Toast.LENGTH_SHORT).show();break;default:break;}}}
4. ZipUtil 解壓縮處理ZIP包的工具類
package com.tony.skin.utils;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.util.Enumeration;import java.util.zip.Deflater;import org.apache.tools.zip.ZipEntry;import org.apache.tools.zip.ZipFile;import org.apache.tools.zip.ZipOutputStream;/** * Zip包壓縮,解壓處理工具類 * @author a * */public class ZipUtil {private ZipFile zipFile; private ZipOutputStream zipOut; //壓縮Zip private int bufSize; //size of bytes private byte[] buf; private int readedBytes; public ZipUtil(){ this(512); } public ZipUtil(int bufSize){ this.bufSize = bufSize; this.buf = new byte[this.bufSize]; } /** * * @param srcFile 需要 壓縮的目錄或者檔案 * @param destFile 壓縮檔的路徑 */public void doZip(String srcFile, String destFile) {// zipDirectoryPath:需要壓縮的檔案夾名File zipDir;String dirName;zipDir = new File(srcFile);dirName = zipDir.getName();try {this.zipOut = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destFile)));//設定壓縮的注釋zipOut.setComment("comment");//設定壓縮的編碼,如果要壓縮的路徑中有中文,就用下面的編碼zipOut.setEncoding("GBK");//啟用壓縮 zipOut.setMethod(ZipOutputStream.DEFLATED); //壓縮層級為最強壓縮,但時間要花得多一點 zipOut.setLevel(Deflater.BEST_COMPRESSION); handleDir(zipDir, this.zipOut,dirName);this.zipOut.close();} catch (IOException ioe) {ioe.printStackTrace();}}/** * 由doZip調用,遞迴完成目錄檔案讀取 * @param dir * @param zipOut * @param dirName 這個主要是用來記錄壓縮檔的一個目錄階層的 * @throws IOException */private void handleDir(File dir, ZipOutputStream zipOut,String dirName) throws IOException {System.out.println("遍曆目錄:"+dir.getName());FileInputStream fileIn;File[] files;files = dir.listFiles();if (files.length == 0) {// 如果目錄為空白,則單獨建立之.// ZipEntry的isDirectory()方法中,目錄以"/"結尾.System.out.println("壓縮的 Name:"+dirName);this.zipOut.putNextEntry(new ZipEntry(dirName));this.zipOut.closeEntry();} else {// 如果目錄不為空白,則分別處理目錄和檔案.for (File fileName : files) {// System.out.println(fileName);if (fileName.isDirectory()) {handleDir(fileName, this.zipOut,dirName+File.separator+fileName.getName()+File.separator);} else {System.out.println("壓縮的 Name:"+dirName + File.separator+fileName.getName());fileIn = new FileInputStream(fileName);this.zipOut.putNextEntry(new ZipEntry(dirName + File.separator+fileName.getName()));while ((this.readedBytes = fileIn.read(this.buf)) > 0) {this.zipOut.write(this.buf, 0, this.readedBytes);}this.zipOut.closeEntry();}}}}/** * 解壓指定zip檔案 * @param unZipfile 壓縮檔的路徑 * @param destFile 解壓到的目錄 */public void unZip(String unZipfile, String destFile) {// unZipfileName需要解壓的zip檔案名稱FileOutputStream fileOut;File file;InputStream inputStream;try {this.zipFile = new ZipFile(unZipfile);for (Enumeration entries = this.zipFile.getEntries(); entries.hasMoreElements();) {ZipEntry entry = (ZipEntry) entries.nextElement();file = new File(destFile+File.separator+entry.getName());if (entry.isDirectory()) {file.mkdirs();} else {// 如果指定檔案的目錄不存在,則建立之.File parent = file.getParentFile();if (!parent.exists()) {parent.mkdirs();}inputStream = zipFile.getInputStream(entry);fileOut = new FileOutputStream(file);while ((this.readedBytes = inputStream.read(this.buf)) > 0) {fileOut.write(this.buf, 0, this.readedBytes);}fileOut.close();inputStream.close();}}this.zipFile.close();} catch (IOException ioe) {ioe.printStackTrace();}}// 設定緩衝區大小public void setBufSize(int bufSize) {this.bufSize = bufSize;}}