開源電子書專案FBReader初探(五)
FBReader如何讀取電子書內容,以及頁面繪製的方式是什麼
先來回顧一下上一節最後說到的點,新角色FBReaderApp呼叫了openBookInternal方法:
private synchronized void openBookInternal(final Book book, Bookmark bookmark, boolean force) {
//忽略部分程式碼..
Model = BookModel.createModel(book, plugin);
Collection.saveBook(book);
ZLTextHyphenator.Instance().load(book.getLanguage());
BookTextView.setModel(Model.getTextModel());
//忽略部分程式碼..
}
複製程式碼
一、BookModle生成過程中,都有哪些“不為人知的祕密”
上一篇,我們已經分析過,在BookModel.createModel生成BookModel時,針對於epub格式的檔案來說,最終會呼叫NativeFormatPlugin的readModelNative:
private native int readModelNative(BookModel model, String cacheDir);
複製程式碼
這裡有兩個引數BookModel和cacheDir,我們先來看看BookModel是怎麼生成的:
public static BookModel createModel(Book book, FormatPlugin plugin) throws BookReadingException {
if (plugin instanceof BuiltinFormatPlugin) {
final BookModel model = new BookModel(book);
((BuiltinFormatPlugin)plugin).readModel(model);
return model;
}
//忽略部分程式碼..
}
複製程式碼
直接new BookModel,並且將book裝入。再來看看cacheDir:
String tempDirectory = SystemInfo.tempDirectory();
複製程式碼
這個SystemInfo上一篇我們已經分析過,其實現為Paths.systemInfo(context)。是用來獲取一些路徑地址的。那麼這裡傳入的路徑是什麼?debug看一下:
傳入了一個路徑給native,取名cache,看來navtive在解析電子書時會生成快取?暫時把這個疑問放一邊,去看一下BookModel這個類:
有好多方法都是灰色的,證明在java程式碼中沒有地方呼叫這些程式碼,細看一下,這些都是一些set賦值操作,不免想到是否在native進行解析時會呼叫呢?經過debug後發現,的確在navtive解析電子書時,會呼叫這些操作賦值許多資料,這也解釋了上一篇最後關於BookModel解析前後內容存在差別的原因。這裡有三個方法,值得我們去關注一下:
1.initInternalHyperlinks——生成BookModel對應的儲存管理CachedCharStorage
public void initInternalHyperlinks(String directoryName, String fileExtension, int blocksNumber) {
myInternalHyperlinks = new CachedCharStorage(directoryName, fileExtension, blocksNumber);
}
CachedCharStorage.class
public CachedCharStorage(String directoryName, String fileExtension, int blocksNumber) {
myDirectoryName = directoryName + '/';
myFileExtension = '.' + fileExtension;
myArray.addAll(Collections.nCopies(blocksNumber, new WeakReference<char[]>(null)));
}
複製程式碼
引數名稱很清楚,檔案目錄、副檔名和blocksNumber。CachedCharStorage在構建時,會根據傳入的blocksNumber建立一個大小為blocksNumber集合,其它的暫時看來還不清楚有什麼用。debug看一下initInternalHyperlinks被呼叫時具體引數傳遞情況:
很明顯了,這個檔案路徑跟我們之前傳遞進去的路徑是一個路徑,副檔名是nlinks。看來native不只是解析,還會在解析的過程中生成快取檔案,而且快取檔案的存放地址就是我們傳入的地址。
2.createTextModel——初始化核心類ZLTextPlainModel
public ZLTextModel createTextModel(
String id, String language, int paragraphsNumber,
int[] entryIndices, int[] entryOffsets,
int[] paragraphLenghts, int[] textSizes, byte[] paragraphKinds,
String directoryName, String fileExtension, int blocksNumber
) {
return new ZLTextPlainModel(
id, language, paragraphsNumber,
entryIndices, entryOffsets,
paragraphLenghts, textSizes, paragraphKinds,
directoryName, fileExtension, blocksNumber, myImageMap, FontManager
);
}
ZLTextPlainModel.class
public ZLTextPlainModel(
String id,
String language,
int paragraphsNumber,
int[] entryIndices,
int[] entryOffsets,
int[] paragraphLengths,
int[] textSizes,
byte[] paragraphKinds,
String directoryName,
String fileExtension,
int blocksNumber,
Map<String,ZLImage> imageMap,
FontManager fontManager
) {
myId = id;
myLanguage = language;
myParagraphsNumber = paragraphsNumber;
myStartEntryIndices = entryIndices;
myStartEntryOffsets = entryOffsets;
myParagraphLengths = paragraphLengths;
myTextSizes = textSizes;
myParagraphKinds = paragraphKinds;
myStorage = new CachedCharStorage(directoryName, fileExtension, blocksNumber);
myImageMap = imageMap;
myFontManager = fontManager;
}
複製程式碼
這個引數個數就很多了,而且有些引數並不能看出來是做什麼的。但是不難發現這裡也有這麼三個引數:directoryName,fileExtension,blocksNumber。那麼這三個引數實際值又是什麼呢?還得需要debug看一下:
地址還是我們傳入的地址,但是這裡檔案型別變成了ncache,而且blocksNumber是12,我們知道CachedCharStorage會對應的建立一個長度為12的集合。
3.呼叫BookModel的setBookTextModel,將2建立的ZLTextPlainModel賦值給BookModel
public void setBookTextModel(ZLTextModel model) {
myBookTextModel = model;
}
複製程式碼
這裡debug可以知道,將第二步建立的ZLTextPlainModel賦值給了BookModel。
回到FBReaderApp的openBookInternal方法,我們將斷點放在BookModel.createModel之後的Collection.saveBook(book),當斷點到這裡時,進入手機,我們去看一下剛才路徑下面是否有我們之前猜測的native生成的快取檔案:
果然!這裡有.ncache和.nlinke檔案,而且個數分別為12和1。跟blocksNumber大小一致。
大膽的猜測一下,這個ncache是不是在native解析內容時,每達到一定大小(圖中128K)就會切分出來一個快取檔案,然後根據某些條件去讀取對應的快取檔案中的內容?
二、獲取頁面對應Bitmap並繪製到cavas上
在之前檢視FBReader的佈局檔案時,我們知道,其頁面中只有一個控制元件——ZLAndroidWidget。既然要看繪製,那不多說直入onDraw:
@Override
protected void onDraw(final Canvas canvas) {
final Context context = getContext();
if (context instanceof FBReader) {
//喚醒螢幕
((FBReader)context).createWakeLock();
} else {
System.err.println("A surprise: view's context is not an FBReader");
}
super.onDraw(canvas);
//final int w = getWidth();
//final int h = getMainAreaHeight();
myBitmapManager.setSize(getWidth(), getMainAreaHeight());
if (getAnimationProvider().inProgress()) {
onDrawInScrolling(canvas);
} else {
onDrawStatic(canvas);
ZLApplication.Instance().onRepaintFinished();
}
}
複製程式碼
這裡引出了myBitmapManager,看一下它是什麼,在哪定義的:
原來是BitmapManagerImpl,那就看一下setSize,是做啥了:
private final int SIZE = 2;
private final Bitmap[] myBitmaps = new Bitmap[SIZE];
void setSize(int w, int h) {
if (myWidth != w || myHeight != h) {
myWidth = w;
myHeight = h;
for (int i = 0; i < SIZE; ++i) {
myBitmaps[i] = null;
myIndexes[i] = null;
}
System.gc();
System.gc();
System.gc();
}
}
複製程式碼
很簡單,判斷、賦值和清空bitmap集合。之前傳遞過來的引數第一個是getWidth即為當前控制元件的寬度,但是第二個引數缺不是getHeight而是getMainAreaHeight:
private int getMainAreaHeight() {
final ZLView.FooterArea footer = ZLApplication.Instance().getCurrentView().getFooterArea();
return footer != null ? getHeight() - footer.getHeight() : getHeight();
}
複製程式碼
這裡資訊量比較大,我們分開來一個一個的看:
1.ZLApplication.Instance() 在FBReader的onCreate中我們已經分析過,是FBReaderApp例項
2.getCurrentView(),經過追溯能夠知道實際為FBView物件。
public final ZLView getCurrentView() {
return myView;
}
賦值方法
protected final void setView(ZLView view) {
if (view != null) {
myView = view;
//忽略部分程式碼...
}
}
FBReaderApp.class
public FBReaderApp(SystemInfo systemInfo, final IBookCollection<Book> collection) {
super(systemInfo);
//忽略部分程式碼...
BookTextView = new FBView(this);
}
private synchronized void openBookInternal(final Book book, Bookmark bookmark, boolean force) {
//忽略部分程式碼...
setView(BookTextView);
//忽略部分程式碼...
}
複製程式碼
3.getFooterArea:
FBView.class
@Override
public Footer getFooterArea() {
//根據ViewOptions中定義的footer型別,建立相應的footer
switch (myViewOptions.ScrollbarType.getValue()) {
case SCROLLBAR_SHOW_AS_FOOTER:
if (!(myFooter instanceof FooterNewStyle)) {
if (myFooter != null) {
myReader.removeTimerTask(myFooter.UpdateTask);
}
myFooter = new FooterNewStyle();
myReader.addTimerTask(myFooter.UpdateTask, 15000);
}
break;
case SCROLLBAR_SHOW_AS_FOOTER_OLD_STYLE:
if (!(myFooter instanceof FooterOldStyle)) {
if (myFooter != null) {
myReader.removeTimerTask(myFooter.UpdateTask);
}
myFooter = new FooterOldStyle();
myReader.addTimerTask(myFooter.UpdateTask, 15000);
}
break;
default:
if (myFooter != null) {
myReader.removeTimerTask(myFooter.UpdateTask);
myFooter = null;
}
break;
}
return myFooter;
}
private abstract class Footer implements FooterArea {
//忽略部分程式碼...
public int getHeight() {
//返回ViewOptions中設定的footer高度
return myViewOptions.FooterHeight.getValue();
}
//忽略部分程式碼...
}
複製程式碼
經過上面三步的分析,可以得出的結論是getMainAreaHeight方法獲取到的高度是ZLAndroidWidget的高度減去Footer的高度。那麼也就是說BitmapManager在建立bitmap時,的最大高度為去掉Footer區域後的高度:
public Bitmap getBitmap(ZLView.PageIndex index) {
//忽略部分程式碼...
myBitmaps[iIndex] = Bitmap.createBitmap(myWidth, myHeight, Bitmap.Config.RGB_565);
//忽略部分程式碼...
}
複製程式碼
我們再回到onDraw中,可以看到其中有一個判斷:
if (getAnimationProvider().inProgress()) {
onDrawInScrolling(canvas);
} else {
onDrawStatic(canvas);
ZLApplication.Instance().onRepaintFinished();
}
//獲取當前翻頁動畫
private AnimationProvider getAnimationProvider() {
final ZLView.Animation type = ZLApplication.Instance().getCurrentView().getAnimationType();
if (myAnimationProvider == null || myAnimationType != type) {
myAnimationType = type;
switch (type) {
case none:
myAnimationProvider = new NoneAnimationProvider(myBitmapManager);
break;
case curl:
myAnimationProvider = new CurlAnimationProvider(myBitmapManager);
break;
case slide:
myAnimationProvider = new SlideAnimationProvider(myBitmapManager);
break;
case slideOldStyle:
myAnimationProvider = new SlideOldStyleAnimationProvider(myBitmapManager);
break;
case shift:
myAnimationProvider = new ShiftAnimationProvider(myBitmapManager);
break;
}
}
return myAnimationProvider;
}
複製程式碼
那麼就是當翻頁動畫正在執行的時候,繪製呼叫onDrawInScrolling,如果動畫沒在執行,說明當前是靜止的狀態,繪製呼叫onDrawStatic。這裡我們先看onDrawStatic:
public final ExecutorService PrepareService = Executors.newSingleThreadExecutor();
private void onDrawStatic(final Canvas canvas) {
canvas.drawBitmap(myBitmapManager.getBitmap(ZLView.PageIndex.current), 0, 0, myPaint);
drawFooter(canvas, null);
post(new Runnable() {
public void run() {
PrepareService.execute(new Runnable() {
public void run() {
final ZLView view = ZLApplication.Instance().getCurrentView();
final ZLAndroidPaintContext context = new ZLAndroidPaintContext(
mySystemInfo,
canvas,
new ZLAndroidPaintContext.Geometry(
getWidth(),
getHeight(),
getWidth(),
getMainAreaHeight(),
0,
0
),
view.isScrollbarShown() ? getVerticalScrollbarWidth() : 0
);
view.preparePage(context, ZLView.PageIndex.next);
}
});
}
});
}
public interface ZLViewEnums {
public enum PageIndex {
previous, current, next;
//忽略部分程式碼...
}
//忽略部分程式碼...
}
private void drawFooter(Canvas canvas, AnimationProvider animator) {
final ZLView view = ZLApplication.Instance().getCurrentView();
final ZLView.FooterArea footer = view.getFooterArea();
//忽略部分程式碼...
if (myFooterBitmap == null) {
myFooterBitmap = Bitmap.createBitmap(getWidth(), footer.getHeight(), Bitmap.Config.RGB_565);
}
final ZLAndroidPaintContext context = new ZLAndroidPaintContext(
mySystemInfo,
new Canvas(myFooterBitmap),
new ZLAndroidPaintContext.Geometry(
getWidth(),
getHeight(),
getWidth(),
footer.getHeight(),
0,
getMainAreaHeight()
),
view.isScrollbarShown() ? getVerticalScrollbarWidth() : 0
);
footer.paint(context);
final int voffset = getHeight() - footer.getHeight();
if (animator != null) {
animator.drawFooterBitmap(canvas, myFooterBitmap, voffset);
} else {
//傳入的animator是null
canvas.drawBitmap(myFooterBitmap, 0, voffset, myPaint);
}
}
複製程式碼
這裡可以看出幹了三件事:
- 在(0,0)繪製一個bitmap,該bitmap是從BitmapManagerImpl中根據ZLView.PageIndex.current獲取的
- 建立一個寬度為控制元件寬度,高度為footer.getHeight()的bitmap,隨後呼叫當前型別footer的paint方法,在bitmap上繪製出要顯示的內容。隨後在(0,getHeight() - footer.getHeight())繪製該bitmap。
- 通過Executors去執行一個Runnable,其中傳遞引數ZLView.PageIndex為next
前兩部比較比較清晰,是繪製了兩個bitmap,那這兩個biamap分別是什麼呢?
debug看一下,第一個bitmap:
第二個bitmap:
再來看一下整個頁面的顯示效果:
額,手機截圖不是很全,但是已經能夠看出,最終結果是連個bitmap拼接後鋪滿了整個控制元件。而且從對上面整個過程的分析來看:FBReader繪製的時候,針對某一頁page,都會去獲取該頁page對應的bitmap,然後再繪製在cavas上。
三、滑動翻頁時的繪製
在翻頁動畫執行中,介面的顯示是這樣的(側滑翻頁):
在上面的分析過程中,已經知道如果當前翻頁動畫正在執行,那麼onDraw會呼叫onDrawInScrolling來繪製頁面內容:
private void onDrawInScrolling(Canvas canvas) {
//忽略部分程式碼...
final AnimationProvider animator = getAnimationProvider();//獲取當前動畫方式
//忽略部分程式碼...
animator.draw(canvas);//繪製頁面內容
//忽略部分程式碼...
}
複製程式碼
這裡我們就拿側滑翻頁動畫來分析:
AnimationProvider.class
public final void draw(Canvas canvas) {
//忽略部分程式碼...
drawInternal(canvas);
//忽略部分程式碼...
}
protected void drawBitmapFrom(Canvas canvas, int x, int y, Paint paint) {
myBitmapManager.drawBitmap(canvas, x, y, ZLViewEnums.PageIndex.current, paint);
}
protected void drawBitmapTo(Canvas canvas, int x, int y, Paint paint) {
myBitmapManager.drawBitmap(canvas, x, y, getPageToScrollTo(), paint);
}
public final ZLViewEnums.PageIndex getPageToScrollTo() {
//根據滑動時的角標,獲取下方顯示的是上一頁還是下一頁
return getPageToScrollTo(myEndX, myEndY);
}
SimpleAnimationProvider.class extends AnimationProvider
@Override
public ZLViewEnums.PageIndex getPageToScrollTo(int x, int y) {
if (myDirection == null) {
return ZLViewEnums.PageIndex.current;
}
//myDirection表示如何滑動是正向,即能滑到下一頁的滑動方向
switch (myDirection) {
case rightToLeft:
return myStartX < x ? ZLViewEnums.PageIndex.previous : ZLViewEnums.PageIndex.next;
case leftToRight:
return myStartX < x ? ZLViewEnums.PageIndex.next : ZLViewEnums.PageIndex.previous;
case up:
return myStartY < y ? ZLViewEnums.PageIndex.previous : ZLViewEnums.PageIndex.next;
case down:
return myStartY < y ? ZLViewEnums.PageIndex.next : ZLViewEnums.PageIndex.previous;
}
return ZLViewEnums.PageIndex.current;
}
SlideAnimationProvider.class extends SimpleAnimationProvider//側滑翻頁
@Override
protected void drawInternal(Canvas canvas) {
if (myDirection.IsHorizontal) {//水平方向翻頁
final int dX = myEndX - myStartX;
setDarkFilter(dX, myWidth);//下面一頁的半透明蒙層
drawBitmapTo(canvas, 0, 0, myDarkPaint);//繪製下面一頁
drawBitmapFrom(canvas, dX, 0, myPaint);//繪製正在滑動的一頁
drawShadowVertical(canvas, 0, myHeight, dX);//繪製分界線處的陰影
} else {//豎直翻頁
final int dY = myEndY - myStartY;
setDarkFilter(dY, myHeight);
drawBitmapTo(canvas, 0, 0, myDarkPaint);
drawBitmapFrom(canvas, 0, dY, myPaint);
drawShadowHorizontal(canvas, 0, myWidth, dY);
}
}
複製程式碼
這個地方,其實也比較簡單,原理就是根據滑動的方向和當前設定的翻頁方式(水平翻頁或豎直翻頁),來獲取底下的bitmap是上一頁內容還是下一頁內容。而當前跟隨手指滑動發生位置變化的bitmap,就是currentPage對應的bitmap。而且是在繪製的時候是根據橫向的滑動偏移dx,來確定canvas的繪製bitmap時的left,這樣隨著手指的移動,頁面也就“動”了起來。
當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。