Implementation of Android native PDF function

Keywords: Android xml github encoding

1, background

Recently, the company hopes to achieve Android native PDF function, requirements: efficient and practical.

After two days of research and coding, a simple Demo is implemented, as shown in the figure above.
There are still many technical points about the realization of PDF function in Android's native end. In order to avoid detours for our classmates who developed Android, this article briefly explains the implementation principle and main technical points of Demo, and attaches the source code.

2. The current status of Android PDF

At present, PDF functionality is still a short board for Android, unlike iOS, which has a powerful official PDF Kit for integration.
However, Android has some mainstream solutions, but each has its own advantages and disadvantages:

1. google doc online reading, based on webview, the domestic need to climb over the wall access (not feasible)
2. The default pdf app opens in jump devices, provided that the mobile phone is equipped with pdf software (optional on demand)
3. Built-in android-pdfview. Based on native, the APK increases by about 15-20M (feasible, but the installation package is a little big)
4. Built-in mupdf, based on native, integration is a bit cumbersome, increase about 9M (feasible, but installation package is a little big)
5. Built-in pdf.js, rich in functions, 5M apk (based on Webview, low performance, JS implementation, complex function customization)
6. Using the x5 kernel, the client needs to use the x5 kernel completely (based on Webview, low performance, can not customize functions)

Referring to the official information, although these schemes can achieve the basic PDF reading function, most of them have complex integration process and low performance, and are prone to memory overflow causing App flip.

3. Scheme Selection

After repeated comparisons of various schemes, PDF Demo was implemented and decided to use: android-pdfview.
Reason:

1. android-pdfview is based on PDFium (PDFium is an open source PDF project of Google + Fuxin software);
2. android-pdfview Github is still in maintenance.
3. android-pdfview Github obtains more stars.
4. The integration of client is more convenient.

Problem analysis:
Running the official android-pdfview demo has many problems:

1. It only realizes the functions of sliding reading and gesture expansion of pdf.
2. Lack of pdf directory tree, thumbnails and other functions;
3. The installation package is too large.
4. The UI is not beautiful.
5. Memory problems;
6. Other...

Nevertheless, don't worry. There are no problems in solving these problems. Ha, ha, ha (laughter is a bit reluctant).

Now, let's start to implement Demo.

4. Demo Design

4.1. Engineering Structure

Before designing Demo, it should be clear that the goal of Demo is to achieve:

1. android-pdfview has realized pdfview, which can be used to read pdf files, gesture to expand pdf pages and jump pdf pages.
   Then, we can extend the function based on android-pdfview, including directory tree, thumbnails, etc.

2. Extended functions should be logically decoupled without affecting the replaceability of android-pdfview code
  (that is, if there is a new version of android-pdfview, it can be replaced directly)

3. The client should be easy to integrate.
  For example, the client only needs to pass in the pdf file, and all loading, operation and memory management need not be concerned about.

How to design Demo project:
Download the latest android-pdfview source code, and you can see that there are two Moudle s:

android-pdf-viewer (latest source code)
Sample (sample app)

If we want to take over all the functions of encapsulating PDF so that sample can only pass PDF files without affecting the future replacement of android-pdf-viewer source code, then we can create a modle, as follows:

sample (rely on pdfui)
pdfui (rely on android-pdf-viewer)
android-pdf-viewer

4.2. Functional Design of PDF

For the convenience of users to read PDF, the following functions should be included:
1. PDF reading (including: finger sliding PDF page, gesture stretching page content, jumping PDF specified page)
2. PDF directory navigation function (including: directory display, directory node folding, expansion, click jump PDF page)
3. PDF thumbnail navigation function (including: thumbnail display, finger sliding, image cache management, click jump PDF page)

5. Solve the problem of too large installation package before encoding

Decompiled Demo installation packages, you can see that the installation packages default integration of the corresponding so library files of Various cpu platforms, installation packages are too large here. In fact, in the normal project development, for the retention or abandonment of so libraries corresponding to each cpu platform, the main consideration is the compatibility of cpu platform and equipment coverage.

Usually, only keeping armeabi-v7a is compatible with most Android devices on the market, so how to delete other SOS at compile time?

It can be configured in android gradle as follows:

android{
......
 splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a' //If you want to include so used by other cpu platforms, you can modify it here.
        }
    }
}

Re-compiled, generated installation package, only about 5M left.

Note: If there are other so libraries in the project, consider carefully how to choose them according to the actual needs of the project.

6. Implementing PDF Reading Function

Simple, because the android-pdf-viewer source code has already implemented this function, let's write a compact version of it.

6.1. Functional points:

1. pdf file in assets can be loaded
2. pdf file of URI type can be loaded (if it is an Online pdf file, it can be downloaded to the local area through the network library first, take its uri, this Demo will not write the network download)
3. The basic display function of PDF (using the control of android-pdf-viewer: PDFView)
4. Can jump to the directory page (directory data can be passed directly through intent)
5. Jump to the preview page (pdf file information can be passed directly through intent)
6. Jump to the specified pdf page according to the page number brought back by the catalog page and preview page

6.2. Code Implementation

Key contents:

1. The use of PDFView control; (Simpler, see the code for details)
2. How to get directory information from PDF files; (See the code for details on how and when to get directory information.)

PDF Reading Page Code: PDFActivity

/**
 * UI Page: PDF Reading
 * <p>
 * Main functions:
 * 1,Receive the passed pdf file (including the file name and uri in assets)
 * 2,Display PDF files
 * 3,Receive the PDF page number returned from the catalog page, preview page, and jump to the specified page
 * <p>
 * Author: Qi Xingchao
 * Date: 2019.08.07
 */
public class PDFActivity extends AppCompatActivity implements
        OnPageChangeListener,
        OnLoadCompleteListener,
        OnPageErrorListener {
    //PDF controls
    PDFView pdfView;
    //Button control: return, directory, thumbnail
    Button btn_back, btn_catalogue, btn_preview;
    //Page number
    Integer pageNumber = 0;
    //PDF catalog collection
    List<TreeNodeData> catelogues;

    //pdf file name (limit: files in assets)
    String assetsFileName;
    //pdf file uri
    Uri uri;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//Setting immersion type
        setContentView(R.layout.activity_pdf);

        initView();//Initialize view
        setEvent();//Set events
        loadPdf();//Loading PDF files
    }

    /**
     * Initialize 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);
    }

    /**
     * Set events
     */
    private void setEvent() {
        //Return
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFActivity.this.finish();
            }
        });
        //Jump directory pages
        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);
            }
        });
        //Skip thumbnail page
        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);
            }
        });
    }

    /**
     * Loading PDF files
     */
    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);
                }
            }
        }
    }

    /**
     * Displaying PDF Files Based on assets
     *
     * @param fileName File name
     */
    private void displayFromAssets(String fileName) {
        pdfView.fromAsset(fileName)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // Unit dp
                .onPageError(this)
                .pageFitPolicy(FitPolicy.BOTH)
                .load();
    }

    /**
     * Displaying PDF files based on uri
     *
     * @param uri File path
     */
    private void displayFromUri(Uri uri) {
        pdfView.fromUri(uri)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // Unit dp
                .onPageError(this)
                .load();
    }

    /**
     * When PDF is successfully loaded:
     * 1,Accessible directory information for PDF
     *
     * @param nbPages the number of pages in this PDF file
     */
    @Override
    public void loadComplete(int nbPages) {
        //Obtain document bookmark information
        List<PdfDocument.Bookmark> bookmarks = pdfView.getTableOfContents();
        if (catelogues != null) {
            catelogues.clear();
        } else {
            catelogues = new ArrayList<>();
        }
        //Converting bookmark to catalog data set
        bookmarkToCatelogues(catelogues, bookmarks, 1);
    }

    /**
     * Converting bookmark to catalog data set (recursion)
     *
     * @param catelogues Catalog data set
     * @param bookmarks  Bookmark data
     * @param level      Catalog tree level (used to control tree node position offset)
     */
    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) {
    }

    /**
     * Bring back the page number from thumbnails, directory pages, and jump to the specified PDF page
     *
     * @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();
        //Memory
        if (pdfView != null) {
            pdfView.recycle();
        }
    }
}

PDF Reading Page Layout File: 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="Return"
            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="Catalog"
            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="preview"
            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. Implementation of PDF Directory Tree

The data of the directory tree (directory name, page number, etc.) has been obtained on the previous page, so this page only needs to consider the implementation of the directory tree control.

Note: The reason why the data of directory tree is not obtained separately on this page is that android-pdfview and pdfium occupy too much memory and do not want to create Pdf related objects again.

7.1. PDF Directory Tree Effect Map

7.2. How to implement tree control?

Android does not have tree controls by default, but we can use RecyclerView or ListView to implement them.
As shown in the figure above:

List each action a catalog data, mainly including: name, page number;
If there is a subdirectory, an arrow image appears, which can be folded and expanded, and the direction of the arrow changes accordingly.
The name text of the subdirectory shifts to the right as the level of the directory tree increases.

Currently Demo is implemented in RecyclerView, how can we achieve the above effect?
Page effects and event effects can be handled in adapter:
1. List item content display

1. Use vertical linear layout manager;
2. Each item contains: arrow image (if there are subdirectories, then display), command name text, page number text;

2. Folding effect

1. Control the content of adapter data set. If a node collapses, delete the corresponding subdirectory data.
On the other hand, add notifyDataSetChanged to notify the data source of change.
2. In addition, a state is needed to mark whether the current node is unfolded or folded to control the display of arrow image direction.

3. Right-deviating effect of directory text

You can fix the left interval (e.g. 20dp) by the directory tree level * and then set the offset for the textview control of the directory.

How to get the hierarchical tree of directory tree? Options:
1. Recursive collection automatic acquisition (need to traverse, less efficient, if it is editable directory structure, it is recommended to choose)
2. Write directly when creating data (because the current demo PDF directory structure will not be edited, so choose this option directly)

7.3. Code implementation:

TreeNodeData:

/**
 * Tree control data class (for inter-page transmission, so Serializable or Parcelable)
 * Author: Qi Xingchao
 * Date: 2019.08.07
 */
public class TreeNodeData implements Serializable {
    //Name
    private String name;
    //Page number
    private int pageNum;
    //Has it been expanded (to control the display of tree node images, i.e. arrows toward images)
    private boolean isExpanded;
    //Display Level (Level 1, Level 2... to control the indentation position of tree nodes)
    private int treeLevel;
    //Subsets (used to load subnodes and to determine whether arrow images are displayed, if the collection is not empty)
    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;
    }
}

Tree Control Adapter

/**
 * Tree control adapter
 * Author: Qi Xingchao
 * Date: 2019.08.07
 */
public class TreeAdapter extends RecyclerView.Adapter<TreeAdapter.TreeNodeViewHolder> {
    //context
    private Context context;
    //data
    public List<TreeNodeData> data;
    //Display data (from hierarchical structure to planar structure)
    public List<TreeNodeData> displayData;
    //treelevel interval (dp)
    private int maginLeft;
    //Delegate object
    private TreeEvent delegate;

    /**
     * Constructor
     *
     * @param context context
     * @param data    data
     */
    public TreeAdapter(Context context, List<TreeNodeData> data) {
        this.context = context;
        this.data = data;
        maginLeft = UIUtils.dip2px(context, 20);
        displayData = new ArrayList<>();

        //Conversion of data to presentation data
        dataToDiaplayData(data);
    }

    /**
     * Conversion of data to presentation data
     *
     * @param data data
     */
    private void dataToDiaplayData(List<TreeNodeData> data) {
        for (TreeNodeData nodeData : data) {
            displayData.add(nodeData);
            if (nodeData.isExpanded() && nodeData.getSubset() != null) {
                dataToDiaplayData(nodeData.getSubset());
            }
        }
    }

    /**
     * Converting a data set to a display collection
     */
    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);
        //Set pictures
        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);
        }
        //Setting Picture Offset
        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);

        //Display text
        holder.title.setText(data.getName());
        holder.pageNum.setText(String.valueOf(data.getPageNum()));

        //Picture Click Event
        holder.img.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Control Tree Node Expansion and Folding
                data.setExpanded(!data.isExpanded());
                //Refresh data source
                reDataToDiaplayData();
            }
        });
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Callback result
                if(delegate!=null){
                    delegate.onSelectTreeNode(data);
                }
            }
        });
    }

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

    /**
     * Define the ViewHolder object of RecyclerView
     */
    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);
        }
    }

    /**
     * Interface: Tree event
     */
    public interface TreeEvent{
        /**
         * When a tree node is selected
         * @param data tree Node data
         */
        void onSelectTreeNode(TreeNodeData data);
    }

    /**
     * Events Setting Tree
     * @param treeEvent Tree Event object
     */
    public void setTreeEvent(TreeEvent treeEvent){
        this.delegate = treeEvent;
    }
}

PDF tree page: PDFCatelogueActivity

/**
 * UI Page: PDF Directory
 * <p>
 * 1,Used to display Pdf directory information
 * 2,Click on the tree item to bring back the Pdf page number to the previous page
 * <p>
 * Author: Qi Xingchao
 * Date: 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();//Initialization control
        setEvent();//Set events
        loadData();//Loading data
    }

    /**
     * Initialization control
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_tree);
    }

    /**
     * Set events
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFCatelogueActivity.this.finish();
            }
        });
    }

    /**
     * Loading data
     */
    private void loadData() {
        //Obtain the transferred data from intent
        Intent intent = getIntent();
        List<TreeNodeData> catelogues = (List<TreeNodeData>) intent.getSerializableExtra("catelogues");

        //Loading data using RecyclerView
        LinearLayoutManager llm = new LinearLayoutManager(this);
        llm.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(llm);
        TreeAdapter adapter = new TreeAdapter(this, catelogues);
        adapter.setTreeEvent(this);
        recyclerView.setAdapter(adapter);
    }


    /**
     * Click on the tree item to bring back the Pdf page number to the previous page
     *
     * @param data tree Node data
     */
    @Override
    public void onSelectTreeNode(TreeNodeData data) {
        Intent intent = new Intent();
        intent.putExtra("pageNum", data.getPageNum());
        setResult(Activity.RESULT_OK, intent);
        finish();
    }
}

Layout file of PDF directory tree: 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="Return"
            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="List of contents"
            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 preview thumbnails

This function is the most complex one in this Demo.

How to convert the content of a PDF page into a picture? (By default, page images cannot be obtained from pdfview)
How to reduce the image memory usage? (Users may quickly slide lists to read and display multiple pictures in real time)
How to optimize the sliding experience of PDF Preview thumbnail list? (The acquisition of pictures takes some time)
How to release memory occupancy in a reasonable and timely manner?

8.1. PDF Preview thumbnail list effect chart

8.2. Functional analysis

1. How to convert the content of a PDF page into a picture?

Looking at the source code of android-pdfview, you can't get a picture of a page through the PDFView control, so you can only analyze the API of pdfium sdk, as follows:

The renderPageBitmap method of pdfium can render a page into a picture, but it needs to pass a series of parameters, and be careful of OutOfMemoryError.

Then, we need to get or create PdfiumCore objects in code, call this method, pass parameters such as PdfDocument, and release bitmap in time when it is used.

2. How to reduce memory usage?

Memory mainly includes:
1. Memory generated by pdfium sdk loading pdf files (we cannot optimize)
2. Memory generated by android-pdfview (source code can be changed if necessary)
3. We turn pdf pages into thumbnails, resulting in memory (must be optimized, otherwise, easy oom)

3.1. When PdfiumCore and PdfDocument are no longer in use, they should be closed in time.
3.2. When thumbnails are no longer in use, they should be released in time.
3.3. Use LruCache to temporarily cache thumbnails to prevent repeated calls to renderPage Bitmap to get images.
3.4. LruCache should be properly controlled. When the preview page is closed, the cache must be emptied to release memory.
3.5 When creating pictures, RGB_565 should be used to save memory overhead (2 bytes per pixel).
3.6 When creating an image, you should specify the width of the image as small as possible, so that you can see it clearly (the memory occupied by the image = width * height * bytes occupied by one pixel point)

3. How to optimize the sliding experience of PDF Preview thumbnail list?

Looking at the pdfium source code, before calling the renderPageBitmap method, you must also ensure that the corresponding page has been opened, that is, the openPage method has been called. However, both methods require a certain amount of time to complete.

Then, if we directly call each RecylerVew's item in the main thread by calling the renderPageBitmap method separately, we will feel the special card when we slide the list, so this method can only be called in the sub thread.

So the question arises again. How should so many sub-threads be managed?

1. Considering the occupancy of CPU, thread pool should be used to control the concurrency and blocking of sub-threads.
2. Considering the user's sliding speed, it is possible that a thread is executing or blocking, and the page has slid past. Then, even if the thread loads the picture, it can not be displayed in the list. Therefore, the thread corresponding to Item item which is invisible to RecyclerView should be cancelled in time to prevent idle work and save memory and cpu overhead.

8.3. Functional Realization

Preview thumbnail tool class: Preview Utils

/**
 * Preview thumbnail tool class
 *
 * 1,pdf Turn pages into thumbnails
 * 2,Picture cache management (save to memory only, use LruCache, pay attention to space size control)
 * 3,Multithread management (thread concurrency, blocking, Future task cancellation)
 *
 * Author: Qi Xingchao
 * Date: 2019.08.08
 */
public class PreviewUtils {
    //Picture Cache Management
    private ImageCache imageCache;
    //Single case
    private static PreviewUtils instance;
    //Thread pool
    ExecutorService executorService;
    //Thread Task Set (which can be used to cancel tasks)
    HashMap<String, Future> tasks;

    /**
     * Singleton (only main thread calls, not thread-safe)
     *
     * @return PreviewUtils Instance object
     */
    public static PreviewUtils getInstance() {
        if (instance == null) {
            instance = new PreviewUtils();
        }
        return instance;
    }

    /**
     * Default constructor
     */
    private PreviewUtils() {
        //Initialization of Picture Cache Management Objects
        imageCache = new ImageCache();
        //Create a concurrent thread pool (recommended maximum concurrency greater than 1 screen grid item)
        executorService = Executors.newFixedThreadPool(20);
        //Create a thread task set to cancel thread execution
        tasks = new HashMap<>();
    }

    /**
     * Loading pictures from pdf files
     *
     * @param context     context
     * @param imageView   Picture control
     * @param pdfiumCore  pdf Core object
     * @param pdfDocument pdf Document object
     * @param pdfName     pdf File name
     * @param pageNum     pdf Page number
     */
    public void loadBitmapFromPdf(final Context context,
                                  final ImageView imageView,
                                  final PdfiumCore pdfiumCore,
                                  final PdfDocument pdfDocument,
                                  final String pdfName,
                                  final int pageNum) {
        //Judging the Legitimacy of Parameters
        if (imageView == null || pdfiumCore == null || pdfDocument == null || pageNum < 0) {
            return;
        }

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

            //Set tags for picture controls
            imageView.setTag(keyPage);

            Log.i("PreViewUtils", "Load pdf Thumbnails:" + keyPage);

            //Get the size of the image view (Note: If you use the normal control size, it takes up too much memory)
            /*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;*/

            //Memory size = image width * image height * number of bytes per pixel (RGB_565 bytes: 2)
            //Note: If you use normal control size, it takes up too much memory, so specifying four thumbnails here will blur a little.
            final int reqWidth = 100;
            final int reqHeight = 150;

            //Pictures from Cache
            Bitmap bitmap = imageCache.getBitmapFromLruCache(keyPage);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }

            //Use thread pool to manage sub-threads
            Future future = executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //Open the page (before calling the renderPageBitmap method, you must make sure that the page is open, important)
                    pdfiumCore.openPage(pdfDocument, pageNum);

                    //Call the native method to render the Pdf page into a picture
                    final Bitmap bm = Bitmap.createBitmap(reqWidth, reqHeight, Bitmap.Config.RGB_565);
                    pdfiumCore.renderPageBitmap(pdfDocument, bm, pageNum, 0, 0, reqWidth, reqHeight);

                    //Cut back to the main thread and set the picture
                    if (bm != null) {
                        //Add pictures to the cache
                        imageCache.addBitmapToLruCache(keyPage, bm);

                        //Cut back the main thread to load the picture
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                if (imageView.getTag().toString().equals(keyPage)) {
                                    imageView.setImageBitmap(bm);
                                    Log.i("PreViewUtils", "Load pdf Thumbnails:" + keyPage + "......Set up!!");
                                }
                            }
                        });
                    }
                }
            });

            //Add tasks to collections
            tasks.put(keyPage, future);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Cancel the task of loading pictures from pdf files
     *
     * @param keyPage Page number
     */
    public void cancelLoadBitmapFromPdf(String keyPage) {
        if (keyPage == null || !tasks.containsKey(keyPage)) {
            return;
        }
        try {
            Log.i("PreViewUtils", "Cancel loading pdf Thumbnails:" + keyPage);
            Future future = tasks.get(keyPage);
            if (future != null) {
                future.cancel(true);
                Log.i("PreViewUtils", "Cancel loading pdf Thumbnails:" + keyPage + "......Canceled!!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Get the image cache object
     * @return Picture cache
     */
    public ImageCache getImageCache(){
        return imageCache;
    }

    /**
     * Picture Cache Management
     */
   public class ImageCache {
        //Picture cache
        private LruCache<String, Bitmap> lruCache;

        //Constructor
        public ImageCache() {
            //Initialize lruCache
            //int maxMemory = (int) Runtime.getRuntime().maxMemory();
            //int cacheSize = maxMemory/8;
            int cacheSize = 1024 * 1024 * 30;//Temporarily set 30M
            lruCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }

        /**
         * Pictures from Cache
         * @param key key
         * @return picture
         */
        public synchronized Bitmap getBitmapFromLruCache(String key) {
            if(lruCache!= null) {
                return lruCache.get(key);
            }
            return null;
        }

        /**
         * Add pictures to the cache
         * @param key key
         * @param bitmap picture
         */
        public synchronized void addBitmapToLruCache(String key, Bitmap bitmap) {
            if (getBitmapFromLruCache(key) == null) {
                if (lruCache!= null && bitmap != null)
                    lruCache.put(key, bitmap);
            }
        }

        /**
         * wipe cache
         */
        public void clearCache(){
            if(lruCache!= null){
                lruCache.evictAll();
            }
        }
    }
}

grid list adapter: GridAdapter

/**
 * grid List adapter
 * Author: Qi Xingchao
 * Date: 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) {
        //Setting up PDF pictures
        final int pageNum = position;
        PreviewUtils.getInstance().loadBitmapFromPdf(context, holder.iv_page, pdfiumCore, pdfDocument, pdfName, pageNum);
        //Setting PDF Page Number
        holder.tv_pagenum.setText(String.valueOf(position));
        //Setting Grid Events
        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 {
            //Cancel tasks when item is not visible
            if(holder.iv_page!=null){
                PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
            }

            //Release bitmap when item is not visible (Note: This Demo uses LruCache cache to manage images, which can be commented out here)
            /*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","Destroy the pdf thumbnail: "+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);
        }
    }

    /**
     * Interface: Grid events
     */
    public interface GridEvent{
        /**
         * When a Grid item is selected
         * @param position tree Node data
         */
        void onGridItemClick(int position);
    }

    /**
     * Setting Grid Events
     * @param event Grid Event object
     */
    public void setGridEvent(GridEvent event){
        this.delegate = event;
    }

    //Grid Event Delegation
    private GridEvent delegate;
}

PDF Preview thumbnail page: PDFP review activity

/**
 * UI Page: PDF Preview thumbnail (Note: This page needs more attention to memory control)
 * <p>
 * 1,Used to display Pdf thumbnail information
 * 2,Click on the thumbnail to bring back the Pdf page number to the previous page
 * <p>
 * Author: Qi Xingchao
 * Date: 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();//Initialization control
        setEvent();
        loadData();
    }

    /**
     * Initialization control
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_grid);
    }

    /**
     * Set events
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Recovery memory
                recycleMemory();

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

    }

    /**
     * Loading data
     */
    private void loadData() {
        //Loading pdf files
        loadPdfFile();

        //Get the total number of pdf pages
        int totalCount = pdfiumCore.getPageCount(pdfDocument);

        //Binding list data
        GridAdapter adapter = new GridAdapter(this, pdfiumCore, pdfDocument, assetsFileName, totalCount);
        adapter.setGridEvent(this);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        recyclerView.setAdapter(adapter);
    }

    /**
     * Loading pdf files
     */
    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);
                }
            }
        }
    }

    /**
     * Loading pdf files in assets
     */
    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();
        }
    }

    /**
     * Loading pdf files based on uri
     */
    void loadUriPdfFile(Uri uri) {
        try {
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
            pdfiumCore = new PdfiumCore(this);
            pdfDocument = pdfiumCore.newDocument(pfd);
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    /**
     * Click on the thumbnail to bring back the Pdf page number to the previous page
     *
     * @param position Page number
     */
    @Override
    public void onGridItemClick(int position) {
        //Recovery memory
        recycleMemory();

        //Return to the previous page number
        Intent intent = new Intent();
        intent.putExtra("pageNum", position);
        setResult(Activity.RESULT_OK, intent);
        finish();
    }

    /**
     * Recovery memory
     */
    private void recycleMemory(){
        //Close the pdf object
        if (pdfiumCore != null && pdfDocument != null) {
            pdfiumCore.closeDocument(pdfDocument);
            pdfiumCore = null;
        }
        //Clear the image cache to free up memory space
        PreviewUtils.getInstance().getImageCache().clearCache();
    }
}

PDF Preview thumbnail page layout file: 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="Return"
            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="Preview thumbnail list"
            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>

summary

There are many functional points and difficulties involved in the document, especially memory management and multi-threading management. There are some unclear suggestions to download Demo and look at the source code more. You are also welcome to leave a message for consultation, that is, you don't necessarily have time to answer, haha...

If you want to use the demo in the project, I suggest to test more, because of the time relationship, I have only done the basic test here.

Demo Download Address (github + Baidu Disk):
https://github.com/qxcwanxss/AndroidPdfViewerDemo
https://pan.baidu.com/s/1_Py36avgQqcJ5C87BaS5Iw

Posted by cschotch on Fri, 11 Oct 2019 17:52:07 -0700