制作安卓PDF閱讀器:三、實(shí)現(xiàn)文本選擇

在安卓平臺(tái),PDFium 早已開源,第三方閱讀器demo破數(shù)千贊,然而盡管相關(guān)的API已經(jīng)包含在在SDK的頭文件中,這么多年了文本選擇基本處于零開發(fā)狀態(tài)。

我為什么要開啟這個(gè)系列,努力試著從源頭開始,用 PDFium 制作一款閱讀器?有人喜歡問這個(gè)做了有什么用,這個(gè)是唯一的嗎?

當(dāng)然不是唯一的,底層技術(shù)更不是我的。不過我認(rèn)為在維護(hù)者的推動(dòng)下,PDFium 越來越完善,功能越來越多,不真正拿來做些什么實(shí)在是可惜了。另一個(gè)重要原因則是,其他APP要么臃腫或者簡陋,要么用著磕手、滑動(dòng)卡頓、誤觸頻發(fā),而且大多還不免費(fèi)。( 更正,近年來倒是多了好多免費(fèi)的PDF閱讀器 )

目標(biāo)期望:

  • 滑動(dòng)不卡。
  • 文本選擇媲美Opera瀏覽器。
  • 擁有超卓的文本操作體驗(yàn),查詞典、段落翻譯、文本分享等更加方便。

項(xiàng)目地址:https://github.com/KnIfER/PolymPic

一、處理超鏈接

熱身運(yùn)動(dòng):當(dāng)檢測(cè)到單擊( GestureDetector )時(shí),若點(diǎn)擊處存在超鏈接,則打印出超鏈接的對(duì)象。

頭文件:fpdf_doc.h

  1. 獲取點(diǎn)擊處的超鏈接
JNI_FUNC(jlong, PdfiumCore, nativeGetLinkAtCoord)(JNI_ARGS, jlong pagePtr, jdouble width, jdouble height, jdouble posX, jdouble posY){
    double px, py;
    FPDF_DeviceToPage((FPDF_PAGE)pagePtr, 0, 0, width, height, 0, posX, posY, &px, &py);
    return (jlong)FPDFLink_GetLinkAtPoint((FPDF_PAGE)pagePtr, px, py);
}

需要將屏幕坐標(biāo)轉(zhuǎn)換為頁面坐標(biāo),然后再次在native層轉(zhuǎn)換為所謂的user space、page space。別問我那是啥我也不知道。不過在論壇提問后,有人替我指出了相關(guān)文檔所在,有時(shí)間去看看!

"User space" is defined in section 8.3.2.3 of the PDF 32000-1:2008 specification.

屏幕坐標(biāo):[event.getX(), event.getY()]
頁面坐標(biāo):先前提過將整本PDF當(dāng)作一張超級(jí)大圖,subsampling-scale-imageview 有一系列的 viewToSource 坐標(biāo)轉(zhuǎn)換方法。屏幕轉(zhuǎn)換得到 source 坐標(biāo)后,減去點(diǎn)擊頁面的左上角坐標(biāo),就是頁面坐標(biāo)。

原始頁面坐標(biāo)需用 FPDF_DeviceToPage 再次轉(zhuǎn)換,才能傳給FPDFLink_GetLinkAtPoint,獲取坐標(biāo)處的鏈接指針。

  1. 鏈接指針不為空時(shí),可以提取超鏈接對(duì)象。
JNI_FUNC(jstring, PdfiumCore, nativeGetLinkTarget)(JNI_ARGS, jlong docPtr, jlong linkPtr){
    DocumentFile *doc = reinterpret_cast<DocumentFile*>(docPtr);
    FPDF_LINK link = reinterpret_cast<FPDF_LINK>(linkPtr);
    FPDF_DEST dest = FPDFLink_GetDest(doc->pdfDocument, link);
    if (dest != NULL) {
        int pageIdx = FPDFDest_GetDestPageIndex(doc->pdfDocument, dest);
        char buffer[16]={0};
        buffer[0]='@';
        sprintf(buffer+1,"%d",pageIdx);
        return env->NewStringUTF(buffer);
    }
    FPDF_ACTION action = FPDFLink_GetAction(link);
    if (action == NULL) {
        return NULL;
    }
    size_t bufferLen = FPDFAction_GetURIPath(doc->pdfDocument, action, NULL, 0);
    if (bufferLen <= 0) {
        return NULL;
    }
    std::string uri;
    FPDFAction_GetURIPath(doc->pdfDocument, action, WriteInto(&uri, bufferLen), bufferLen);
    return env->NewStringUTF(uri.c_str());
}

超鏈接對(duì)象統(tǒng)一返回字符串,可以是Uri地址,也可以是頁碼@頁碼。

二、在單擊處獲取一個(gè)單詞

熱身運(yùn)動(dòng)2:在單擊處獲取一個(gè)英文單詞或者漢語詞組,需要用到安卓的 BreakIterator。

頭文件:fpdf_text.h

首先實(shí)現(xiàn) nativeGetCharIndexAtCoord 方法,獲取單擊附近的文字索引,需進(jìn)行同樣的坐標(biāo)轉(zhuǎn)換。

JNI_FUNC(jint, PdfiumCore, nativeGetCharIndexAtCoord)(JNI_ARGS, jlong pagePtr, jdouble width, jdouble height, jlong textPtr, jdouble posX, jdouble posY, jdouble tolX, jdouble tolY){
    double px, py;
    FPDF_DeviceToPage((FPDF_PAGE)pagePtr, 0, 0, width, height, 0, posX, posY, &px, &py);
    return FPDFText_GetCharIndexAtPos((FPDF_TEXTPAGE)textPtr, px, py, tolX, tolY);
}

若返回的文字index大于等于零,則此 index 指向該頁面全部文本當(dāng)中的一個(gè)字符。全部文本用 FPDFText_GetText 獲?。▽?shí)現(xiàn) nativeGetText):

JNI_FUNC(jstring, PdfiumCore, nativeGetText)(JNI_ARGS, jlong textPtr) {
    int len = FPDFText_CountChars((FPDF_TEXTPAGE)textPtr);
    //unsigned short* buffer = malloc(len*sizeof(unsigned short));
    unsigned short* buffer = new unsigned short[len+1];
    FPDFText_GetText((FPDF_TEXTPAGE)textPtr, 0, len, buffer);
    jstring ret = env->NewString(buffer, len);
    delete []buffer;
    return ret;
}

接下來就可以用 BreakIterator 分詞了:

...   @@@ public void prepareText()

    allText = pdfiumCore.nativeGetText(tid);
    if(pageBreakIterator==null) {
        pageBreakIterator = new BreakIteratorHelper();
    }
    pageBreakIterator.setText(allText);

...   @@@ public String getWordAtPos(float posX, float posY)

    int charIdx = pdfiumCore.nativeGetCharIndexAtCoord(pid.get(), size.getWidth(), size.getHeight(), tid
            , posX, posY, 10.0, 10.0);
    String ret=null;
    
    if(charIdx>=0) {
        int ed=pageBreakIterator.following(charIdx);
        int st=pageBreakIterator.previous();
        獲得的單詞就是 allText.substring(st, ed)
    }
...

三、實(shí)現(xiàn)文本選擇

1. 繪制選框

與繪制PDF本身差不多,不過 bitmap 換成 rect 而已。用到的API依次是FPDFText_CountRects、FPDFText_GetRect。

直接將選框覆蓋繪制在前。若要繪制在后面的背景上,就需要三層透明視圖了,那么加載鋪塊和縮略圖的時(shí)候就要用透明色清空 bitmap,頁面的白色背景等也需要另外繪制(Google PDF Viewer應(yīng)該就是這樣,還給背景加了陰影)。這些較為復(fù)雜,到時(shí)候再說。

有個(gè)問題可能需要解決:同一行的選框,部分沒有合并。

都是小事兒,暫時(shí)不在這上面花時(shí)間。

2. 繪制控點(diǎn)

之前做過類似的事情,將普通 TextView 自帶的文本選擇功能禁用了,然后用API自己做出一個(gè)來,包括單擊選詞,長按托選,放大鏡等等。所以相關(guān)的內(nèi)容還是熟悉的。

繪制 Selection Handle 可以用 AppCompat 支持庫中的圖標(biāo)資源:

        handleLeft=getResources().getDrawable(R.drawable.abc_text_select_handle_left_mtrl_dark);
        handleRight=getResources().getDrawable(R.drawable.abc_text_select_handle_right_mtrl_dark);

控點(diǎn)的觸控操作也很簡單,在 Action_Down 中檢測(cè)落點(diǎn)是否在其中一個(gè) handle 內(nèi)。若是,則在 Action_Move 中一邊移動(dòng)該 handle,一邊檢測(cè)新的字符索引,作為文本選擇的新邊界。

由于PDF的復(fù)雜性,頁面上的字符索引可能間雜排列,比如頭一段開頭是100,下一段開頭50,再下一段150。這就造成先前簡單的選擇系統(tǒng)“失效”了:

沒什么解決方案,API 就這么點(diǎn)。而且,靜讀天下、Google PDF 查看器都是這樣的,唯有 ezpdfreader 沒有這個(gè)問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 版本記錄 前言 今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個(gè)框架就PDFKit,看了下才看見是iOS11.0新添加的框架...
    刀客傳奇閱讀 8,897評(píng)論 0 15
  • 久違的晴天,家長會(huì)。 家長大會(huì)開好到教室時(shí),離放學(xué)已經(jīng)沒多少時(shí)間了。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,794評(píng)論 16 22
  • 創(chuàng)業(yè)是很多人的夢(mèng)想,多少人為了理想和不甘選擇了創(chuàng)業(yè)來實(shí)現(xiàn)自我價(jià)值,我就是其中一個(gè)。 創(chuàng)業(yè)后,我由女人變成了超人,什...
    亦寶寶閱讀 1,993評(píng)論 4 1
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    余生動(dòng)聽閱讀 10,803評(píng)論 0 11
  • 可愛進(jìn)取,孤獨(dú)成精。努力飛翔,天堂翱翔。戰(zhàn)爭美好,孤獨(dú)進(jìn)取。膽大飛翔,成就輝煌。努力進(jìn)取,遙望,和諧家園??蓯塾巫?..
    趙原野閱讀 3,457評(píng)論 1 1

友情鏈接更多精彩內(nèi)容