vudroid是一款能讀PDF和djvu兩種格式的閱讀器,其特點是能夠跳頁、全屏閱讀、能夠一次兩頁連續緩衝、能自由進行頁面的縮放、能記憶閱讀位置、劃屏翻頁流暢。其缺點是當檔案大時,渲染速度較慢,偶爾會退出。
在分析vudroid源碼的過程中,我們將其分為三個部分:
1)操作主介面(選擇檔案、瀏覽閱讀曆史部分)
2)閱讀介面(涉及閱讀時的操作、事件處理)
3)源檔案解析部分(主要是讀取PDF和djvu檔案部分,該部分涉及對相關檔案的格式解析)
1、主介面
vudroid的主介面 比較簡單,主要包括一個檔案瀏覽介面和一個瀏覽閱讀曆史介面,使用一個tabHost來實現 ,其介面如下:
browse介面瀏覽sdcard檔案夾,顯示其中的檔案目錄,對於檔案只顯示pdf、djvu、djv三種格式檔案。而recent則顯示最近瀏覽過的檔案清單。
這一部分的實現是比較簡單的,主要包括了3個檔案:
1、MainBrowserActivity
我們先來看看MainBrowserActivity.java的源碼:
public class MainBrowserActivity extends BaseBrowserActivity{ private final static HashMap<String, Class<? extends Activity>> extensionToActivity = new HashMap<String, Class<? extends Activity>>(); static { extensionToActivity.put("pdf", PdfViewerActivity.class); extensionToActivity.put("djvu", DjvuViewerActivity.class); extensionToActivity.put("djv", DjvuViewerActivity.class); } @Override protected FileFilter createFileFilter() { return new FileFilter() { public boolean accept(File pathname) { for (String s : extensionToActivity.keySet()) { if (pathname.getName().endsWith("." + s)) return true; } return pathname.isDirectory(); } }; } @Override protected void showDocument(Uri uri) { final Intent intent = new Intent(Intent.ACTION_VIEW, uri); String uriString = uri.toString(); String extension = uriString.substring(uriString.lastIndexOf('.') + 1); intent.setClass(this, extensionToActivity.get(extension)); startActivity(intent); }}
這裡將所有與介面相關的東西都留給BaseBrowserActivity.java去做了。MainBrowserActivity只負責判斷可以閱讀類型的檔案,並轉到相關的處理處理類去處理。MainBrowserActivity重載了createFileFilter函數,用於建立一個檔案篩選器。這實際上是一個解耦合的過程,後續需要添加其他格式的檔案,如txt,則只需修改createFileFilter函數即可,而沒有必要動BaseBrowserActivity中的代碼。
2、BaseBrowserActivity
對於BaseBrowserActivity檔案,其實可以猜到它所完成的功能:產生兩個listview去填充tabHost。於是看它的oncreate函數:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.browser); viewerPreferences = new ViewerPreferences(this); final ListView browseList = initBrowserListView(); final ListView recentListView = initRecentListView(); TabHost tabHost = (TabHost) findViewById(R.id.browserTabHost); tabHost.setup(); tabHost.addTab(tabHost.newTabSpec("Browse").setIndicator("Browse").setContent(new TabHost.TabContentFactory() { public View createTabContent(String s) { return browseList; } })); tabHost.addTab(tabHost.newTabSpec("Recent").setIndicator("Recent").setContent(new TabHost.TabContentFactory() { public View createTabContent(String s) { return recentListView; } })); }
這裡對於 tabHost的兩個tab:第一個browse,需要顯示當前檔案目錄,點擊檔案夾時顯示下一個檔案夾目錄,而點擊檔案時則轉到瀏覽該檔案的activity;而第二個recent,則只需要做到點擊檔案時,轉到瀏覽該檔案的activity即可。於是我們可以看到,這兩個tab中listview的setOnItemClickListener是有區別的。
對於browse:
private ListView initBrowserListView() { final ListView listView = new ListView(this); adapter = new BrowserAdapter(this, filter); listView.setAdapter(adapter); listView.setOnItemClickListener(onItemClickListener); listView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); return listView; }
private final AdapterView.OnItemClickListener onItemClickListener = new AdapterView.OnItemClickListener() { @SuppressWarnings({"unchecked"}) public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { final File file = ((AdapterView<BrowserAdapter>)adapterView).getAdapter().getItem(i); if (file.isDirectory()) { setCurrentDir(file); } else { showDocument(file); } } };
而對於recent:
private ListView initRecentListView() { ListView listView = new ListView(this); recentAdapter = new UriBrowserAdapter(); listView.setAdapter(recentAdapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @SuppressWarnings({"unchecked"}) public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { showDocument(((AdapterView<UriBrowserAdapter>) adapterView).getAdapter().getItem(i)); } }); listView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); return listView; }
從源碼可以看出,這兩者的區別是顯而易見的。
3、ViewerPreferences
ViewerPreferences檔案主要是利用SharedPreferences儲存了兩個參數:是否全屏(FULL_SCREEN)、最近瀏覽檔案清單。
對於最近瀏覽檔案,其代碼為:
public void addRecent(Uri uri) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString("recent:" + uri.toString(), uri.toString() + "\n" + System.currentTimeMillis()); editor.commit(); } public List<Uri> getRecent() { TreeMap<Long, Uri> treeMap = new TreeMap<Long, Uri>(); for (String key : sharedPreferences.getAll().keySet()) { if (key.startsWith("recent")) { String uriPlusDate = sharedPreferences.getString(key, null); String[] uriThenDate = uriPlusDate.split("\n"); treeMap.put(Long.parseLong(uriThenDate.length > 1 ? uriThenDate[1] : "0"), Uri.parse(uriThenDate[0])); } } ArrayList<Uri> list = new ArrayList<Uri>(treeMap.values()); Collections.reverse(list); return list; }
4、adapter
項目中用到的兩個Adapter都是重載過的:BrowserAdapter和UriBrowserAdapter。
Adapter的作用主要是串連view和dataset。其中有一個比較重要的函數是getView,每個資料集的一條資料會產生一個view,該view就是通過該函數得來的。因此如果你如果要讓一個view顯示自己的資料,就必須重載該函數。
public View getView(int i, View view, ViewGroup viewGroup) { final View browserItem = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.browseritem, viewGroup, false); final ImageView imageView = (ImageView) browserItem.findViewById(R.id.browserItemIcon); final Uri uri = uris.get(i); final TextView textView = (TextView) browserItem.findViewById(R.id.browserItemText); textView.setText(uri.getLastPathSegment()); imageView.setImageResource(R.drawable.book); return browserItem; }
另外,Adapter的資料集在發生變化時,需要通知view去改變,是通過notifyDataSetInvalidated()函數完成的。
public void setUris(List<Uri> uris) { this.uris = uris; notifyDataSetInvalidated(); }