Android 原生方式PDF文件预览

2,278 阅读4分钟

Android 原生PDF预览

实现思路过程

  • PdfRenderer用于将文件写入Bitmap
 // create a new renderer
 PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor());

 // let us just render all pages
 final int pageCount = renderer.getPageCount();
 for (int i = 0; i < pageCount; i++) {
     Page page = renderer.openPage(i);

     // say we render for showing on the screen
     page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY);

     // do stuff with the bitmap

     // close the page
     page.close();
 }

 // close the renderer
 renderer.close();
  • 自定义SurfaceView用于将Bitmap绘制到Canvas画布上
// 获取 SurfaceHolder 对象的锁,以进行图形操作  
Canvas canvas = mSurfaceHolder.lockCanvas();  
if (canvas != null) {  
canvas.drawColor(Color.LTGRAY);  
canvas.drawBitmap(mBitmap, 0, 0, null);  
// 释放锁  
mSurfaceHolder.unlockCanvasAndPost(canvas);  
}
  • 自定义RecycleView控制多个SurfaceView展示多个页面
  • LinearSnapHelper关联RecycleView单个Item铺满一屏
  • 页面结束释放Bitmap

具体代码

  • 自定义SurfaceView
package com.example.tss.render;  
  
  
import android.content.Context;  
import android.graphics.Bitmap;  
import android.graphics.Canvas;  
import android.graphics.Color;  
import android.util.AttributeSet;  
import android.view.SurfaceHolder;  
import android.view.SurfaceView;  
  
/**  
* @author Administrator  
* @date 2024/3/29 0029 10:11  
* @description: MySurfaceView  
*/  
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {  
  
private SurfaceHolder mSurfaceHolder;  
private boolean isSurfaceCreated;  
  
public boolean isSurfaceCreated() {  
return isSurfaceCreated;  
}  
  
public MySurfaceView(Context context, AttributeSet attrs) {  
super(context, attrs);  
// 获取 SurfaceHolder 对象,并设置回调  
mSurfaceHolder = getHolder();  
mSurfaceHolder.addCallback(this);  
}  
  
@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
}  
  
@Override  
public void surfaceCreated(SurfaceHolder holder) {  
isSurfaceCreated = true;  
  
if (onAlreadyListener != null) {  
Bitmap bitmap = onAlreadyListener.onAlready(holder);  
// 在这里进行渲染操作  
drawSurface(bitmap);  
}  
}  
  
@Override  
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {  
// 可以在这里进行 surface 大小变化时的操作  
}  
  
@Override  
public void surfaceDestroyed(SurfaceHolder holder) {  
isSurfaceCreated = false;  
if (onAlreadyListener != null) {  
onAlreadyListener.onFinish(holder);  
}  
}  
  
private void drawSurface(Bitmap mBitmap) {  
// 获取 SurfaceHolder 对象的锁,以进行图形操作  
Canvas canvas = mSurfaceHolder.lockCanvas();  
if (canvas != null) {  
canvas.drawColor(Color.LTGRAY);  
canvas.drawBitmap(mBitmap, 0, 0, null);  
// 释放锁  
mSurfaceHolder.unlockCanvasAndPost(canvas);  
}  
}  
  
private OnAlreadyListener onAlreadyListener;  
  
public void setOnAlreadyListener(OnAlreadyListener onAlreadyListener) {  
this.onAlreadyListener = onAlreadyListener;  
}  
  
public interface OnAlreadyListener {  
Bitmap onAlready(SurfaceHolder holder);  
  
void onFinish(SurfaceHolder holder);  
}  
}
  • 创建item_render_pdf.xml
<?xml version="1.0" encoding="utf-8"?>  
<com.example.tss.render.MySurfaceView xmlns:android="http://schemas.android.com/apk/res/android"  
android:id="@+id/surfaceView"  
android:layout_width="match_parent"  
android:layout_height="match_parent"  
android:layout_marginBottom="2dp">  
  
</com.example.tss.render.MySurfaceView>
  • 创建MulRenderAdapter
package com.example.tss.render;  
  
import android.graphics.Bitmap;  
import android.util.LruCache;  
import android.view.LayoutInflater;  
import android.view.SurfaceHolder;  
import android.view.ViewGroup;  
  
import androidx.annotation.NonNull;  
import androidx.recyclerview.widget.RecyclerView;  
  
import com.example.tss.databinding.ItemRenderPdfBinding;  
  
/**  
* @author Administrator  
* @date 2024/3/29 0029 11:21  
* @description: MulRenderAdapter  
*/  
public class MulRenderAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {  
private LruCache<Integer, Bitmap> bitmapLruCache = new LruCache<>(Integer.MAX_VALUE);  
  
public void add(Integer integer, Bitmap bitmap) {  
this.bitmapLruCache.put(integer, bitmap);  
notifyDataSetChanged();  
}  
  
public LruCache<Integer, Bitmap> getBitmapLruCache() {  
return bitmapLruCache;  
}  
  
@NonNull  
@Override  
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {  
@NonNull ItemRenderPdfBinding binding = ItemRenderPdfBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);  
return new MyHolder(binding);  
}  
  
@Override  
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {  
MyHolder myHolder = (MyHolder) holder;  
myHolder.binding.surfaceView.setOnAlreadyListener(new MySurfaceView.OnAlreadyListener() {  
  
@Override  
public Bitmap onAlready(SurfaceHolder holder) {  
return bitmapLruCache.get(myHolder.getAdapterPosition());  
}  
@Override  
public void onFinish(SurfaceHolder holder) {  
  
}  
});  
}  
  
@Override  
public int getItemCount() {  
return bitmapLruCache.putCount();  
}  
  
class MyHolder extends RecyclerView.ViewHolder {  
ItemRenderPdfBinding binding;  
  
public MyHolder(ItemRenderPdfBinding binding) {  
super(binding.getRoot());  
this.binding = binding;  
}  
}  
}
  • 创建fragment_pdf_renderer.xml
<?xml version="1.0" encoding="utf-8"?>  
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
xmlns:tools="http://schemas.android.com/tools"  
android:layout_width="match_parent"  
android:layout_height="match_parent"  
tools:context=".PdfRendererFragment">  
  
<androidx.recyclerview.widget.RecyclerView  
android:id="@+id/recycleView"  
android:layout_width="match_parent"  
android:layout_height="match_parent" />  
  
</FrameLayout>
  • 创建PdfRendererFragment
package com.example.tss;  
  
import android.content.Context;  
import android.content.res.AssetManager;  
import android.graphics.Bitmap;  
import android.graphics.pdf.PdfRenderer;  
import android.os.Bundle;  
import android.os.ParcelFileDescriptor;  
import android.util.TypedValue;  
import android.view.LayoutInflater;  
import android.view.View;  
import android.view.ViewGroup;  
  
import androidx.fragment.app.Fragment;  
import androidx.recyclerview.widget.LinearLayoutManager;  
import androidx.recyclerview.widget.LinearSnapHelper;  
  
import com.example.tss.databinding.FragmentPdfRendererBinding;  
import com.example.tss.render.MulRenderAdapter;  
  
import java.io.File;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.InputStream;  
  
/**  
* A simple {@link Fragment} subclass.  
* Use the {@link PdfRendererFragment#newInstance} factory method to  
* create an instance of this fragment.  
*/  
public class PdfRendererFragment extends Fragment {  
//来自assets根目录  
private static final String PDF_FILE = "examplex.pdf";  
  
private PdfRenderer renderer;  
  
// TODO: Rename parameter arguments, choose names that match  
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER  
private static final String ARG_PARAM1 = "param1";  
private static final String ARG_PARAM2 = "param2";  
  
// TODO: Rename and change types of parameters  
private String mParam1;  
private String mParam2;  
private FragmentPdfRendererBinding binding;  
private MulRenderAdapter mulRenderAdapter;  
  
public PdfRendererFragment() {  
// Required empty public constructor  
}  
  
/**  
* Use this factory method to create a new instance of  
* this fragment using the provided parameters.  
*  
* @param param1 Parameter 1.  
* @param param2 Parameter 2.  
* @return A new instance of fragment PdfRendererFragment.  
*/  
// TODO: Rename and change types and number of parameters  
public static PdfRendererFragment newInstance(String param1, String param2) {  
PdfRendererFragment fragment = new PdfRendererFragment();  
Bundle args = new Bundle();  
args.putString(ARG_PARAM1, param1);  
args.putString(ARG_PARAM2, param2);  
fragment.setArguments(args);  
return fragment;  
}  
  
@Override  
public void onCreate(Bundle savedInstanceState) {  
super.onCreate(savedInstanceState);  
if (getArguments() != null) {  
mParam1 = getArguments().getString(ARG_PARAM1);  
mParam2 = getArguments().getString(ARG_PARAM2);  
}  
// 从 assets 中加载 PDF 文件  
Context context = getActivity();  
try {  
AssetManager assetManager = context.getAssets();  
InputStream inputStream = assetManager.open(PDF_FILE);  
// assets中文件提取到File对象中 
File file = new File(context.getFilesDir(), PDF_FILE);  
FileOutputStream fileOutputStream = new FileOutputStream(file);  
byte[] buffer = new byte[1024];  
int length;  
while ((length = inputStream.read(buffer)) > 0) {  
fileOutputStream.write(buffer, 0, length);  
}  
  
ParcelFileDescriptor parcelFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);  
renderer = new PdfRenderer(parcelFileDescriptor);  
fileOutputStream.close();  
inputStream.close();  
} catch (IOException e) {  
throw new RuntimeException(e);  
}  
}  
  
@Override  
public View onCreateView(LayoutInflater inflater, ViewGroup container,  
Bundle savedInstanceState) {  
// Inflate the layout for this fragment R.layout.fragment_pdf_renderer  
binding = FragmentPdfRendererBinding.inflate(inflater, container, false);  
binding.recycleView.setLayoutManager(new LinearLayoutManager(getContext()));  
mulRenderAdapter = new MulRenderAdapter();  
binding.recycleView.setAdapter(mulRenderAdapter);  
LinearSnapHelper snapHelper=new LinearSnapHelper();  
snapHelper.attachToRecyclerView(binding.recycleView);  
renderPage();  
return binding.getRoot();  
}  
  
@Override  
public void onDestroyView() {  
if (mulRenderAdapter.getBitmapLruCache()!=null) {  
for (int i = 0; i < mulRenderAdapter.getBitmapLruCache().putCount(); i++) {  
mulRenderAdapter.getBitmapLruCache().get(i).recycle();  
}  
}  
super.onDestroyView();  
}  
/**  
* 渲染多个PDF页面到Bitmap,加载到SurfaceView  
*/  
private void renderPage() {  
int w = getContext().getResources().getDisplayMetrics().widthPixels;  
int padding= (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100,getResources().getDisplayMetrics());  
int h = getContext().getResources().getDisplayMetrics().heightPixels-padding;  
  
// let us just render all pages  
final int pageCount = renderer.getPageCount();  
for (int i = 0; i < pageCount; i++) {  
PdfRenderer.Page page = renderer.openPage(i);  
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);  
// say we render for showing on the screen  
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);  
// do stuff with the bitmap  
mulRenderAdapter.add(i, bitmap);  
// close the page  
page.close();  
}  
  
// close the renderer  
renderer.close();  
}  
  
}
  • 效果展示

    Screen_recording_20240330_155923 00_00_00-00_00_30.gif
  • 源码出处

  • PdfRenderer 已移至一个模块,此模块可使用与平台版本无关的 Google Play 系统更新来进行更新,并且我们将通过创建兼容的 API Surface 低于 Android 15 的版本(称为 PdfRendererPreV)来支持在 Android 11(API 级别 30)中更新这些变更。

  • 生成查看PdfDocument

  • 另一种思路直接绘制一个大长图渲染多个Bitmap,具备上下滑动效果