背景知識(shí)介紹
與其他平臺(tái)的應(yīng)用程序一樣,Android中的應(yīng)用程序也會(huì)使用各種資源,比如圖片,字串等,會(huì)把它們放入源碼的相應(yīng)文件夾下面,如/res/drawable, /res/xml, /res/values/, /res/raw, /res/layout和/assets。Android也支持并鼓勵(lì)開發(fā)者把UI相關(guān)的布局和元素,用XML資源來實(shí)現(xiàn)。總結(jié)起來,Android中支持的資源有:
•顏色值 /res/values 以resources為Root的XML文件,定義形式為<color name>value</color>
•字串 /res/values 以resources為Root的XML文件<string name>value</string>
•圖片 /res/drawable 直接放入,支持9 Patch可自由拉伸
•圖片的顏色 /res/values 以resources為Root的XML文件,定義形式為<drawable name>value</drawable>
•單位資源 /res/values 以resources為Root的XML文件<dimen name>value</dimen>
•菜單 /res/menu 以menuo為root的XML文件
•布局 /res/layout 這個(gè)就是GUI的布局和元素
•風(fēng)格和主題 /res/values 以resources為Root的XML文件<style name>value</style>
•動(dòng)畫 /res/anim 有二種:一個(gè)是幀動(dòng)畫(frame animation),也就是連續(xù)變換圖片以animation-list為Root的XML文件;另外一種就是補(bǔ)間動(dòng)畫(tweened animation),它對(duì)應(yīng)于API中的Animation和AnimationSet,有translate、scale、rotate、alpha四種,以set為root來定義,這個(gè)set就相當(dāng)于AnimationSet
再說下目錄:
•/res/anim 用于存放動(dòng)畫
•/res/drawable 存放圖片,或等同于圖片的資源如shape,或selector
•/res/menu 存放Menu
•/res/values 存放修飾性資源:字串,顏色,單位,風(fēng)格和主題
•/res/layout 存放UI布局和元素
•/res/raw 存放運(yùn)行時(shí)想使用的原始文件
•/assets 存放運(yùn)行時(shí)想使用的原始文件
除了原始文件目錄/res/raw和/assets以外,其他的資源在編譯的時(shí)候都會(huì)被第三方軟件aapt進(jìn)行處理,一個(gè)是把圖片和XML文件進(jìn)行處理,例如把XML編譯成為二進(jìn)制形式;另外處理的目的就是生成R.java文件,這個(gè)文件是訪問資源時(shí)必須要用到的。
/res目錄下面的所有文件都會(huì)映射到R.java文件中,以整數(shù)Id的形式被標(biāo)識(shí),相同類型的資源被一個(gè)內(nèi)部類來封裝,一個(gè)R.java的文件類似于這樣:
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
package com.android.explorer;
public final class R {
public static final class attr {
}
public static final class drawable {
public static final int icon=0x7f020000;
}
public static final class id {
public static final int action=0x7f060004;
public static final int description_panel=0x7f060001;
public static final int fileinfo=0x7f060003;
public static final int filename=0x7f060002;
public static final int linearlayout_test_1=0x7f060005;
public static final int linearlayout_test_2=0x7f060006;
public static final int linearlayout_test_3=0x7f060007;
public static final int thumbnail=0x7f060000;
}
public static final class layout {
public static final int fileant_list_item=0x7f030000;
public static final int linearlayout_test=0x7f030001;
}
public static final class raw {
public static final int androidmanifest=0x7f040000;
}
public static final class string {
public static final int app_name=0x7f050001;
public static final int hello=0x7f050000;
}
}
從這個(gè)R.java就可看出在/res中定義或提供資源時(shí)的注意事項(xiàng):
1. 同一個(gè)類型,或同一文件夾下面的資源不可以使用相同的文件名,也就是說不能用文件擴(kuò)展名來區(qū)別不同的文件,因?yàn)镽.java中只保留資源的文件名而不管擴(kuò)展名,所以如果有二個(gè)圖片一個(gè)是icon.png另一個(gè)是icon.jpg,那么在R.java中只會(huì)有一個(gè)R.drawable.icon。另外一個(gè)則會(huì)無法訪問到。
2. 資源文件的名字必須符合Java變量的命名規(guī)則,且不能有大寫,只能是'[a-z][0-9]._',否則會(huì)有編譯錯(cuò)誤,因?yàn)镽.java中的變量Id要與資源中的文件一一對(duì)應(yīng),也就是說用資源文件名來作為Id的變量名,所以一定要符合Java變量的命名規(guī)則,另外它還不能有大寫。
3. 除了SDK支持的folder外,不能再有子Folder,雖不會(huì)有編譯錯(cuò)誤,但是子Folder會(huì)被完全忽略,如在/res/layout下在建一個(gè)子Folder activity(/res/layout/acitivity/, 那么你在生成的R.java中是看不到activity和其內(nèi)的內(nèi)容的。
4. 對(duì)于資源文件的大小有限制,最好不要讓單個(gè)文件大于1M,這是SDK文檔說明的限制,但具體的我沒有進(jìn)行試驗(yàn)(據(jù)說2.2版本以后的可支持到10M,不知道是真的還是假的)
5. 所有/res下面的資源都能通過Resources()并提供Id來訪問。
使用原始資源
對(duì)于大多數(shù)資源在編譯時(shí)會(huì)對(duì)文件內(nèi)容進(jìn)行特殊處理,以方便Apk在運(yùn)行時(shí)訪問。 如果想要運(yùn)行時(shí)使用未經(jīng)處理的原始資源,可以把資源文件放在/res/raw和/assets目錄下面,這二個(gè)目錄的主要區(qū)別在于:
1. /res/raw中的文件會(huì)被映射到R.java中
雖然/res/raw中的文件不會(huì)被aapt處理成為二進(jìn)制,但是它的文件還是被映射到R.java中,以方便以資源Id形式來訪問
2. 子目錄結(jié)構(gòu)
如上面所述,/res/raw中雖可以有子目錄,但是在程序運(yùn)行時(shí)是無法訪問到的,因?yàn)?res下面的所有非法子目錄在R.java中都是看不到的。而且這些子目錄和文件都不會(huì)被編譯進(jìn)入Apk中,解壓Apk文件后也看不到/res/raw下面去找了。
而/assets是允許有子目錄的,并且完全可以訪問到,并且會(huì)被打包進(jìn)Apk,解壓Apk后,這些文件仍然存在并且與源碼包中的一樣。
3. 訪問方式
/res/raw下面的文件(子文件夾是訪問不到的了)的訪問方式是通過Resources,并且必須提供資源的Id
InputStream in = Context.getResources().openRawResource(R.id.filename);
所以為什么子文件夾無法訪問,因?yàn)闆]有Id啊。
而/assets則要通過AssetManager來訪問。下面著重講解如何訪問/assets下面的資源文件。
通過AssetManager來訪問/assets下面的原始資源文件
1. 文件的讀取方式
用AssetManager.open(String filename)來打開一個(gè)文件,這是一組重載方法,還有其他參數(shù)可以設(shè)置打開模式等,可以參考文檔
這里的filename是相對(duì)于/assets的路徑,比如:
InputStream in = mAssetManager.open("hello.txt"); // '/assets/hello.txt'
InputStream in2 = mAssetManager.open("config/ui.txt"); // '/assets/config/ui.txt'
2. 文件夾處理 --- 如何遍歷/assets
可以看到如果想要訪問/assets下面的文件,必須要知道文件名和相對(duì)于/assets的路徑。所以,如果你不預(yù)先知道其下面有什么的時(shí)候又該如何處理呢?那就需要列出它下面所有的文件,然后再選取我們需要的,所以新的問題來了,如何列出/assets下面所有的文件呢?
AssetManager提供了一個(gè)列出/assets下某個(gè)路徑下面的方法:
public finalString[]list(String path)
Since: API Level 1
Return a String array of all the assets at the given path.
Parameters
path A relative path within the assets, i.e., "docs/home.html".
Returns
•String[] Array of strings, one for each asset. These file names are relative to 'path'. You can open the file by concatenating 'path' and a name in the returned string (via File) and passing that to open().
其實(shí)這個(gè)文檔寫的有問題,list()是列出一個(gè)文件夾下面的文件,所以應(yīng)該傳入一個(gè)文件夾路徑而非文檔中的"docs/home.html"。
還有一個(gè)最大的問題就是如何列出根目錄/assets下面的內(nèi)容,因?yàn)橹挥兄懒烁夸浵旅娴臇|西,才能去相對(duì)的子目錄去找東西,所以這是個(gè)必須最先解決的問題。
其實(shí)文檔沒有說的太明白這個(gè)方法到底如何使用,也就是說這個(gè)String參數(shù)到底如何傳。猜想著根目錄為/assets,所以嘗試了以下:
mAssetManager.list("."); // returns array size is 0
mAssetManager.list("/"); // returns [AndroidManifest.xml, META-INF, assets, classes.dex, res, resources.arsc] // don't worry, u can see these files though, no way to access them
mAssetManager.list("/assets"); // returns array size is 0
//Google了一下,找到了正解:
mAssetManager.list(""); // returns stuff in /assets
然后再根據(jù)所列出的子項(xiàng)去遞歸遍歷子文件,直到找到所有的文件為止。
常見的問題
1. 資源文件只能以InputStream方式來獲取
如果想操作文件怎么辦,如果想要用文件Uri怎么辦。光靠API當(dāng)然不行,它只給你InputStream,也就是說它是只讀的??尚械霓k法就是讀取文件然后寫入一個(gè)臨時(shí)文件中,再對(duì)臨時(shí)文件進(jìn)行想要的文件操作??梢栽趦?nèi)部存儲(chǔ)或外部存儲(chǔ)上面用Context提供的接口來創(chuàng)建文件,詳細(xì)的請(qǐng)參考<Android開發(fā)筆記之: 數(shù)據(jù)存儲(chǔ)方式詳解>。Java牛人可能想要用Java本身的能力:
File File.createTempFile(String prefix, String suffix);
File File.createTempFile(String prefix, String suffix, File path);
這也是可以的,但要考慮Android系統(tǒng)的特性,也就是說所寫的路徑是否有權(quán)限。比如對(duì)于第一個(gè)方法,用的是"java.io.tmpdir"這個(gè)在Android當(dāng)中就是"/sdcard",所以當(dāng)沒有SD卡時(shí)這個(gè)方法必拋異常。
2. 所有資源文件都是只讀的,運(yùn)行時(shí)無法更改
因?yàn)?,程序運(yùn)行時(shí)是把Apk動(dòng)態(tài)解析加載到內(nèi)存中,也就是說,Apk是不會(huì)有變化的,它是無法被改變的。
3. 所有的資源文件夾/res和/assets也都是只讀的,不可寫入
如上面所說,Apk是在編譯后是無法再改變的了。
實(shí)例
下面是一個(gè)實(shí)例,可以遞歸式的遍歷/assets下面所有的文件夾和文件
package com.android.explorer;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
/*
* Explore all stuff in /assets and perform actions specified by users.
*/
public class FileAntActivity extends ListActivity {
private static final String TAG = "FileAntActivity";
private AssetManager mAssetManager;
private static final String EXTRA_CURRENT_DIRECTORY = "current_directory";
private static final String EXTRA_PARENT = "parent_directory";
public static final String FILEANT_VIEW = "com.android.fileant.VIEW";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String current = null;
String parent = null;
if (intent != null && intent.hasExtra(EXTRA_CURRENT_DIRECTORY)) {
current = intent.getStringExtra(EXTRA_CURRENT_DIRECTORY);
}
if (current == null) {
current = "";
}
if (intent != null && intent.hasExtra(EXTRA_PARENT)) {
parent = intent.getStringExtra(EXTRA_PARENT);
}
if (parent == null) {
parent = "";
}
mAssetManager = getAssets();
if (TextUtils.isEmpty(parent)) {
setTitle("/assets");
} else {
setTitle(parent);
}
try {
// List all the stuff in /assets
if (!TextUtils.isEmpty(parent)) {
current = parent + File.separator + current;
}
Log.e(TAG, "current: '" + current + "'");
String[] stuff = mAssetManager.list(current);
setListAdapter(new FileAntAdapter(this, stuff, current));
} catch (IOException e) {
e.printStackTrace();
}
}
private class FileAntAdapter extends BaseAdapter {
private Context mContext;
private String[] mEntries;
private String mParentDirectory;
public FileAntAdapter(Context context, String[] data, String parent) {
mContext = context;
this.mEntries = data;
mParentDirectory = parent;
}
public int getCount() {
return mEntries.length;
}
public Object getItem(int position) {
return mEntries[position];
}
public long getItemId(int position) {
return (long) position;
}
public View getView(final int position, View item, ViewGroup parent) {
LayoutInflater factory = LayoutInflater.from(mContext);
if (item == null) {
item = factory.inflate(R.layout.fileant_list_item, null);
TextView filename = (TextView) item.findViewById(R.id.filename);
TextView fileinfo = (TextView) item.findViewById(R.id.fileinfo);
ImageButton action = (ImageButton) item.findViewById(R.id.action);
final String entry = mEntries[position];
filename.setText(entry);
boolean isDir = isDirectory(entry);
if (isDir) {
fileinfo.setText("Click to view folder");
action.setVisibility(View.GONE);
item.setClickable(true);
item.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent intent = new Intent(FILEANT_VIEW);
intent.putExtra(EXTRA_CURRENT_DIRECTORY, entry);
intent.putExtra(EXTRA_PARENT, mParentDirectory);
startActivity(intent);
}
});
} else {
final String type =
MimeTypeMap.getSingleton().getMimeTypeFromExtension(getExtension(entry));
fileinfo.setText(type);
item.setClickable(false);
action.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
String filepath = entry;
if (!TextUtils.isEmpty(mParentDirectory)) {
filepath = mParentDirectory + File.separator + filepath;
}
BufferedInputStream in = new BufferedInputStream(mManager.open(filepath));
// Do whatever you like with this input stream
}
});
}
}
return item;
}
}
/**
* Test Whether an entry is a file or directory based on the rule:
* File: has extension *.*, or starts with ".", which is a hidden files in Unix/Linux,
* otherwise, it is a directory
* @param filename
* @return
*/
private boolean isDirectory(String filename) {
return !(filename.startsWith(".") || (filename.lastIndexOf(".") != -1));
}
private String getExtension(String filename) {
int index = filename.lastIndexOf(".");
if (index == -1) {
return "";
}
return filename.substring(index + 1, filename.length()).toLowerCase();
}
}