开源电子书项目FBReader初探(三)

栏目: IOS · Android · 发布时间: 5年前

内容简介:通过上一篇分析,我们已经知道如何响应并打开菜单,而且菜单中第一项是打开本地书柜,这一篇我们就以此为入口,去探究FBReader的书柜是怎么实现,以及是如何分辨一本书并且能打开一本书的。打开本地书柜action:ShowLibraryAction,直接看其run方法:该Activity为ListActivity:

通过上一篇分析,我们已经知道如何响应并打开菜单,而且菜单中第一项是打开本地书柜,这一篇我们就以此为入口,去探究FBReader的书柜是怎么实现,以及是如何分辨一本书并且能打开一本书的。

一、打开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:

开源电子书项目FBReader初探(三)

查看其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的创建:

开源电子书项目FBReader初探(三)
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又是怎么一回事呢?我们接下来就一步步去弄清这些问题。

先来看一下它的继承关系图:

开源电子书项目FBReader初探(三)

由于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:

开源电子书项目FBReader初探(三)

其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是如何识别的呢?

开源电子书项目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

开源电子书项目FBReader初探(三)

废话不多说,直接看阅读按钮:

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初探(三)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Grails权威指南

Grails权威指南

瑞切 / 张若飞 / 电子工业 / 2007-11 / 49.80元

《Grails权威指南》译自由Grails项目负责人Graeme Keith Rocher编写的《The Definitive Guide to Grails》,着重介绍了如何在Grails框架下使用Groovy语言进行敏捷的Web开发。本书详细讲解Grails开发的全部过程,包括项目构架、控制器与视图、与关系数据库之间的ORM映射,以及与Ajax和Java平台的无缝集成。同时该书也揭示了Grai......一起来看看 《Grails权威指南》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具