内容简介:通过上一篇分析,我们已经知道如何响应并打开菜单,而且菜单中第一项是打开本地书柜,这一篇我们就以此为入口,去探究FBReader的书柜是怎么实现,以及是如何分辨一本书并且能打开一本书的。打开本地书柜action:ShowLibraryAction,直接看其run方法:该Activity为ListActivity:
通过上一篇分析,我们已经知道如何响应并打开菜单,而且菜单中第一项是打开本地书柜,这一篇我们就以此为入口,去探究FBReader的书柜是怎么实现,以及是如何分辨一本书并且能打开一本书的。
一、打开FBReader本地书柜时,首页内容显示都做了些什么
打开本地书柜action:ShowLibraryAction,直接看其run方法:
@Override protected void run(Object ... params) { final Intent externalIntent = new Intent(FBReaderIntents.Action.EXTERNAL_LIBRARY); final Intent internalIntent = new Intent(BaseActivity.getApplicationContext(), LibraryActivity.class); //查询是否有满足条件的插件书柜,有则打开插件中的书柜,没有就打开本地书柜 LibraryActivity if (PackageUtil.canBeStarted(BaseActivity, externalIntent, true)) { try { startLibraryActivity(externalIntent); } catch (ActivityNotFoundException e) { startLibraryActivity(internalIntent); } } else { startLibraryActivity(internalIntent); } } 复制代码
该Activity为ListActivity:
查看其onCreate:
private final BookCollectionShadow myCollection = new BookCollectionShadow(); @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); // 忽略部分代码... new LibraryTreeAdapter(this); myCollection.bindToService(this, new Runnable() { public void run() { setProgressBarIndeterminateVisibility(!myCollection.status().IsComplete); myRootTree = new RootTree(myCollection, PluginCollection.Instance(Paths.systemInfo(LibraryActivity.this))); myCollection.addListener(LibraryActivity.this); init(getIntent()); } }); } 复制代码
可以发现,这里设置了adapter为 LibraryTreeAdapter ,那么这个页面显示的内容数据是从哪来的呢?别着急,这里我们先去看一下myCollection是个什么东东,我们就顺着onCreate中调用的bindToService看起:
public synchronized boolean bindToService(Context context, Runnable onBindAction) { if (myInterface != null && myContext == context) { if (onBindAction != null) { Config.Instance().runOnConnect(onBindAction); } return true; } else { if (onBindAction != null) { synchronized (myOnBindActions) { myOnBindActions.add(onBindAction); } } final boolean result = context.bindService( FBReaderIntents.internalIntent(FBReaderIntents.Action.LIBRARY_SERVICE), this, Service.BIND_AUTO_CREATE ); if (result) { myContext = context; } return result; } } 复制代码
可以看出,这个collection会绑定LibraryService,而且传递的ServiceConnection为this,那么说明这个collection实现了ServiceConnection这个接口,顺着这个逻辑去看一下onServiceConnected:
public void onServiceConnected(ComponentName name, IBinder service) { synchronized (this) { myInterface = LibraryInterface.Stub.asInterface(service); } final List<Runnable> actions; synchronized (myOnBindActions) { actions = new ArrayList<Runnable>(myOnBindActions); myOnBindActions.clear(); } for (Runnable a : actions) { Config.Instance().runOnConnect(a); } // 忽略部分代码... } 复制代码
通过分析这两个方法可以得知,bindToService(context,runnable)是当已绑定service时,则直接通过Config来执行runnable,否则先绑定service,然后再通过Config来执行runnable。那么这个Config又是何方神圣呢?我们就顺着其runOnConnect来一探究竟:
public abstract void runOnConnect(Runnable runnable); 复制代码
向下查找,可以找到其唯一实现在子类ConfigShadow中:
@Override public void runOnConnect(Runnable runnable) { if (myInterface != null) {//当前已成功绑定service,则直接执行run runnable.run(); } else { synchronized (myDeferredActions) { myDeferredActions.add(runnable);//未成功绑定放入集合中,待执行 } } } 复制代码
再来看一下ConfigShadow的构造方法:
public ConfigShadow(Context context) { myContext = context; context.bindService( FBReaderIntents.internalIntent(FBReaderIntents.Action.CONFIG_SERVICE), this, Service.BIND_AUTO_CREATE ); } 复制代码
可以看出该类绑定了ConfigService,而且自身实现了ServiceConnection接口:
public void onServiceConnected(ComponentName name, IBinder service) { synchronized (this) { myInterface = ConfigInterface.Stub.asInterface(service); myContext.registerReceiver( myReceiver, new IntentFilter(FBReaderIntents.Event.CONFIG_OPTION_CHANGE) ); } final List<Runnable> actions; synchronized (myDeferredActions) { actions = new ArrayList<Runnable>(myDeferredActions); myDeferredActions.clear(); } for (Runnable a : actions) { a.run(); } } 复制代码
可以看出,在成功链接ConfigService之后,首先绑定了Config_Option_Change的广播接收者,并且将待执行集合中的runnable依次取出并执行。
那么ConfigService是做啥的呢?直接进去看看:
public class ConfigService extends Service { private ConfigInterface.Stub myConfig; @Override public IBinder onBind(Intent intent) { return myConfig; } @Override public void onCreate() { super.onCreate(); myConfig = new SQLiteConfig(this); } // 忽略部分代码 } 复制代码
可以看到onBind返回的是SQLiteConfig,这里也就比较明确了,是操作Config数据库的。那么ConfigShadow又是在哪被创建的呢,通过find可以查到其唯一创建在ZLAndroidApplication:
public abstract class ZLAndroidApplication extends Application { // 忽略部分代码 private ConfigShadow myConfig; @Override public void onCreate() { super.onCreate(); myConfig = new ConfigShadow(this); } } 复制代码
到此,我们先简单总结一下:
- 应用在启动时通过创建 ConfigShadow 绑定了 ConfigService (进程:configService)
- LibraryActivity在启动时通过 BookCollectionShadow 绑定了 LibraryService (进程:libraryService)
- BookCollectionShadow的 bindToService(context,runnable) 方法,在绑定LibraryService成功后,会通过Config的 runOnConnect 方法处理runnable
通过以上简单的总结,我们可以知道,下一步一般会执行runnable中的方法:
myRootTree = new RootTree(myCollection, PluginCollection.Instance(Paths.systemInfo(LibraryActivity.this))); myCollection.addListener(LibraryActivity.this); init(getIntent()); 复制代码
那么我们就一步步来分析,这些操作都做了些什么:
1.RootTree的创建:
public RootTree(IBookCollection collection, PluginCollection pluginCollection) { super(collection, pluginCollection); // 收藏 new FavoritesTree(this); // 最近读过 new RecentBooksTree(this); // 作者 new AuthorListTree(this); // 按书名 new TitleListTree(this); // 按系列 new SeriesListTree(this); // 标签 new TagListTree(this); // 同步 if (new SyncOptions().Enabled.getValue()) { new SyncTree(this); } // 文件夹 new FileFirstLevelTree(this); } 复制代码
FBReader对于树形结构通过ZLTree来定义。
看到这里,我们应该可以判断,书库首页打开时显示的数据就来自于此,那么这些数据又是怎么样传递给Adapter的呢?看样还得继续看。
2.BookCollectionShadow添加监听,这个我们后面再分析。
3.调用init(intent)方法:
//TreeActivity protected void init(Intent intent) { //首次打开书库时intent中没有数据,key为null final FBTree.Key key = (FBTree.Key)intent.getSerializableExtra(TREE_KEY_KEY); final FBTree.Key selectedKey = (FBTree.Key)intent.getSerializableExtra(SELECTED_TREE_KEY_KEY); //根据指定的key获取对应的tree myCurrentTree = getTreeByKey(key); myCurrentKey = myCurrentTree.getUniqueKey(); final TreeAdapter adapter = getTreeAdapter(); adapter.replaceAll(myCurrentTree.subtrees(), false); // 忽略部分代码... } //LibraryActivity @Override protected LibraryTree getTreeByKey(FBTree.Key key) { return key != null ? myRootTree.getLibraryTree(key) : myRootTree; } //TreeAdapter LibraryTreeAdapter父类 public void replaceAll(final Collection<FBTree> items, final boolean invalidateViews) { myActivity.runOnUiThread(new Runnable() { public void run() { synchronized (myItems) { myItems.clear(); myItems.addAll(items); } notifyDataSetChanged(); if (invalidateViews) { myActivity.getListView().invalidateViews(); } } }); } 复制代码
通过上面三个方法的,我们可以知道,在起初进入LibraryAcivity时,由于intent中没有数据,故取出的key为null,在调用getTreeByKey时返回RootTree,并通过Adapter的replaceAll方法,修改数据并nofity。
二、揭开BookCollectionShadow的神秘面纱
通过上面的分析,我们已经知道BookCollectionShadow是用来绑定LibraryService的。通过它,可以跟LibraryService进行通讯。另外,上面还有提到注册监听者,这个监听者又是怎么一回事,LibraryService又是怎么一回事呢?我们接下来就一步步去弄清这些问题。
先来看一下它的继承关系图:
由于BookCollectionShadow实现了IBookCollection接口,而该接口定义行为与LibraryService.aidl一致,并且BookCollectionShadow是用来绑定LibraryService的,细看其对故其对IBookCollection的实现均会通过LibraryInterface实例由进程间通讯,调用LibraryService的对应方法。
那么我们就去看一下,这个LibraryService到底是个做啥的:
public class LibraryService extends Service 复制代码
1.onCreate:
@Override public void onCreate() { super.onCreate(); synchronized (ourDatabaseLock) { if (ourDatabase == null) { ourDatabase = new SQLiteBooksDatabase(LibraryService.this); } } myLibrary = new LibraryImplementation(ourDatabase); bindService( new Intent(this, DataService.class), DataConnection, DataService.BIND_AUTO_CREATE ); } 复制代码
可以看出这里做了三件事:
- 创建了 SQLiteBooksDatabase ,该数据库为Book数据库
- 创建了 LibraryImplementation 实例myLibrary,并把数据库传递进去
- 绑定了 DataService (进程:dataService)
2.onBind:
@Override public IBinder onBind(Intent intent) { return myLibrary; } 复制代码
从这里可以看出,返回的IBinder对象为myLibrary,那么也就意味着LibraryImplementation继承自LibraryService.Stub,也就具体实现了LibraryInterface。
3.LibraryImplementation:
public final class LibraryImplementation extends LibraryInterface.Stub { private final BooksDatabase myDatabase; private final List<FileObserver> myFileObservers = new LinkedList<FileObserver>(); private BookCollection myCollection; LibraryImplementation(BooksDatabase db) { myDatabase = db; myCollection = new BookCollection( Paths.systemInfo(LibraryService.this), myDatabase, Paths.bookPath() ); reset(true); } //忽略部分代码... } 复制代码
这里发现实例化了一个名字不带Shadow的 BookCollection ,这个BookCollection又是干啥的呢?
4.BookCollection:
public class BookCollection extends AbstractBookCollection<DbBook> 对比看一下 BookCollectionShadow public class BookCollectionShadow extends AbstractBookCollection<Book> implements ServiceConnection 复制代码
通过继承关系,我们可以得知BookCollection跟BookCollectionShadow一样是继承自AbstractBookCollection,只不过泛型的AbstractBook类型不同。而且通过上面BookCollectionShadow的继承图来看,可以推出BookCollection同样实现了IBookCollection接口。
5.由于LibraryService在onBind时返回的实例为LibraryImplementation,所以在BookCollectionShadow中:
public void onServiceConnected(ComponentName name, IBinder service) { synchronized (this) { // LibraryInterface的实例myInterface,实际为LibraryService中的LibraryImplementation myInterface = LibraryInterface.Stub.asInterface(service); } // 忽略部分代码... } 复制代码
通过查看代码,可以看出BookCollectionShadow对接口IBookCollection的实现具体均是通过myInterface调用同样的方法跨进程完成,我们随便挑一个实现方法看一下:
public synchronized void rescan(String path) { if (myInterface != null) { try { myInterface.rescan(path); } catch (RemoteException e) { // ignore } } } 复制代码
6.myInterface的真身LibraryImplementation对LibraryInterface的具体实现,同5我们还是查看rescan:
public void rescan(String path) { // myCollection为BookCollection的实例 myCollection.rescan(path); } 复制代码
这里就很明显了,LibraryImplementation对LibraryInterface的具体实现,最后的执行主体就是BookCollection。
到此,我们就可以简单对BookCollectionShadow做一个总结:
- 绑定服务 LibraryService
- 实现 IBookCollection ,与服务LibraryService有同样的功能
- 绑定LibraryService成功时,获取到的LibraryInterface实例为 LibraryImplementation
- 调用IBookCollection定义的方法时,实际是由LibraryImplementation调用内部的 BookCollection 实例来最终完成
- BookCollection行为基本以通过 SQLiteBooksDatabase 操作Book.db为主
BookCollectionShadow是BookCollection的“影子”,真正的实现是BookCollection。
三、书柜页面LibraryActivity,是如何响应不同类型item点击的
上面我们已经知道,书柜首页的数据来自于RootTree,接下来我们就从“文件夹”这个item,继续往下分析。
既然要查看item的点击处理,那就查看点击处理方法onListItemClick:
@Override protected void onListItemClick(ListView listView, View view, int position, long rowId) { final LibraryTree tree = (LibraryTree)getTreeAdapter().getItem(position); if (tree instanceof ExternalViewTree) { runOrInstallExternalView(true); } else { final Book book = tree.getBook(); if (book != null) { showBookInfo(book); } else { openTree(tree); } } } 复制代码
一个判断book != null,非空打开书籍详情,为空就打开子树。从RootTree初始化中,我们知道“文件夹”对应的Tree为FileFirstLevelTree:
其getBook方法通过追溯可以发现是在LibraryTree中,而且return null。那么就是打开子tree,具体是通过方法openTree:
private void openTree(final FBTree tree, final FBTree treeToSelect, final boolean storeInHistory) { switch (tree.getOpeningStatus()) { case WAIT_FOR_OPEN: case ALWAYS_RELOAD_BEFORE_OPENING: final String messageKey = tree.getOpeningStatusMessage(); if (messageKey != null) { UIUtil.createExecutor(TreeActivity.this, messageKey).execute( new Runnable() { public void run() { tree.waitForOpening(); } }, new Runnable() { public void run() { openTreeInternal(tree, treeToSelect, storeInHistory); } } ); } else { // 对于FileFirstLevelTree来说会执行这块 tree.waitForOpening(); openTreeInternal(tree, treeToSelect, storeInHistory); } break; default: openTreeInternal(tree, treeToSelect, storeInHistory); break; } } 复制代码
对于FileFirstLevelTree来说:
@Override public Status getOpeningStatus() { return Status.ALWAYS_RELOAD_BEFORE_OPENING; } public String getOpeningStatusMessage() { return null; } 复制代码
那么上面的openTree会执行标记处的代码,也就是会执行waitForOpening方法:
@Override public void waitForOpening() { clear(); for (String dir : Paths.BookPathOption.getValue()) { addChild(dir, resource().getResource("fileTreeLibrary").getValue(), dir); } addChild("/", "fileTreeRoot"); final List<String> cards = Paths.allCardDirectories(); if (cards.size() == 1) { addChild(cards.get(0), "fileTreeCard"); } else { final ZLResource res = resource().getResource("fileTreeCard"); final String title = res.getResource("withIndex").getValue(); final String summary = res.getResource("summary").getValue(); int index = 0; for (String dir : cards) { addChild(dir, title.replaceAll("%s", String.valueOf(++index)), summary); } } } 复制代码
这段代码可以暂时不去关心具体都是做了哪些,我们只需要知道最终经过waitForOpening这个方法,他生成了当前tree的子tree,那么子tree又是如何生成的呢?这个我们就要去着重看一下方法addChild:
private void addChild(String path, String title, String summary) { final ZLFile file = ZLFile.createFileByPath(path); if (file != null) { new FileTree(this, file, title, summary); } } 复制代码
这里我们不免会有个疑惑,既然是add打头的方法,为什么跟到最后,发现,并没有哪里进行了相关add操作呢?其实,这个add是发生在了FileTree创建的时候:
FileTree(LibraryTree parent, ZLFile file, String name, String summary) { super(parent); //忽略部分代码 } 一直往上追溯,查看super构造方法,最终在ZLTree: protected ZLTree(T parent, int position) { //忽略部分代码... Parent = parent; if (parent != null) { Level = parent.Level + 1; //此处执行add操作,将this当前tree,添加到parent中 parent.addSubtree((T)this, position); } else { Level = 0; } } 复制代码
通过执行waitForOpening,已经准备好了当前tree的子tree,那么就进入到后续显示子tree的处理了:
private void openTreeInternal(FBTree tree, FBTree treeToSelect, boolean storeInHistory) { switch (tree.getOpeningStatus()) { case READY_TO_OPEN: case ALWAYS_RELOAD_BEFORE_OPENING: if (storeInHistory && !myCurrentKey.equals(tree.getUniqueKey())) { myHistory.add(myCurrentKey); } onNewIntent(new Intent(this, getClass()) .setAction(OPEN_TREE_ACTION) .putExtra(TREE_KEY_KEY, tree.getUniqueKey()) .putExtra( SELECTED_TREE_KEY_KEY, treeToSelect != null ? treeToSelect.getUniqueKey() : null ) .putExtra(HISTORY_KEY, new ArrayList<FBTree.Key>(myHistory)) ); break; case CANNOT_OPEN: UIMessageUtil.showErrorMessage(TreeActivity.this, tree.getOpeningStatusMessage()); break; } } 复制代码
这里同样的会执行ALWAYS_RELOAD_BEFORE_OPENING这个case,也就是说会调用onNewIntent,并且在intent中传递了key为TREE_KEY_KEY,value为tree.getUniqueKey()的参数。接着我们来看一下onNewIntent:
@Override protected void onNewIntent(final Intent intent) { OrientationUtil.setOrientation(this, intent); if (OPEN_TREE_ACTION.equals(intent.getAction())) { runOnUiThread(new Runnable() { public void run() { init(intent); } }); } else { super.onNewIntent(intent); } } 复制代码
上面分析时,我们知道在调用方法init(intent)时,会读取intent中的数据,这时key不为空,可以根据key获得对应的tree,并且再次调用adapter的replaceAll替换数据并notify。
到这里,我们来个简单的总结。
当点击的item不是book时:
- 调用该tree的 waitForOpening 方法,准备子tree
- 在生成子tree时,将 父tree 作为 构造参数 传入子tree,会将子tree添加到父tree的subtree中
- 调用 onNewIntent 方法,并将点击的 tree.getgetUniqueKey 传入intent中
- Activity的onNewIntent方法出发,会执行 init(intent)
- init(intent)方法会读取传递过来的 key ,根据此key获取到对应的tree
- 调用adapter的 replcaAll 方法,将adapter的数据替换为获取到的 tree.subTrees
当点击的item为book时,打开图书详情。
四、FBReader在扫描本地文件时,是如何识别出可阅读电子书的
通过上面的分析,我们已经知道,当我们点击非图书item时,都是经历了些什么。那么当我们继续往下点,进入SD卡目录去扫描其中文件时,可以发现当FBReader扫描到某个item是单子书时会显示出来电子书的图标,那么FBReader是如何识别的呢?
由书柜首页“电子书”,点击进入子tree时,通过上面的分析,我们知道其准备子tree时调用了addChild方法,而其中生成的子tree为FileTree:
private void addChild(String path, String title, String summary) { final ZLFile file = ZLFile.createFileByPath(path); if (file != null) { new FileTree(this, file, title, summary); } } 复制代码
这里调用了一个非常重要的方法ZLFile.createFileByPath(path):
public static ZLFile createFileByPath(String path) { if (path == null) { return null; } ZLFile cached = ourCachedFiles.get(path); if (cached != null) { //缓存中有,则直接返回缓存中的ZLFile return cached; } int len = path.length(); char first = len == 0 ? '*' : path.charAt(0); if (first != '/') { //路径为资源路径时 while (len > 1 && first == '.' && path.charAt(1) == '/') { path = path.substring(2); len -= 2; first = len == 0 ? '*' : path.charAt(0); } return ZLResourceFile.createResourceFile(path); } int index = path.lastIndexOf(':'); if (index > 1) { //路径中包含:时 final ZLFile archive = createFileByPath(path.substring(0, index)); if (archive != null && archive.myArchiveType != 0) { return ZLArchiveEntryFile.createArchiveEntryFile( archive, path.substring(index + 1) ); } } return new ZLPhysicalFile(path); } 复制代码
我们知道SD卡中的文件夹或文件路径以“/”开头,而且不包含“:”,那么在生成子tree时,就会对应生成ZLPhysicalFile。这里我们也就知道了ZLPhysicalFile是用来描述物理文件的,而且FBReader中对文件的描述,统一是基于ZLFile。接着,我们就进入ZLPhysicalFile,去了解一下这个类具体是做啥的:
private final File myFile;//路径对应的文件实体 ZLPhysicalFile(String path) { this(new File(path)); } public ZLPhysicalFile(File file) { myFile = file; init(); } protected void init() { final String name = getLongName(); final int index = name.lastIndexOf('.'); //获取文件拓展名 myExtension = (index > 0) ? name.substring(index + 1).toLowerCase().intern() : ""; myShortName = name.substring(name.lastIndexOf('/') + 1); int archiveType = ArchiveType.NONE; //根据特定扩展名,给文件设置archiveType if (myExtension == "zip") { archiveType |= ArchiveType.ZIP; } else if (myExtension == "oebzip") { archiveType |= ArchiveType.ZIP; } else if (myExtension == "epub") { archiveType |= ArchiveType.ZIP; } else if (myExtension == "tar") { archiveType |= ArchiveType.TAR; } else if (lowerCaseName.endsWith(".tgz")) { //nothing to-do myNameWithoutExtension = myNameWithoutExtension.substr(0, myNameWithoutExtension.length() - 3) + "tar"; //myArchiveType = myArchiveType | ArchiveType.TAR | ArchiveType.GZIP; } myArchiveType = archiveType; } 复制代码
这里ZLPhysicalFile在初始化时做了两件事:
- 保存路径对应的真实File
- 对文件进行识别,看其是否属于支持的电子书格式类型,如果是的话,则将其archiveType设置为对应值
这样我们也就能够看出,如果遍历到的文件为特定格式的电子书文件时,其archiveType是会有对应值的。那么就可以猜测是不是在LibraryActivity的Adapter中应该是有对该值的判断?进去看一下:
public View getView(int position, View convertView, final ViewGroup parent) { final LibraryTree tree = (LibraryTree)getItem(position); //忽略部分代码... if (myCoverManager == null) { view.measure(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); final int coverHeight = view.getMeasuredHeight(); final TreeActivity activity = getActivity(); myCoverManager = new CoverManager(activity, activity.ImageSynchronizer, coverHeight * 15 / 32, coverHeight); view.requestLayout(); } final ImageView coverView = ViewUtil.findImageView(view, R.id.library_tree_item_icon); if (!myCoverManager.trySetCoverImage(coverView, tree)) { coverView.setImageResource(getCoverResourceId(tree)); } return view; } private int getCoverResourceId(LibraryTree tree) { if (tree.getBook() != null) { return R.drawable.ic_list_library_book; } else if (tree instanceof ExternalViewTree) { return R.drawable.plugin_bookshelf; } else if (tree instanceof FavoritesTree) { return R.drawable.ic_list_library_favorites; } else if (tree instanceof FileTree) { final ZLFile file = ((FileTree)tree).getFile(); if (file.isArchive()) { return R.drawable.ic_list_library_zip; } else if (file.isDirectory() && file.isReadable()) { return R.drawable.ic_list_library_folder; } else { return R.drawable.ic_list_library_permission_denied; } }... } 复制代码
发现在Adapter的getView方法中,并没有对archiveType的判断,但是却发现:
- 这里会创建封面管理器CoverManager
- 并且当调用myCoverManager.trySetCoverImage返回false的时候,才会给imageview设置图片资源
- getCoverResourceId方法是根据当前tree的类型来给它设置对应图片资源的
那么这个trySetCoverImage方法又做了什么呢?
public boolean trySetCoverImage(ImageView coverView, FBTree tree) { final CoverHolder holder = getHolder(coverView, tree); Bitmap coverBitmap; try { //取缓存中的封面bitmap coverBitmap = Cache.getBitmap(holder.Key); } catch (CoverCache.NullObjectException e) { return false; } if (coverBitmap == null) { final ZLImage cover = tree.getCover(); if (cover instanceof ZLImageProxy) { final ZLImageProxy img = (ZLImageProxy)cover; if (img.isSynchronized()) { setCoverForView(holder, img); } else { img.startSynchronization( myImageSynchronizer, holder.new CoverSyncRunnable(img) ); } } else if (cover != null) { coverBitmap = getBitmap(cover); } } if (coverBitmap != null) { //如果封面coverBitmap存在,就设置给Imageview holder.CoverView.setImageBitmap(coverBitmap); return true; } return false; } 复制代码
其中对是否有封面的一个核心方法是tree.getCover():
public final ZLImage getCover() { if (!myCoverRequested) { //生成封面 myCover = createCover(); //忽略部分代码... } return myCover; } @Override public ZLImage createCover() { return CoverUtil.getCover(getBook(), PluginCollection); } 复制代码
最终是调用了CoverUtil的getConver的方法:
public static ZLImage getCover(AbstractBook book, IFormatPluginCollection collection) { if (book == null) { return null; } synchronized (book) { return getCover(ZLFile.createFileByPath(book.getPath()), collection); } } 复制代码
所以当book不为空时,才会执行获取封面的方法,那么就需要看一下getBook获取的book对象是否为空:
@Override public Book getBook() { if (myBook == null) { //根据文件路径去尝试获取book myBook = Collection.getBookByFile(myFile.getPath()); if (myBook == null) { myBook = NULL_BOOK; } } return myBook instanceof Book ? (Book)myBook : null; } 复制代码
这里又出现了Collection,我们知道其最终执行者是BookCollection,那我们就直接进入BookCollection的getBookByFile(path)方法看一下:
public DbBook getBookByFile(String path) { //熟悉的方法,根据路径生成不同的文件类型,我们知道SD卡中的文件最终会生成ZLPhysicalFile return getBookByFile(ZLFile.createFileByPath(path)); } private DbBook getBookByFile(ZLFile bookFile) { if (bookFile == null) { return null; } return getBookByFile(bookFile, PluginCollection.getPlugin(bookFile)); } private DbBook getBookByFile(ZLFile bookFile, final FormatPlugin plugin) { if (plugin == null || !isFormatActive(plugin)) { return null; } //忽略部分代码... } 复制代码
至此,我们可以知道,如果plugin为空的话,那么就会直接返回null,所以plugin不为空是至关重要的,那么我们就去看一下这个plugin是怎么获取的:
public FormatPlugin getPlugin(ZLFile file) { final FileType fileType = FileTypeCollection.Instance.typeForFile(file); final FormatPlugin plugin = getPlugin(fileType); if (plugin instanceof ExternalFormatPlugin) { return file == file.getPhysicalFile() ? plugin : null; } return plugin; } 复制代码
通过FileTypeCollection来获取当前file的文件类型,那么FileTypeCollection中定义了哪些可以识别的电子书文件类型呢?可以从其构造方法中看出:
private FileTypeCollection() { //fb2 addType(new FileTypeFB2()); //epub oebzip opf addType(new FileTypeEpub()); //mobi azw azw3 addType(new FileTypeMobipocket()); //html htm addType(new FileTypeHtml()); //txt addType(new SimpleFileType("txt", "txt", MimeType.TYPES_TXT)); //rtf addType(new SimpleFileType("RTF", "rtf", MimeType.TYPES_RTF)); //pdf addType(new SimpleFileType("PDF", "pdf", MimeType.TYPES_PDF)); //djvu djv addType(new FileTypeDjVu()); //cbz cbr addType(new FileTypeCBZ()); //zip addType(new SimpleFileType("ZIP archive", "zip", Collections.singletonList(MimeType.APP_ZIP))); //msdoc addType(new SimpleFileType("msdoc", "doc", MimeType.TYPES_DOC)); } 复制代码
FileTypeCollection是怎么根据ZLFile获取其文件类型的呢?拿epub格式为例,其实现在FileTypeEpub类中:
@Override public boolean acceptsFile(ZLFile file) { //获取文件扩展名 final String extension = file.getExtension(); return //比对扩展名 "epub".equalsIgnoreCase(extension) || "oebzip".equalsIgnoreCase(extension) || ("opf".equalsIgnoreCase(extension) && file != file.getPhysicalFile()); } 复制代码
很明显,是根据文件扩展名来获取文件类型,如果能获取到文件类型FileType对象,则说明当前文件为可支持的电子书文件,进而调用后面的getPlugin(fileType),就能够取到对应的文件解析插件。这个时候我们再回头去看BookCollection的getBookByFile方法:
private DbBook getBookByFile(ZLFile bookFile, final FormatPlugin plugin) { //忽略部分代码... book = new DbBook(bookFile, plugin); //保存识别出来的book到数据库 saveBook(book); return book; } 复制代码
这里会生成DbBook,而且DbBook在生成的时候会通过插件plugin去读取Book中的内容:
DbBook(ZLFile file, FormatPlugin plugin) throws BookReadingException { this(-1, plugin.realBookFile(file), null, null, null); BookUtil.readMetainfo(this, plugin); mySaveState = SaveState.NotSaved; } static void readMetainfo(AbstractBook book, FormatPlugin plugin) throws BookReadingException { book.myEncoding = null; book.myLanguage = null; book.setTitle(null); book.myAuthors = null; book.myTags = null; book.mySeriesInfo = null; book.myUids = null; book.mySaveState = AbstractBook.SaveState.NotSaved; //读取book内容 plugin.readMetainfo(book); if (book.myUids == null || book.myUids.isEmpty()) { plugin.readUids(book); } //忽略部分代码... } 复制代码
plugin读取Book内容的方法,最终会调用native方法:
private native int readMetainfoNative(AbstractBook book); 复制代码
针对这部分内容,我们来做一个总结:
- 在生成子tree时, ZLFile.createFileByPath 会首先被调用,并且会根据对 文件路径 的判别生成对应的文件类型
- 如果路径为手机存储的路径时,会生成 ZLPhysicalFile
- ZLPhysicalFile在被创建时,为根据文件路径实例化 真实的File ,并且会根据文件的扩展名,设置当前文件对应的 archiveType
- LibraryActivity的Adapter在getView方法中,会根据 CoverManager 对当前tree的封面获取情况,来设置具体的图标
- CoverManager在获取当前tree的封面时,会触发Tree的 getBook 方法,来尝试获取当前路径对应的Book
- getBook方法会触发 getPlugin 方法,而getPlugin则是会判断 文件扩展名 ,当扩展名为支持的文件类型时,才会返回对应的文件解析插件getPlugin,可支持的文件类型定义再 FileTypeCollection
- 当文件类型为支持电子书格式时,最终会调用 new DbBook(bookFile, plugin) 生成book
- DbBook在初始化时会调用plugin的 readMetainfo 并最终调用 native 方法 readMetainfoNative 去读取book内容,如编码、语言、标题、作者、标签等等
五、打开电子书,终于进入了阅读页面
还记得LibraryActivity的方法:
@Override protected void onListItemClick(ListView listView, View view, int position, long rowId) { //忽略部分代码... final LibraryTree tree = (LibraryTree)getTreeAdapter().getItem(position); final Book book = tree.getBook(); if (book != null) { //显示图书信息 showBookInfo(book); } } private void showBookInfo(Book book) { final Intent intent = new Intent(getApplicationContext(), BookInfoActivity.class); FBReaderIntents.putBookExtra(intent, book); OrientationUtil.startActivity(this, intent); } 复制代码
进入图书信息Activity
废话不多说,直接看阅读按钮:
setupButton(R.id.book_info_button_open, "openBook", new View.OnClickListener() { public void onClick(View view) { if (myDontReloadBook) { finish(); } else { //阅读电子书,传递参数为book FBReader.openBookActivity(BookInfoActivity.this, myBook, null); } } }); 复制代码
终于,进入了阅读页面!
当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。
以上所述就是小编给大家介绍的《开源电子书项目FBReader初探(三)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 开源电子书项目FBReader初探(一)
- 开源电子书项目FBReader初探(二)
- 开源电子书项目FBReader初探(四)
- 开源电子书项目FBReader初探(五)
- 开源电子书项目FBReader初探(六)
- 初探 Google 开源的 Python 命令行库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML5从入门到精通
明日科技 / 清华大学出版社 / 2012-9 / 59.80元
《HTML5从入门到精通》系统、全面地讲解了HTML语言及其最新版本HTML5的新功能与新特性,技术新颖实用。书中所有知识点均结合实例进行讲解,方便读者动手实践。同时在每章的最后还设置了习题,通过这些习题可以对本章学到的知识进行巩固。《HTML5从入门到精通》不仅能够使读者系统而全面地学习理论知识,还能满足读者充分实践的需求。一起来看看 《HTML5从入门到精通》 这本书的介绍吧!