PDF文件渲染PdfRenderer
在前面的博文中,讲到可以通过Vudroid和MuPDF读取PDF文件,可是这两个开源框架都要使用jni编译出so库,不但步骤繁琐,而且兼容性也有欠缺。幸好Android在5.0后就开始支持PDF文件的读取,直接在内核中集成了PDF的渲染操作,很大程度上方便了开发者,这个内核中的PDF管理工具便是PdfRenderer。
PdfRenderer允许从多个来源读取PDF文件,不同来源的PDF文件打开操作由ParcelFileDescriptor完成,该类的对象可以通过两种方式获得,一种方式是从assets目录下读取pdf文件,另一种方式是从存储卡上读取pdf文件。
从assets目录下读取pdf文件的代码举例如下:
ParcelFileDescriptor fd = getAssets().openFd("example.pdf").getParcelFileDescriptor();从存储卡上读取pdf文件的代码举例如下:
ParcelFileDescriptor fd = ParcelFileDescriptor.open(
new File("example.pdf"), ParcelFileDescriptor.MODE_READ_ONLY);
打开PDF文件只是第一步,接下来还要使用PdfRenderer加载pdf文件,并进行相关的处理操作,PdfRenderer的常用方法说明如下:
构造函数:从ParcelFileDescriptor对象构造一个PdfRenderer实例。
getPageCount:获取PDF文件的页数。
openPage:打开PDF文件的指定页面,该方法返回一个PdfRenderer.Page对象。
close:关闭PDF文件。
从上面列出的方法看到,PdfRenderer只是提供了对整个PDF文件的管理操作,具体页面的处理比如渲染得由PdfRenderer.Page对象来完成,下面是Page的常用方法说明:
getIndex:获取该页的页码。
getWidth:获取该页的宽度。
getHeight:获取该页的高度。
render:渲染该页面的内容,并将渲染结果写入到一个Bitmap位图对象中。开发者可在此把Bitmap对象保存为存储卡上的图片文件。
close:关闭该pdf页。
总而言之,PdfRenderer的作用就是把一个pdf文件转换为若干个图片,然后开发者可将这些图片展示到手机屏幕上。下面是使用PdfRenderer读取并显示pdf文件的效果图:
下面是使用PdfRenderer读取pdf文件的主要代码:
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() +
"/Download/pdf/" + MD5Util.encrypByMd5(path);
ArrayList<String> imgArray = new ArrayList<String>();
try {
ParcelFileDescriptor fd = ParcelFileDescriptor.open(
new File(path), ParcelFileDescriptor.MODE_READ_ONLY);
PdfRenderer pdfRenderer = new PdfRenderer(fd);
for (int i=0; i<pdfRenderer.getPageCount(); i++) {
String imgPath = String.format("%s/%d.jpg", dir, i);
imgArray.add(imgPath);
final PdfRenderer.Page page = pdfRenderer.openPage(i);
Bitmap bitmap = Bitmap.createBitmap(page.getWidth(), page.getHeight(),
Bitmap.Config.ARGB_8888);
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
FileUtil.saveBitmap(imgPath, bitmap);
page.close();
}
pdfRenderer.close();
} catch (Exception e) {
e.printStackTrace();
}
PdfSelfAdapter adapter = new PdfSelfAdapter(getSupportFragmentManager(), imgArray);
vp_content.setAdapter(adapter);
vp_content.setCurrentItem(0);
vp_content.setVisibility(View.VISIBLE);
栈视图StackView
因为PDF文件本质上是一本书籍,所以在手机上浏览PDF页面,用户更习惯从上到下的层叠显示,而不是ViewPager那种从左到右的画卷方式。在Android的控件家族当中,比较接近上下层叠方式的是栈视图StackView,它的前后两项视图有部分是重叠在一起的,然后可以通过上下滑动来切换当前显示的顶层视图。
StackView的使用方式类似于ListView,都是调用setAdapter方法设置一组子项,多出来的属性只有loopViews,该属性用于控制是否循环显示子项视图。
下面是使用StackView浏览pdf页面的效果图:
层叠翻页效果
上面提到的StackView,仍然不完全符合现实生活中的书页排列,比如上下两页只是部分区域重叠不是完全覆盖,另外前后页面是通过上下滑动切换而不是通过左右滑动切换,所以要想实现现实生活中的层叠翻页效果,还是得自定义书籍页面的控件。
自定义层叠翻页控件,可借鉴ViewFlipper的实现,首先定义一个总体的框架视图,用于存放当前页面与前后两页;其次定义具体页面的视图,每个页面视图展示一个PDF页面。框架视图主要负责两块工作:
1、接管屏幕上的触摸事件,通知当前的页面视图向左或者向右滑动,并在松开手势时判断接下来是继续翻页,还是恢复原状;
2、在翻页结束时,在屏幕上重新组织当前页面与前后两页,类似于ViewPager+Fragment的三页缓存机制;
页面视图主要负责三块工作:
1、将当前页面高亮显示,其它页面变暗显示;
2、按照用户的手势触摸,将当前页面滑动相应的距离;
3、在用户松开手势时,如果当前页面滑动距离不超过页面宽度的二分之一,则将当前页滑动到原来的位置;如果当前页面滑动距离超过页面宽度的二分之一,则将当前页滑动到原来的相反位置,即原来是显示着的则现在隐藏,原来是隐藏着的则现在显示。
下面是层叠翻页的效果图:
下面是层叠翻页的框架视图代码:
public class ViewSlider extends FrameLayout implements BookView.OnScrollListener {
private final static String TAG = "ViewSlider";
private Context mContext;
private int mWidth, mHeight;
private float rawX = 0;
private ArrayList<String> mPathArray = new ArrayList<String>();
private int mPos = 0;
private BookView mPreView, mCurrentView, mNextView;
private int mShowPage;
private static int SHOW_NONE = 0;
private static int SHOW_PRE = 1;
private static int SHOW_NEXT = 2;
private boolean isScroll = false;
public ViewSlider(Context context) {
this(context, null);
}
public ViewSlider(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewSlider(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
public void setFilePath(ArrayList<String> pathArray) {
removeAllViews();
mPathArray = pathArray;
if (mPathArray.size() > 0) {
mCurrentView = getBookPage(0, true);
addView(mCurrentView);
}
if (mPathArray.size() > 1) {
mNextView = getBookPage(1, false);
addView(mNextView, 0);
}
}
private BookView getBookPage(int position, boolean isUp) {
BookView page = new BookView(mContext);
MarginLayoutParams params = new LinearLayout.LayoutParams(
mWidth, LayoutParams.WRAP_CONTENT);
page.setLayoutParams(params);
ImageView iv = new ImageView(mContext);
iv.setLayoutParams(params);
iv.setScaleType(ScaleType.FIT_CENTER);
iv.setImageBitmap(BitmapFactory.decodeFile(mPathArray.get(position)));
page.addView(iv);
page.setUp(isUp);
return page;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isScroll) {
return super.onTouchEvent(event);
}
int distanceX = (int) (event.getRawX() - rawX);
Log.d(TAG, "action="+event.getAction()+", distanceX="+distanceX);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
rawX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
if (distanceX > 0) { //展示上一页
if (mPos == 0) {
mShowPage = SHOW_NONE;
} else {
mShowPage = SHOW_PRE;
mPreView.setUp(true);
mPreView.setMargin(-mWidth + distanceX);
mCurrentView.setUp(false);
}
} else { //展示下一页
if (mPos == mPathArray.size()-1 || mNextView==null) {
mShowPage = SHOW_NONE;
} else if (mNextView != null) {
mShowPage = SHOW_NEXT;
mCurrentView.setMargin(distanceX);
}
}
break;
case MotionEvent.ACTION_UP:
if (mShowPage == SHOW_PRE) {
int direction = Math.abs(distanceX)<mWidth/2 ? BookView.DIRECTION_LEFT : BookView.DIRECTION_RIGHT;
//Log.d(TAG, "direction="+direction+", mShowPage="+mShowPage+", distanceX="+distanceX);
mPreView.scrollView(direction, -mWidth+distanceX, this);
isScroll = true;
} else if (mShowPage == SHOW_NEXT) {
int direction = Math.abs(distanceX)>mWidth/2 ? BookView.DIRECTION_LEFT : BookView.DIRECTION_RIGHT;
//Log.d(TAG, "direction="+direction+", mShowPage="+mShowPage+", distanceX="+distanceX);
mCurrentView.scrollView(direction, distanceX, this);
isScroll = true;
} else {
isScroll = false;
}
break;
}
return true;
}
@Override
public void onScrollEnd(int direction) {
//Log.d(TAG, "direction="+direction+", mPos="+mPos);
if (mShowPage == SHOW_PRE) {
if (direction == BookView.DIRECTION_RIGHT) {
mPos--;
if (mNextView != null) {
removeView(mNextView);
}
mNextView = mCurrentView;
mCurrentView = mPreView;
if (mPos > 0) {
mPreView = getBookPage(mPos-1, false);
addView(mPreView);
mPreView.setMargin(-mWidth);
} else {
mPreView = null;
}
}
mCurrentView.setUp(true);
} else if (mShowPage == SHOW_NEXT) {
if (direction == BookView.DIRECTION_LEFT) {
mPos++;
if (mPreView != null) {
removeView(mPreView);
}
mPreView = mCurrentView;
mCurrentView = mNextView;
if (mPos < mPathArray.size()-1) {
mNextView = getBookPage(mPos+1, false);
addView(mNextView, 0);
} else {
mNextView = null;
}
}
mCurrentView.setUp(true);
}
isScroll = false;
}
}
下面是层叠翻页的页面视图代码:
public class BookView extends FrameLayout {
private final static String TAG = "BookView";
private Context mContext;
private int mWidth, mHeight;
private boolean mIsUp = false;
private MarginLayoutParams mParams;
public static int DIRECTION_LEFT = -1;
public static int DIRECTION_RIGHT = 1;
public BookView(Context context) {
super(context);
mContext = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mIsUp) {
canvas.drawColor(Color.TRANSPARENT);
} else {
canvas.drawColor(0x55000000);
}
}
public void setUp(boolean isUp) {
mIsUp = isUp;
invalidate();
}
public void setMargin(int margin) {
mParams = (MarginLayoutParams) getLayoutParams();
mParams.leftMargin = margin;
setLayoutParams(mParams);
invalidate();
}
public void scrollView(int direction, int distance, OnScrollListener listener) {
mListener = listener;
mHandler.postDelayed(new ScrollRunnable(direction, distance), mTimeGap);
}
private OnScrollListener mListener;
public static interface OnScrollListener {
public abstract void onScrollEnd(int direction);
}
private int mTimeGap = 20;
private int mDistanceGap = 20;
private Handler mHandler = new Handler();
private class ScrollRunnable implements Runnable {
private int mDirection;
private int mDistance;
public ScrollRunnable(int direction, int distance) {
mDirection = direction;
mDistance = distance;
}
@Override
public void run() {
if (mDirection==DIRECTION_LEFT && mDistance>-mWidth) {
mDistance -= mDistanceGap;
if (mDistance < -mWidth) {
mDistance = -mWidth;
}
mParams.leftMargin = mDistance;
setLayoutParams(mParams);
mHandler.postDelayed(new ScrollRunnable(mDirection, mDistance), mTimeGap);
} else if (mDirection==DIRECTION_RIGHT && mDistance<0) {
mDistance += mDistanceGap;
if (mDistance > 0) {
mDistance = 0;
}
mParams.leftMargin = mDistance;
setLayoutParams(mParams);
mHandler.postDelayed(new ScrollRunnable(mDirection, mDistance), mTimeGap);
} else if (mListener != null) {
mListener.onScrollEnd(mDirection);
}
}
}
}
另外有种自然翻页效果,也就是书页卷起来翻动,这个翻页动画参见以前的博文《
Android开发笔记(十八)书籍翻页动画》。
点击下载本文用到的层叠翻页的书籍浏览代码
点此查看Android开发笔记的完整目录