Android原生PDF功能實(shí)現(xiàn):PDF閱讀代芜、PDF頁面跳轉(zhuǎn)、PDF手勢伸縮浓利、PDF目錄樹挤庇、PDF預(yù)覽縮略圖

PDF Demo 效果

1、背景

近期贷掖,公司希望實(shí)現(xiàn)安卓原生端的PDF功能嫡秕,要求:高效、實(shí)用苹威。

經(jīng)過兩天的調(diào)研昆咽、編碼,實(shí)現(xiàn)了一個簡單Demo,如上圖所示掷酗。
關(guān)于安卓原生端的PDF功能實(shí)現(xiàn)调违,技術(shù)點(diǎn)還是很多的,為了咱們安卓開發(fā)的同學(xué)少走彎路泻轰,通過此文章技肩,簡單講解下Demo的實(shí)現(xiàn)原理和主要技術(shù)點(diǎn),并附上源碼浮声。

2虚婿、安卓PDF現(xiàn)狀

目前,PDF功能仍然是安卓的一個短板泳挥,不像iOS然痊,有官方強(qiáng)大的PDF Kit可供集成。
不過羡洁,安卓也有一些主流的方案玷过,不過各有優(yōu)缺點(diǎn):

1、google doc 在線閱讀筑煮,基于webview,國內(nèi)需翻墻訪問(不可行)
2粤蝎、跳轉(zhuǎn)設(shè)備中默認(rèn)pdf app打開真仲,前提需要手機(jī)安裝了pdf 軟件(可按需選擇)
3、內(nèi)置 android-pdfview初澎,基于原生native, apk增加約15~20M(可行秸应,不過安裝包有點(diǎn)大)
4、內(nèi)置 mupdf碑宴,基于原生native, 集成有點(diǎn)麻煩软啼,增加約9M(可行,不過安裝包稍有點(diǎn)大)
5延柠、內(nèi)置 pdf.js祸挪,功能豐富,apk增加5M(基于Webview,性能低贞间,js實(shí)現(xiàn)贿条,功能定制復(fù)雜)
6、使用x5內(nèi)核增热,需要客戶端完全使用x5內(nèi)核(基于Webview,性能低整以,不能定制功能)

查閱官方資料,這些方案雖然能實(shí)現(xiàn)基本的PDF閱讀功能峻仇,但是多數(shù)方案公黑,集成過程較復(fù)雜,且性能低下,容易內(nèi)存溢出造成App閃退凡蚜。

3奠骄、方案選擇

經(jīng)過對各方案的反復(fù)比對,本次實(shí)現(xiàn)PDF Demo番刊,決定使用:android-pdfview含鳞。
原因:

1、android-pdfview基于PDFium實(shí)現(xiàn)(PDFium是谷歌 + 福昕軟件的PDF開源項(xiàng)目)芹务;
2蝉绷、android-pdfview Github仍在維護(hù);
3枣抱、android-pdfview Github獲得的星星較多熔吗;
4、客戶端集成較方便佳晶;

問題分析:
運(yùn)行android-pdfview官方demo桅狠,問題也很多:

1、僅實(shí)現(xiàn)了pdf滑動閱讀轿秧、手勢伸縮的功能中跌;
2、缺少pdf目錄樹菇篡、縮略圖等功能漩符;
3、安裝包過大驱还;
4嗜暴、UI不美觀;
5议蟆、內(nèi)存問題闷沥;
6、其他...

不過咐容,不用擔(dān)心舆逃,解決了這些問題不就沒有問題了嘛胜卤,哈尚揣、哈呜呐、哈(笑聲有點(diǎn)勉強(qiáng)哈)

下面趁仙,咱們開始實(shí)現(xiàn)Demo吧物臂。

4骄蝇、Demo設(shè)計(jì)

4.1靡努、工程結(jié)構(gòu)

在設(shè)計(jì)之前扭仁,應(yīng)明確Demo的實(shí)現(xiàn)目標(biāo):

1炊琉、android-pdfview已實(shí)現(xiàn)了pdfview展蒂,可用于閱讀pdf文件又活,手勢伸縮pdf頁面、跳轉(zhuǎn)pdf頁面锰悼,
   那么柳骄,咱們基于android-pdfview擴(kuò)展功能即可,功能包括:目錄樹箕般、縮略圖等耐薯;

2、擴(kuò)展的功能應(yīng)邏輯解耦丝里,不能影響android-pdfview代碼的可替換性
  (即:如果android-pdfview有新版本曲初,直接替換即可)

3、客戶端應(yīng)很方便集成
  (如:客戶端僅需要傳遞過來pdf文件杯聚,所有的加載臼婆、操作、內(nèi)存管理均無需關(guān)心)

Demo工程如何設(shè)計(jì):
下載android-pdfview最新源碼,可以看到共包含兩個Moudle:

android-pdf-viewer(最新源碼)
sample (示例app)

如果颁独,我們要接管封裝pdf的所有功能,讓sample只傳遞pdf文件即可坯墨,且不影響將來替換android-pdf-viewer的源碼骄瓣,那么我們創(chuàng)建一個modle即可,如下圖:

sample (依賴pdfui)
pdfui (依賴android-pdf-viewer)
android-pdf-viewer

4.2、PDF功能設(shè)計(jì)

為了便于用戶閱讀PDF妨托,應(yīng)該包含以下功能:
1兰伤、PDF閱讀(包含:手指滑動pdf頁面均澳、手勢伸縮頁面內(nèi)容找前、跳轉(zhuǎn)pdf指定頁面)
2五嫂、PDF目錄導(dǎo)航功能(包含:目錄展示躯枢、目錄節(jié)點(diǎn)折疊水慨、展開朝抖、點(diǎn)擊跳轉(zhuǎn)pdf頁面)
3、PDF縮略圖導(dǎo)航功能(包含:縮略圖展示谍珊、手指滑動、圖片緩存管理侮邀、點(diǎn)擊跳轉(zhuǎn)pdf頁面)

PDF功能代碼結(jié)構(gòu)

5、編碼之前绊茧,先解決安裝包過大的問題

反編譯Demo的安裝包,可以看到题暖,安裝包中默認(rèn)集成了各cpu平臺對應(yīng)的so庫文件捉超,安裝包過大的原因也就在這兒拼岳。其實(shí)正常項(xiàng)目開發(fā)中绝骚,對于各cpu平臺對應(yīng)的so庫的保留或舍棄粪牲,主要考慮cpu平臺兼容性、設(shè)備覆蓋率止剖。

通常情況下腺阳,僅保留armeabi-v7a可以兼容市面上絕大多數(shù)安卓設(shè)備,那么穿香,如何編譯時刪除其他的so呢亭引?

可在android gradle中配置,如下:

android{
......
 splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a' //如果想包含其他cpu平臺使用的so皮获,修改這里即可
        }
    }
}

重新編譯焙蚓,生成的安裝包,僅剩5M左右了洒宝。

注意:如果項(xiàng)目中還有其他so庫购公,要根據(jù)項(xiàng)目實(shí)際需求,認(rèn)真思考如何取舍了雁歌。

6君丁、實(shí)現(xiàn)PDF閱讀功能

很簡單,因?yàn)閍ndroid-pdf-viewer源碼中已經(jīng)實(shí)現(xiàn)了該功能将宪,我們寫一份精簡版的吧。

6.1橡庞、功能點(diǎn):

1较坛、可加載assets中的pdf文件
2、可加載uri類型的pdf文件(如果是線上的pdf文件扒最,可通過網(wǎng)絡(luò)庫先下載到本地丑勤,取其uri,本次Demo就不寫網(wǎng)絡(luò)下載了)
3吧趣、pdf的基本展示功能(使用android-pdf-viewer的控件實(shí)現(xiàn):PDFView)
4法竞、可跳轉(zhuǎn)至目錄頁面(目錄數(shù)據(jù)可通過intent直接傳遞過去)
5耙厚、可跳轉(zhuǎn)至預(yù)覽頁面(pdf文件信息可通過intent直接傳遞過去)
6、根據(jù)目錄頁面岔霸、預(yù)覽頁面帶回的頁碼薛躬,跳轉(zhuǎn)至指定的pdf頁面

PDF閱讀功能效果圖

6.2、代碼實(shí)現(xiàn)

重點(diǎn)內(nèi)容:

1呆细、PDFView控件的使用型宝;(比較簡單,詳見代碼)
2絮爷、如何從PDF文件中獲得目錄信息趴酣;(如何獲得目錄信息、什么時機(jī)獲取坑夯,詳見代碼)

PDF閱讀頁面的代碼:PDFActivity

/**
 * UI頁面:PDF閱讀
 * <p>
 * 主要功能:
 * 1岖寞、接收傳遞過來的pdf文件(包括assets中的文件名、文件uri)
 * 2柜蜈、顯示PDF文件
 * 3仗谆、接收目錄頁面、預(yù)覽頁面返回的PDF頁碼跨释,跳轉(zhuǎn)到指定的頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFActivity extends AppCompatActivity implements
        OnPageChangeListener,
        OnLoadCompleteListener,
        OnPageErrorListener {
    //PDF控件
    PDFView pdfView;
    //按鈕控件:返回胸私、目錄、縮略圖
    Button btn_back, btn_catalogue, btn_preview;
    //頁碼
    Integer pageNumber = 0;
    //PDF目錄集合
    List<TreeNodeData> catelogues;

    //pdf文件名(限:assets里的文件)
    String assetsFileName;
    //pdf文件uri
    Uri uri;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//設(shè)置沉浸式
        setContentView(R.layout.activity_pdf);

        initView();//初始化view
        setEvent();//設(shè)置事件
        loadPdf();//加載PDF文件
    }

    /**
     * 初始化view
     */
    private void initView() {
        pdfView = findViewById(R.id.pdfView);
        btn_back = findViewById(R.id.btn_back);
        btn_catalogue = findViewById(R.id.btn_catalogue);
        btn_preview = findViewById(R.id.btn_preview);
    }

    /**
     * 設(shè)置事件
     */
    private void setEvent() {
        //返回
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFActivity.this.finish();
            }
        });
        //跳轉(zhuǎn)目錄頁面
        btn_catalogue.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(PDFActivity.this, PDFCatelogueActivity.class);
                intent.putExtra("catelogues", (Serializable) catelogues);
                PDFActivity.this.startActivityForResult(intent, 200);
            }
        });
        //跳轉(zhuǎn)縮略圖頁面
        btn_preview.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(PDFActivity.this, PDFPreviewActivity.class);
                intent.putExtra("AssetsPdf", assetsFileName);
                intent.setData(uri);
                PDFActivity.this.startActivityForResult(intent, 201);
            }
        });
    }

    /**
     * 加載PDF文件
     */
    private void loadPdf() {
        Intent intent = getIntent();
        if (intent != null) {
            assetsFileName = intent.getStringExtra("AssetsPdf");
            if (assetsFileName != null) {
                displayFromAssets(assetsFileName);
            } else {
                uri = intent.getData();
                if (uri != null) {
                    displayFromUri(uri);
                }
            }
        }
    }

    /**
     * 基于assets顯示 PDF 文件
     *
     * @param fileName 文件名稱
     */
    private void displayFromAssets(String fileName) {
        pdfView.fromAsset(fileName)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // 單位 dp
                .onPageError(this)
                .pageFitPolicy(FitPolicy.BOTH)
                .load();
    }

    /**
     * 基于uri顯示 PDF 文件
     *
     * @param uri 文件路徑
     */
    private void displayFromUri(Uri uri) {
        pdfView.fromUri(uri)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // 單位 dp
                .onPageError(this)
                .load();
    }

    /**
     * 當(dāng)成功加載PDF:
     * 1鳖谈、可獲取PDF的目錄信息
     *
     * @param nbPages the number of pages in this PDF file
     */
    @Override
    public void loadComplete(int nbPages) {
        //獲得文檔書簽信息
        List<PdfDocument.Bookmark> bookmarks = pdfView.getTableOfContents();
        if (catelogues != null) {
            catelogues.clear();
        } else {
            catelogues = new ArrayList<>();
        }
        //將bookmark轉(zhuǎn)為目錄數(shù)據(jù)集合
        bookmarkToCatelogues(catelogues, bookmarks, 1);
    }

    /**
     * 將bookmark轉(zhuǎn)為目錄數(shù)據(jù)集合(遞歸)
     *
     * @param catelogues 目錄數(shù)據(jù)集合
     * @param bookmarks  書簽數(shù)據(jù)
     * @param level      目錄樹級別(用于控制樹節(jié)點(diǎn)位置偏移)
     */
    private void bookmarkToCatelogues(List<TreeNodeData> catelogues, List<PdfDocument.Bookmark> bookmarks, int level) {
        for (PdfDocument.Bookmark bookmark : bookmarks) {
            TreeNodeData nodeData = new TreeNodeData();
            nodeData.setName(bookmark.getTitle());
            nodeData.setPageNum((int) bookmark.getPageIdx());
            nodeData.setTreeLevel(level);
            nodeData.setExpanded(false);
            catelogues.add(nodeData);
            if (bookmark.getChildren() != null && bookmark.getChildren().size() > 0) {
                List<TreeNodeData> treeNodeDatas = new ArrayList<>();
                nodeData.setSubset(treeNodeDatas);
                bookmarkToCatelogues(treeNodeDatas, bookmark.getChildren(), level + 1);
            }
        }
    }

    @Override
    public void onPageChanged(int page, int pageCount) {
        pageNumber = page;
    }

    @Override
    public void onPageError(int page, Throwable t) {
    }

    /**
     * 從縮略圖岁疼、目錄頁面帶回頁碼,跳轉(zhuǎn)到指定PDF頁面
     *
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            int pageNum = data.getIntExtra("pageNum", 0);
            if (pageNum > 0) {
                pdfView.jumpTo(pageNum);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //是否內(nèi)存
        if (pdfView != null) {
            pdfView.recycle();
        }
    }
}

PDF閱讀頁面的布局文件:activity_pdf.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="10dp"/>

        <Button
            android:id="@+id/btn_catalogue"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="目錄"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_alignParentRight="true"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginRight="10dp"/>

        <Button
            android:id="@+id/btn_preview"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="預(yù)覽"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_toLeftOf="@+id/btn_catalogue"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginRight="10dp"/>
    </RelativeLayout>

    <com.github.barteksc.pdfviewer.PDFView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top"/>

</RelativeLayout>

7缆娃、PDF目錄樹的實(shí)現(xiàn)

目錄樹的數(shù)據(jù)(目錄名稱捷绒、頁碼...),已在上個頁面獲取了贯要,所以此頁面只需考慮目錄樹控件的實(shí)現(xiàn)暖侨。

注意:之所以沒在這個頁面單獨(dú)獲取目錄樹的數(shù)據(jù),主要考慮到android-pdfview崇渗、pdfium內(nèi)存占用太大了字逗,不想再次創(chuàng)建Pdf的相關(guān)對象。

7.1宅广、PDF目錄樹效果圖

7.2葫掉、樹形控件如何實(shí)現(xiàn)?

安卓默認(rèn)沒有樹形控件跟狱,不過我們可以使用RecyclerView或ListView實(shí)現(xiàn)俭厚。
如上圖所示:

列表每一行為一條目錄數(shù)據(jù),主要包括:名稱驶臊、頁碼挪挤;
如果有子目錄叼丑,則出現(xiàn)箭頭圖片,該項(xiàng)可折疊扛门、展開鸠信,箭頭方向隨之改變;
子目錄的名稱文本隨目錄樹級別遞增向右偏移尖飞;

當(dāng)前Demo實(shí)現(xiàn)方式為RecyclerView症副,應(yīng)該如何實(shí)現(xiàn)上面的效果?
可在adapter中處理頁面效果政基、事件效果:
1贞铣、列表項(xiàng)內(nèi)容展示

1、使用垂直線性布局管理器沮明;
2辕坝、每個item包含:箭頭圖片(如果有子目錄,則顯示)荐健、目錄名稱文本酱畅、頁碼文本;

2江场、折疊效果

1纺酸、控制adapter數(shù)據(jù)集合的內(nèi)容即可,如果某節(jié)點(diǎn)折疊了址否,就把對應(yīng)的子目錄數(shù)據(jù)刪除即可佑附,
反之音同,加上叽赊,再notifyDataSetChanged通知數(shù)據(jù)源改變取劫;
2惦银、除此之外殊校,還需有一個狀態(tài)來標(biāo)記當(dāng)前節(jié)點(diǎn)是展開還是折疊敬察,用于控制箭頭圖片方向的顯示虫给;

3、目錄文本向右偏移效果

可通過目錄樹層級 * 固定左側(cè)間隔(如: 20dp)视卢,然后為目錄的textview控件設(shè)置偏移即可酝掩;

目錄樹層級樹如何獲取? 可選方案:
1宙拉、遞歸集合自動獲染戴蕖(需要遍歷淹朋,效率低一點(diǎn)数尿,如果是可編輯的目錄結(jié)構(gòu)淘这,建議選擇)
2朦乏、創(chuàng)建數(shù)據(jù)的時候吃引,直接寫死(因當(dāng)前demo的PDF目錄結(jié)構(gòu)不會被編輯筹陵,所以直接選擇這個方案吧)

7.3、代碼實(shí)現(xiàn):

樹形控件的數(shù)據(jù)對象TreeNodeData:

/**
 * 樹形控件數(shù)據(jù)類(會用于頁面間傳輸镊尺,所以需實(shí)現(xiàn)Serializable 或 Parcelable)
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class TreeNodeData implements Serializable {
    //名稱
    private String name;
    //頁碼
    private int pageNum;
    //是否已展開(用于控制樹形節(jié)點(diǎn)圖片顯示朦佩,即箭頭朝向圖片)
    private boolean isExpanded;
    //展示級別(1級、2級...庐氮,用于控制樹形節(jié)點(diǎn)縮進(jìn)位置)
    private int treeLevel;
    //子集(用于加載子節(jié)點(diǎn)语稠,也用于判斷是否顯示箭頭圖片,如集合不為空弄砍,則顯示)
    private List<TreeNodeData> subset;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public boolean isExpanded() {
        return isExpanded;
    }

    public void setExpanded(boolean expanded) {
        isExpanded = expanded;
    }

    public int getTreeLevel() {
        return treeLevel;
    }

    public void setTreeLevel(int treeLevel) {
        this.treeLevel = treeLevel;
    }

    public List<TreeNodeData> getSubset() {
        return subset;
    }

    public void setSubset(List<TreeNodeData> subset) {
        this.subset = subset;
    }
}

樹形控件適配器 : TreeAdapter

/**
 * 樹形控件適配器
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class TreeAdapter extends RecyclerView.Adapter<TreeAdapter.TreeNodeViewHolder> {
    //上下文
    private Context context;
    //數(shù)據(jù)
    public List<TreeNodeData> data;
    //展示數(shù)據(jù)(由層級結(jié)構(gòu)改為平面結(jié)構(gòu))
    public List<TreeNodeData> displayData;
    //treelevel間隔(dp)
    private int maginLeft;
    //委托對象
    private TreeEvent delegate;

    /**
     * 構(gòu)造函數(shù)
     *
     * @param context 上下文
     * @param data    數(shù)據(jù)
     */
    public TreeAdapter(Context context, List<TreeNodeData> data) {
        this.context = context;
        this.data = data;
        maginLeft = UIUtils.dip2px(context, 20);
        displayData = new ArrayList<>();

        //數(shù)據(jù)轉(zhuǎn)為展示數(shù)據(jù)
        dataToDiaplayData(data);
    }

    /**
     * 數(shù)據(jù)轉(zhuǎn)為展示數(shù)據(jù)
     *
     * @param data 數(shù)據(jù)
     */
    private void dataToDiaplayData(List<TreeNodeData> data) {
        for (TreeNodeData nodeData : data) {
            displayData.add(nodeData);
            if (nodeData.isExpanded() && nodeData.getSubset() != null) {
                dataToDiaplayData(nodeData.getSubset());
            }
        }
    }

    /**
     * 數(shù)據(jù)集合轉(zhuǎn)為可顯示的集合
     */
    private void reDataToDiaplayData() {
        if (this.data == null || this.data.size() == 0) {
            return;
        }
        if(displayData == null){
            displayData = new ArrayList<>();
        }else{
            displayData.clear();
        }
        dataToDiaplayData(this.data);
        notifyDataSetChanged();
    }

    @Override
    public TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);
        return new TreeNodeViewHolder(view);
    }

    @Override
    public void onBindViewHolder(TreeNodeViewHolder holder, int position) {
        final TreeNodeData data = displayData.get(position);
        //設(shè)置圖片
        if (data.getSubset() != null) {
            holder.img.setVisibility(View.VISIBLE);
            if (data.isExpanded()) {
                holder.img.setImageResource(R.drawable.arrow_h);
            } else {
                holder.img.setImageResource(R.drawable.arrow_v);
            }
        } else {
            holder.img.setVisibility(View.INVISIBLE);
        }
        //設(shè)置圖片偏移位置
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.img.getLayoutParams();
        int ratio = data.getTreeLevel() <= 0? 0 : data.getTreeLevel()-1;
        params.setMargins(maginLeft * ratio, 0, 0, 0);
        holder.img.setLayoutParams(params);

        //顯示文本
        holder.title.setText(data.getName());
        holder.pageNum.setText(String.valueOf(data.getPageNum()));

        //圖片點(diǎn)擊事件
        holder.img.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //控制樹節(jié)點(diǎn)展開仙畦、折疊
                data.setExpanded(!data.isExpanded());
                //刷新數(shù)據(jù)源
                reDataToDiaplayData();
            }
        });
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //回調(diào)結(jié)果
                if(delegate!=null){
                    delegate.onSelectTreeNode(data);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return displayData.size();
    }

    /**
     * 定義RecyclerView的ViewHolder對象
     */
    class TreeNodeViewHolder extends RecyclerView.ViewHolder {
        ImageView img;
        TextView title;
        TextView pageNum;

        public TreeNodeViewHolder(View view) {
            super(view);
            img = view.findViewById(R.id.iv_arrow);
            title = view.findViewById(R.id.tv_title);
            pageNum = view.findViewById(R.id.tv_pagenum);
        }
    }

    /**
     * 接口:Tree事件
     */
    public interface TreeEvent{
        /**
         * 當(dāng)選擇了某tree節(jié)點(diǎn)
         * @param data tree節(jié)點(diǎn)數(shù)據(jù)
         */
        void onSelectTreeNode(TreeNodeData data);
    }

    /**
     * 設(shè)置Tree的事件
     * @param treeEvent Tree的事件對象
     */
    public void setTreeEvent(TreeEvent treeEvent){
        this.delegate = treeEvent;
    }
}

PDF目錄樹頁面:PDFCatelogueActivity

/**
 * UI頁面:PDF目錄
 * <p>
 * 1、用于顯示Pdf目錄信息
 * 2音婶、點(diǎn)擊tree item慨畸,帶回Pdf頁碼到前一個頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFCatelogueActivity extends AppCompatActivity implements TreeAdapter.TreeEvent {

    RecyclerView recyclerView;
    Button btn_back;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
        setContentView(R.layout.activity_catelogue);

        initView();//初始化控件
        setEvent();//設(shè)置事件
        loadData();//加載數(shù)據(jù)
    }

    /**
     * 初始化控件
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_tree);
    }

    /**
     * 設(shè)置事件
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFCatelogueActivity.this.finish();
            }
        });
    }

    /**
     * 加載數(shù)據(jù)
     */
    private void loadData() {
        //從intent中獲得傳遞的數(shù)據(jù)
        Intent intent = getIntent();
        List<TreeNodeData> catelogues = (List<TreeNodeData>) intent.getSerializableExtra("catelogues");

        //使用RecyclerView加載數(shù)據(jù)
        LinearLayoutManager llm = new LinearLayoutManager(this);
        llm.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(llm);
        TreeAdapter adapter = new TreeAdapter(this, catelogues);
        adapter.setTreeEvent(this);
        recyclerView.setAdapter(adapter);
    }


    /**
     * 點(diǎn)擊tree item,帶回Pdf頁碼到前一個頁面
     *
     * @param data tree節(jié)點(diǎn)數(shù)據(jù)
     */
    @Override
    public void onSelectTreeNode(TreeNodeData data) {
        Intent intent = new Intent();
        intent.putExtra("pageNum", data.getPageNum());
        setResult(Activity.RESULT_OK, intent);
        finish();
    }
}

PDF目錄樹的布局文件:activity_catelogue.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_marginLeft="10dp"
            android:layout_marginBottom="10dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textColor="#ffffff"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="15dp"
            android:text="目錄列表"
            android:textColor="#ffffff"
            android:textSize="18sp" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_tree"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />

</RelativeLayout>

8衣式、PDF預(yù)覽縮略圖

這個功能算是本Demo中最為復(fù)雜的一個了:

如何將PDF某頁面的內(nèi)容轉(zhuǎn)成圖片寸士?(默認(rèn)是無法從pdfview中獲得頁面圖片的)
如何減少圖片內(nèi)存的占用?(用戶可能快速滑動列表碴卧,實(shí)時讀取弱卡、顯示多張圖片)
如何優(yōu)化PDF預(yù)覽縮略圖列表的滑動體驗(yàn)?(圖片的獲取需要一定時間)
如何合理的及時釋放內(nèi)存占用住册?

8.1婶博、PDF預(yù)覽縮略圖列表的效果圖

8.2、功能分析

1界弧、如何將PDF某頁面的內(nèi)容轉(zhuǎn)成圖片凡蜻?

查看android-pdfview的源碼,無法通過PDFView控件獲得某頁面的圖片垢箕,所以只能分析pdfium sdk的API了划栓,如下圖:



pdfium的renderPageBitmap方法可以將頁面渲染成圖片,不過需要傳遞一系列參數(shù)条获,而且要小心OutOfMemoryError忠荞。

那么,我們需要在代碼中獲取或者創(chuàng)建PdfiumCore對象,調(diào)用該方法委煤,傳遞PdfDocument等參數(shù)堂油,當(dāng)bitmap使用完后,應(yīng)及時釋放掉碧绞。

2府框、如何減少內(nèi)存的占用?

內(nèi)存主要包括:
1讥邻、pdfium sdk加載pdf文件產(chǎn)生的內(nèi)存(我們無法優(yōu)化)
2迫靖、android-pdfview產(chǎn)生的內(nèi)存(如果有需要,可改其源碼)
3兴使、我們將pdf頁面轉(zhuǎn)為縮略圖系宜,而產(chǎn)生的內(nèi)存(必須優(yōu)化,否則发魄,容易o(hù)om)

3.1盹牧、當(dāng)PdfiumCore、PdfDocument不再使用時励幼,應(yīng)及時關(guān)閉汰寓;
3.2、當(dāng)縮略圖不再使用時赏淌,應(yīng)及時釋放踩寇;
3.3、可使用LruCache臨時緩存縮略圖六水,防止重復(fù)調(diào)用renderPageBitmap獲取圖片俺孙;
3.4、LruCache應(yīng)合理管控掷贾,當(dāng)預(yù)覽頁面關(guān)閉時睛榄,必須清空緩存,以釋放內(nèi)存想帅;
3.5场靴、創(chuàng)建圖片時,應(yīng)使用RGB_565港准,能節(jié)約內(nèi)存開銷(一個像素點(diǎn)旨剥,占2字節(jié))
3.6、創(chuàng)建圖片時浅缸,應(yīng)盡可能小的指定圖片的寬高轨帜,能看清就行(圖片占用的內(nèi)存 = 寬 * 高 * 一個像素點(diǎn)占的字節(jié)數(shù))

3、如何優(yōu)化PDF預(yù)覽縮略圖列表的滑動體驗(yàn)衩椒?

查看pdfium源碼蚌父,調(diào)用renderPageBitmap方法之前哮兰,還必須確保對應(yīng)的頁面已被打開,即調(diào)用了openPage方法苟弛。然而喝滞,這兩個方法都需要一定時間才能執(zhí)行完成的。

那么膏秫,如果我們直接在主線程中讓每個RecylerVew的item分別調(diào)用renderPageBitmap方法右遭,滑動列表時,會感覺特別卡缤削,所以該方法只能放在子線程中調(diào)用了狸演。

那么問題又來了,那么多子線程應(yīng)該如何管控僻他?

1、考慮CPU的占用腊尚,應(yīng)使用線程池控制子線程并發(fā)吨拗、阻塞;
2婿斥、考慮到用戶滑動速度劝篷,有可能某線程正執(zhí)行或者阻塞著呢,頁面已經(jīng)滑過去了民宿,那么娇妓,即使該線程加載出來了圖片,也無法顯示到列表中活鹰。所以對于RecyclerView已不可見的Item項(xiàng)對應(yīng)的線程哈恰,應(yīng)及時取消,防止做無用功志群,也節(jié)省了內(nèi)存和cpu開銷着绷。

8.3、功能實(shí)現(xiàn)

預(yù)覽縮略圖工具類:PreviewUtils

/**
 * 預(yù)覽縮略圖工具類
 *
 * 1锌云、pdf頁面轉(zhuǎn)為縮略圖
 * 2荠医、圖片緩存管理(僅保存到內(nèi)存,可使用LruCache桑涎,注意空間大小控制)
 * 3彬向、多線程管理(線程并發(fā)、阻塞攻冷、Future任務(wù)取消)
 *
 * 作者:齊行超
 * 日期:2019.08.08
 */
public class PreviewUtils {
    //圖片緩存管理
    private ImageCache imageCache;
    //單例
    private static PreviewUtils instance;
    //線程池
    ExecutorService executorService;
    //線程任務(wù)集合(可用于取消任務(wù))
    HashMap<String, Future> tasks;

    /**
     * 單例(僅主線程調(diào)用娃胆,無需做成線程安全的)
     *
     * @return PreviewUtils實(shí)例對象
     */
    public static PreviewUtils getInstance() {
        if (instance == null) {
            instance = new PreviewUtils();
        }
        return instance;
    }

    /**
     * 默認(rèn)構(gòu)造函數(shù)
     */
    private PreviewUtils() {
        //初始化圖片緩存管理對象
        imageCache = new ImageCache();
        //創(chuàng)建并發(fā)線程池(建議最大并發(fā)數(shù)大于1屏grid item的數(shù)量)
        executorService = Executors.newFixedThreadPool(20);
        //創(chuàng)建線程任務(wù)集合,用于取消線程執(zhí)行
        tasks = new HashMap<>();
    }

    /**
     * 從pdf文件中加載圖片
     *
     * @param context     上下文
     * @param imageView   圖片控件
     * @param pdfiumCore  pdf核心對象
     * @param pdfDocument pdf文檔對象
     * @param pdfName     pdf文件名稱
     * @param pageNum     pdf頁碼
     */
    public void loadBitmapFromPdf(final Context context,
                                  final ImageView imageView,
                                  final PdfiumCore pdfiumCore,
                                  final PdfDocument pdfDocument,
                                  final String pdfName,
                                  final int pageNum) {
        //判斷參數(shù)合法性
        if (imageView == null || pdfiumCore == null || pdfDocument == null || pageNum < 0) {
            return;
        }

        try {
            //緩存key
            final String keyPage = pdfName + pageNum;

            //為圖片控件設(shè)置標(biāo)記
            imageView.setTag(keyPage);

            Log.i("PreViewUtils", "加載pdf縮略圖:" + keyPage);

            //獲得imageview的尺寸(注意:如果使用正辰采溃控件尺寸缕棵,太占內(nèi)存了)
            /*int w = imageView.getMeasuredWidth();
            int h = imageView.getMeasuredHeight();
            final int reqWidth = w == 0 ? UIUtils.dip2px(context,100) : w;
            final int reqHeight = h == 0 ? UIUtils.dip2px(context,150) : h;*/

            //內(nèi)存大小= 圖片寬度 * 圖片高度 * 一個像素占的字節(jié)數(shù)(RGB_565 所占字節(jié):2)
            //注意:如果使用正撤醢啵控件尺寸,太占內(nèi)存了招驴,所以此處指定四縮略圖看著會模糊一點(diǎn)
            final int reqWidth = 100;
            final int reqHeight = 150;

            //從緩存中取圖片
            Bitmap bitmap = imageCache.getBitmapFromLruCache(keyPage);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }

            //使用線程池管理子線程
            Future future = executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //打開頁面(調(diào)用renderPageBitmap方法之前篙程,必須確保頁面已open,重要)
                    pdfiumCore.openPage(pdfDocument, pageNum);

                    //調(diào)用native方法别厘,將Pdf頁面渲染成圖片
                    final Bitmap bm = Bitmap.createBitmap(reqWidth, reqHeight, Bitmap.Config.RGB_565);
                    pdfiumCore.renderPageBitmap(pdfDocument, bm, pageNum, 0, 0, reqWidth, reqHeight);

                    //切回主線程虱饿,設(shè)置圖片
                    if (bm != null) {
                        //將圖片加入緩存
                        imageCache.addBitmapToLruCache(keyPage, bm);

                        //切回主線程加載圖片
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                if (imageView.getTag().toString().equals(keyPage)) {
                                    imageView.setImageBitmap(bm);
                                    Log.i("PreViewUtils", "加載pdf縮略圖:" + keyPage + "......已設(shè)置!触趴!");
                                }
                            }
                        });
                    }
                }
            });

            //將任務(wù)添加到集合
            tasks.put(keyPage, future);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 取消從pdf文件中加載圖片的任務(wù)
     *
     * @param keyPage 頁碼
     */
    public void cancelLoadBitmapFromPdf(String keyPage) {
        if (keyPage == null || !tasks.containsKey(keyPage)) {
            return;
        }
        try {
            Log.i("PreViewUtils", "取消加載pdf縮略圖:" + keyPage);
            Future future = tasks.get(keyPage);
            if (future != null) {
                future.cancel(true);
                Log.i("PreViewUtils", "取消加載pdf縮略圖:" + keyPage + "......已取消5ⅰ!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 獲得圖片緩存對象
     * @return 圖片緩存
     */
    public ImageCache getImageCache(){
        return imageCache;
    }

    /**
     * 圖片緩存管理
     */
   public class ImageCache {
        //圖片緩存
        private LruCache<String, Bitmap> lruCache;

        //構(gòu)造函數(shù)
        public ImageCache() {
            //初始化 lruCache
            //int maxMemory = (int) Runtime.getRuntime().maxMemory();
            //int cacheSize = maxMemory/8;
            int cacheSize = 1024 * 1024 * 30;//暫時設(shè)定30M
            lruCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }

        /**
         * 從緩存中取圖片
         * @param key 鍵
         * @return 圖片
         */
        public synchronized Bitmap getBitmapFromLruCache(String key) {
            if(lruCache!= null) {
                return lruCache.get(key);
            }
            return null;
        }

        /**
         * 向緩存中加圖片
         * @param key 鍵
         * @param bitmap 圖片
         */
        public synchronized void addBitmapToLruCache(String key, Bitmap bitmap) {
            if (getBitmapFromLruCache(key) == null) {
                if (lruCache!= null && bitmap != null)
                    lruCache.put(key, bitmap);
            }
        }

        /**
         * 清空緩存
         */
        public void clearCache(){
            if(lruCache!= null){
                lruCache.evictAll();
            }
        }
    }
}

grid列表適配器: GridAdapter

/**
 * grid列表適配器
 * 作者:齊行超
 * 日期:2019.08.08
 */
public class GridAdapter extends RecyclerView.Adapter<GridAdapter.GridViewHolder> {

    Context context;
    PdfiumCore pdfiumCore;
    PdfDocument pdfDocument;
    String pdfName;
    int totalPageNum;


    public GridAdapter(Context context, PdfiumCore pdfiumCore, PdfDocument pdfDocument, String pdfName, int totalPageNum) {
        this.context = context;
        this.pdfiumCore = pdfiumCore;
        this.pdfDocument = pdfDocument;
        this.pdfName = pdfName;
        this.totalPageNum = totalPageNum;
    }

    @Override
    public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
        return new GridViewHolder(view);
    }

    @Override
    public void onBindViewHolder(GridViewHolder holder, int position) {
        //設(shè)置PDF圖片
        final int pageNum = position;
        PreviewUtils.getInstance().loadBitmapFromPdf(context, holder.iv_page, pdfiumCore, pdfDocument, pdfName, pageNum);
        //設(shè)置PDF頁碼
        holder.tv_pagenum.setText(String.valueOf(position));
        //設(shè)置Grid事件
        holder.iv_page.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(delegate!=null){
                    delegate.onGridItemClick(pageNum);
                }
            }
        });
        return;
    }

    @Override
    public void onViewDetachedFromWindow(GridViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        try {
            //item不可見時冗懦,取消任務(wù)
            if(holder.iv_page!=null){
                PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
            }

            //item不可見時爽冕,釋放bitmap  (注意:本Demo使用了LruCache緩存來管理圖片,此處可注釋掉)
            /*Drawable drawable = holder.iv_page.getDrawable();
            if (drawable != null) {
                Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
                if (bitmap != null && !bitmap.isRecycled()) {
                    bitmap.recycle();
                    bitmap = null;
                    Log.i("PreViewUtils","銷毀pdf縮略圖:"+holder.iv_page.getTag().toString());
                }
            }*/
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    @Override
    public int getItemCount() {
        return totalPageNum;
    }

    class GridViewHolder extends RecyclerView.ViewHolder {
        ImageView iv_page;
        TextView tv_pagenum;

        public GridViewHolder(View itemView) {
            super(itemView);
            iv_page = itemView.findViewById(R.id.iv_page);
            tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
        }
    }

    /**
     * 接口:Grid事件
     */
    public interface GridEvent{
        /**
         * 當(dāng)選擇了某Grid項(xiàng)
         * @param position tree節(jié)點(diǎn)數(shù)據(jù)
         */
        void onGridItemClick(int position);
    }

    /**
     * 設(shè)置Grid事件
     * @param event Grid事件對象
     */
    public void setGridEvent(GridEvent event){
        this.delegate = event;
    }

    //Grid事件委托
    private GridEvent delegate;
}

PDF預(yù)覽縮略圖頁面:PDFPreviewActivity

/**
 * UI頁面:PDF預(yù)覽縮略圖(注意:此頁面披蕉,需多關(guān)注內(nèi)存管控)
 * <p>
 * 1颈畸、用于顯示Pdf縮略圖信息
 * 2、點(diǎn)擊縮略圖没讲,帶回Pdf頁碼到前一個頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFPreviewActivity extends AppCompatActivity implements GridAdapter.GridEvent {

    RecyclerView recyclerView;
    Button btn_back;
    PdfiumCore pdfiumCore;
    PdfDocument pdfDocument;
    String assetsFileName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
        setContentView(R.layout.activity_preview);

        initView();//初始化控件
        setEvent();
        loadData();
    }

    /**
     * 初始化控件
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_grid);
    }

    /**
     * 設(shè)置事件
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //回收內(nèi)存
                recycleMemory();

                PDFPreviewActivity.this.finish();
            }
        });

    }

    /**
     * 加載數(shù)據(jù)
     */
    private void loadData() {
        //加載pdf文件
        loadPdfFile();

        //獲得pdf總頁數(shù)
        int totalCount = pdfiumCore.getPageCount(pdfDocument);

        //綁定列表數(shù)據(jù)
        GridAdapter adapter = new GridAdapter(this, pdfiumCore, pdfDocument, assetsFileName, totalCount);
        adapter.setGridEvent(this);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        recyclerView.setAdapter(adapter);
    }

    /**
     * 加載pdf文件
     */
    private void loadPdfFile() {
        Intent intent = getIntent();
        if (intent != null) {
            assetsFileName = intent.getStringExtra("AssetsPdf");
            if (assetsFileName != null) {
                loadAssetsPdfFile(assetsFileName);
            } else {
                Uri uri = intent.getData();
                if (uri != null) {
                    loadUriPdfFile(uri);
                }
            }
        }
    }

    /**
     * 加載assets中的pdf文件
     */
    void loadAssetsPdfFile(String assetsFileName) {
        try {
            File f = FileUtils.fileFromAsset(this, assetsFileName);
            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
            pdfiumCore = new PdfiumCore(this);
            pdfDocument = pdfiumCore.newDocument(pfd);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 基于uri加載pdf文件
     */
    void loadUriPdfFile(Uri uri) {
        try {
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
            pdfiumCore = new PdfiumCore(this);
            pdfDocument = pdfiumCore.newDocument(pfd);
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    /**
     * 點(diǎn)擊縮略圖眯娱,帶回Pdf頁碼到前一個頁面
     *
     * @param position 頁碼
     */
    @Override
    public void onGridItemClick(int position) {
        //回收內(nèi)存
        recycleMemory();

        //返回前一個頁碼
        Intent intent = new Intent();
        intent.putExtra("pageNum", position);
        setResult(Activity.RESULT_OK, intent);
        finish();
    }

    /**
     * 回收內(nèi)存
     */
    private void recycleMemory(){
        //關(guān)閉pdf對象
        if (pdfiumCore != null && pdfDocument != null) {
            pdfiumCore.closeDocument(pdfDocument);
            pdfiumCore = null;
        }
        //清空圖片緩存,釋放內(nèi)存空間
        PreviewUtils.getInstance().getImageCache().clearCache();
    }
}

PDF預(yù)覽縮略圖頁面的布局文件:activity_preview.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_marginLeft="10dp"
            android:layout_marginBottom="10dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textColor="#ffffff"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="15dp"
            android:text="預(yù)覽縮略圖列表"
            android:textColor="#ffffff"
            android:textSize="18sp" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_grid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />
</RelativeLayout>

總結(jié)

文章中涉及的功能點(diǎn)較多爬凑,難點(diǎn)也較多徙缴,尤其是內(nèi)存管理、多線程管理嘁信,有不明白的建議下載Demo于样,多看下源碼。也歡迎留言咨詢吱抚,就是不一定有時間解答百宇,哈哈。秘豹。携御。。

如果希望把該demo用到項(xiàng)目中既绕,建議多測試一下啄刹,因?yàn)闀r間關(guān)系,我這邊僅做了基本測試凄贩。

Demo下載地址(github + 百度網(wǎng)盤):
https://github.com/qxcwanxss/AndroidPdfViewerDemo
https://pan.baidu.com/s/1_Py36avgQqcJ5C87BaS5Iw

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末誓军,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子疲扎,更是在濱河造成了極大的恐慌昵时,老刑警劉巖捷雕,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異壹甥,居然都是意外死亡救巷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門句柠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浦译,“玉大人,你說我怎么就攤上這事溯职【眩” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵谜酒,是天一觀的道長叹俏。 經(jīng)常有香客問我,道長僻族,這世上最難降的妖魔是什么她肯? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮鹰贵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘康嘉。我一直安慰自己碉输,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布亭珍。 她就那樣靜靜地躺著敷钾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肄梨。 梳的紋絲不亂的頭發(fā)上阻荒,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音众羡,去河邊找鬼侨赡。 笑死,一個胖子當(dāng)著我的面吹牛粱侣,可吹牛的內(nèi)容都是我干的羊壹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼齐婴,長吁一口氣:“原來是場噩夢啊……” “哼油猫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起柠偶,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤情妖,失蹤者是張志新(化名)和其女友劉穎睬关,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體毡证,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡电爹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了情竹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片藐不。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秦效,靈堂內(nèi)的尸體忽然破棺而出雏蛮,到底是詐尸還是另有隱情,我是刑警寧澤阱州,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布挑秉,位于F島的核電站,受9級特大地震影響苔货,放射性物質(zhì)發(fā)生泄漏犀概。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一夜惭、第九天 我趴在偏房一處隱蔽的房頂上張望姻灶。 院中可真熱鬧,春花似錦诈茧、人聲如沸产喉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曾沈。三九已至,卻和暖如春鸥昏,著一層夾襖步出監(jiān)牢的瞬間塞俱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工吏垮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留障涯,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓膳汪,卻偏偏與公主長得像像樊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子旅敷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353