精华内容
下载资源
问答
  • pdf合并成一个的操作方法

    千次阅读 2015-01-15 14:44:32
    最近,楼主发了一套200页的PDF文件,里面有图也有字,想把它们打印出来,可是其中的页面很小,近乎于正方形,要是把所有的页面合并在一个PDF文件 中,便于管理与阅读,有什么好的方法呢?  最简单的方法:下载...
    最近,楼主发了一套200页的PDF文件,里面有图也有字,想把它们打印出来,可是其中的页面很小,近乎于正方形,要是把所有的页面合并在一个PDF文件
    


    中,便于管理与阅读,有什么好的方法呢?
      最简单的方法:下载一个PDF合并器,如常用的迅捷PDF合并软件(最好是V1.1版本)
      操作步骤:
      (1)首先看看桌面上有木有这个软件,没有就去百度下一个,此处不必多言下载过程,伙伴们都的
      (2)安装这个软件后,然后就给大家展示一下文件合并过程,双击软件快捷图标,点击“添加文件”按钮,将实现打包好的200页电子书一次性拖到操


    pdf合并软件 http://www.onlinedown.net/soft/224294.htm


    作面板上;
      (3)设置路径中,常以“自定义文件到你保持文件的地点,就看到你保持的提取页面了夹“为合并成的PDF文件保存位置,选定后单击”合并文件”,
    pdf合并软件 http://www.skycn.com/soft/appid/429284.html

    等待片刻,在弹出的提示框中按下确定。到你保持文件的地点,就看到你保持的合并页面了。
      批量图片能够合并成一个PDF文件?当然可以,操作方法如上所述。如觉得对您有用。请在本经验的末尾处或本经验的右上方点击“有用,收藏”。谢


    谢!

    展开全文
  • 页面调用Adobe Reader自带的控件实现pdf打印

    万次阅读 热门讨论 2017-11-24 14:58:00
     但是,有的场景会考虑到pdf文件的敏感,需要pdf文件不能被下载,只能打印且只能打印一次,这就需要考虑使用控件了。   2、Adobe Reader    Adobe Reader是一款优秀的PDF文档阅读软件。你可以使用Adobe ...

    1、业务场景

     

           一般的业务场景里,只需要将pdf文件下载到本地,然后打开直接打印就可以了。

          但是,有的场景会考虑到pdf文件的敏感性,需要pdf文件不能被下载,只能打印且只能打印一次,这就需要考虑使用控件了。

     

    2、Adobe Reader

     

          Adobe Reader是一款优秀的PDF文档阅读软件。你可以使用Adobe Reader查看、打印和管理PDF文件,还可以使用 Adobe Reader的多媒体工具可以播放PDF中的视频和音乐。

          这里我们会用到Adobe Reader里面自带的页面控件来实现pdf文件的打印功能。

          点击下载Adobe Reader XI 11

     

    3、页面调用Adobe Reader自带的控件实现pdf打印

     

    <html>
    	<head>
    		<meta charset="UTF-8">
    		<title>pdf打印</title>
    	</head>
    	<body>
    		<div align="center">
    			<object id="pdfObj" classid="clsid:CA8A9780-280D-11CF-A24D-444553540000" width="950" height="562" border="0">
    				<param name="src" value="print/myfile.pdf" />
    			</object>
    		</div>
    	</body>
    </html>

     

     

          其中<param name="src" value="print/myfile.pdf" />  中的value是pdf文件的相对路径。

          需要注意的一点是,360浏览器打开页面时,必须用兼容模式,不然预期功能无法实现。

     

          下面是博主的案例

           Adobe Reader ActiveX控件 打印pdf文件案例1

     

          当我们移动鼠标时,页面上会浮现出一个工具栏:

     

           预览界面浮动工具栏

     

          这个浮动工具栏有5个按钮,他们的作用从左到右分别是:保存,打印,缩小,放大,显示工具栏。

     

          这里我们点击打印按钮,就可以弹出操作系统自带的打印配置页面了:

     

          打印配置页

          配置好相关属性后,点击打印即可成功打印pdf文件。

     

    4、关闭控件的下载功能,且保证用户只能打印1次pdf文件

     

          当有预览界面的时候,用户总能进行下载,所以我们可以去掉预览界面:将object控件中的width和height两个属性均设置为0

          去掉预览界面后,我们可以用js代码调用控件提供的相关接口进行打印,代码如下:

     

    <html>
    	<head>
    		<meta charset="UTF-8">
    		<title>pdf打印</title>
    	</head>
    	<body>
    		<div align="center">
    			<button id="prtBtn" onclick="printPage()">打印(P)</button>
    			<object id="pdfObj" classid="clsid:CA8A9780-280D-11CF-A24D-444553540000" width="0" height="0" border="0">
    				<param name="src" value="print/myfile.pdf" />
    			</object>
    		</div>
    		<script type="text/javascript">
    		function printPage(){
    			try {
    				 pdfObj.PrintAll();
    				 document.getElementById("prtBtn").setAttribute("disabled", true);
    			} catch (e) {
    				alert("未安装adobe reader插件,请联系管理员安装!");
    			}
    		}
    		</script>
    	</body>
    </html>

     

     

          该页面只会显示一个打印按钮:

     

          Adobe Reader ActiveX控件 打印pdf文件案例2

     

          如此,点击打印之后,打印按钮变灰,且跳过打印配置页,直接打印pdf文件(这个时候打印会根据系统配置的默认打印机来打印)。
     

     

    附:Adobe Reader ActiveX控件对外发布的功能说明

     

          (参考:控件接口实现源码-英文版

          Adobe Reader ActiveX控件提供的Methods

     

          1、GetAcroPdfVersion:获取一个显示Acrobat ActiveX控件版本的值

          2、GoBackwardStack:如果前面的视图存在,则转到视图堆栈上的先前视图。前面的视图可能在不同的文档中

          3、GoForwardStack:如果下一个视图存在,则转到视图堆栈的下一个视图。下一个视图可能在另一个文档中

          4、GotoFirstPage:转到文档的第一页,保持页面内的当前位置和缩放级别

          5、GotoLastPage:进入文档的最后一页,保持页面内的当前位置和缩放级别

          6、GotoNextPage:如果存在,则进入文档的下一页。保持页面内的当前位置和缩放级别

          7、GotoPreviousPage:如果存在,则进入文档的前一页。保持页面内的当前位置和缩放级别

          8、LoadFile:打开并在浏览器中显示指定的文档

          9、PostMessage:(这个函数没有被Adobe记录下来)

          10、Print:根据用户对话框中选择的选项打印文档。这些选项包括嵌入式打印(在给定页面上的一个边界矩形内打印),以及对指定的打印机进行交互式打印。

          11、PrintAll:在不显示用户对话框的情况下打印整个文档。使用默认的打印机、页面设置和作业设置。

          12、PrintAllFit:在不显示用户对话框的情况下打印整个文档,如果需要,页面会缩小,以适应打印机中页面的可想象区域。使用默认的打印机、页面设置和作业设置。
          *注:其有1个参数,在打印文档时确定是否要缩放可映像区域。值为0表示不应该使用扩展,而正值值表明,如果需要,页面会缩小,以适应打印机中页面的可想象区域。

          13、PrintPages:打印指定的页面,而不显示用户对话框。使用默认的打印机、页面设置和作业设置。
          *注:其有2个参数,指定打印第几页到第几页

          14、PrintPagesFit:打印指定的页面,而不显示用户对话框。使用默认的打印机、页面设置和作业设置
          *注:其中第3个参数表示在打印文档时确定是否要缩放可映像区域。值为0表示不应该使用扩展,而正值值表明,如果需要,页面会缩小,以适应打印机中页面的可想象区域。

          15、PrintWithDialog:根据用户对话框中选择的选项打印文档。这些选项包括嵌入式打印(在给定页面上的一个边界矩形内打印),以及对指定的打印机进行交互式打印。

          16、SetCurrentHighlight:突出显示当前页面中指定的边界矩形中的文本选择。

          17、SetCurrentPage:转到文档中指定的页面。保持页面内的当前位置和缩放级别

          18、SetLayoutMode:根据指定的值设置页面视图的布局模式

          19、SetNamedDest:将页面视图更改为指定字符串中的指定目标

          20、SetPageMode:根据指定的值设置页面模式

          21、ShowScrollbars:确定滚动条是否会出现在文档视图中

          22、ShowToolbar:确定工具栏是否会出现在查看器中

          23、SetView:根据指定的字符串设置页面的视图

          24、SetViewRect:根据指定的坐标设置视图矩形(X偏移,Y偏移,宽度,高度)

          25、SetViewScroll:根据指定的字符串设置页面的视图。根据视图模式,页面要么滚动到右边,要么按偏移量指定的数量进行滚动。

          26、Zoom:根据指定的值设置放大率

          27、SetZoomScroll:根据指定的值设置放大率,并根据指定的数量水平和垂直滚动页面视图。

                                                                                                                                                   

     

    首次发博文,希望各位朋友积极评论,不吝赐教!

    展开全文
  • Jasperreport连续打印多个报表模板

    千次阅读 2012-09-19 19:51:58
    Jasperreport连续打印多个报表模板,这个问题一直困扰我很久。网上查了很多资料都没有具体的解决办法,通过查询jasperreport的API发现提供了解决这个问题的方法。以下是我的过程记录: 问题描述:一般报表设计和...
    Jasperreport连续打印多个报表模板,这个问题一直困扰我很久。网上查了很多资料都没有具体的解决办法,通过查询jasperreport的API发现提供了解决这个问题的方法。以下是我的过程记录:
    

    问题描述:一般报表设计和打印中,主要是对单个报表模板进行数据填充、打印、导出。但是,也有个别情况需要连续打印多个报表模板。比如,政府机关的上报材料,属于一个完整的文档。如果一个一个模板打印会显得非常麻烦。

    问题解决过程:一般我们导出报表文件是使用这个语句:
    exporter.setParamete(JRExporterParameter.JASPER_PRINT, jasperprint);但是它是对单个模板进行操作,通过查找jasperreport的API发现还提供了这样一个语句:
    exporter.setParameter(JRExporterParameter.JASPER_PRINT_LIST, jasperprint);可以连续对多个报表模板操作。它的方法是通过一个list来存储多个JasperPrint对象,从而实现多个报表模板的打印。在我解决问题的过程中发现导出PDF文件格式比较简单,但是导出WORD文档时发现编码格式出现问题,老是出现乱码。即使这样: exporter.setParameter(JRExporterParameter.CHARACTER_ENCODING, "GB2312");也没有用。后来浏览一篇贴子:“导出excel时出现乱码问题”,和我的问题差不多。终于大功告成!
    展开全文
  • 【自定义View】从零开始写一个PDF查看器 个安卓平台的 PDF 查看器,支持滑动、缩放、三级缓存、放大展示高清图、支持一些功能配置。 支持滑动、缩放、缩放后的滑动、缩放后的平移 支持放大后查看高清 PDF 页 支持...

    效果展示

    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

    Android 平台已有的解决方案

    目前 android 平台上查看 pdf 的方案还是有很多可选的:

    1. 编写本地 html,引入 pdf.js,利用 WebView 去渲染 pdf
    2. 集成 AndroidPdfViewer 或其他类似的 library,使用第三方 so 库去处理 pdf 并渲染
    3. 自定义 View 结合系统原生的 pdf 处理类 PdfRenderer 进行 pdf 渲染

    以上几种方式,各自都有优缺点。
    使用 WebView 方式去渲染是最简单直接的,兼容性也好,但是当渲染比较大的 pdf 文档的时候会 oom;
    第三方的 so 库渲染,通常兼容性会比较好,稳定性也比较好,但是由于使用到 so 库,会显著增加 apk 的包体积;
    使用系统的 PdfRenderer + 自定义 View 来渲染 pdf,由于 pdf 渲染是由系统原生处理的,不需要额外的 so 包,所以不会显著增加 apk 的体积,也比较稳定,但是这个类是 API 21 后加入的,所以它只支持 android 5.0+。

    自定义 View 做 PDF 查看器的优势

    上边已经说明了,其实各种方式都各有优缺点,选择哪种方案完全取决于你对项目的评估和所作的决策。
    我对 pdf 查看器的选择标准:

    1. 内存优化必须要好,拒绝 OOM
    2. 流畅、稳定、高可定制化
    3. 由于是简单查看 pdf,不希望因为它使 apk 的体积增加太多

    以上边的标准,WebView 方案第一个排除掉,WebView 方式可控性太低了,打开一个 WebView 页面也比较慢;第三方的 so 库渲染,缺点就是 apk 包体积会增大,其他各方面都很优秀,使用起来也很方便,如果项目主要任务就是 pdf 相关的,那增大包体积也可以接受。
    自定义 View + PdfRenderer 来实现 pdf 查看器的优势在于,系统原生渲染 pdf,安全可靠稳定,不会额外增加安装包体积;自定义 View 处理 pdf 的显示和交互逻辑,自由程度比较大;从零写一个 pdf 查看器,对自己也会有很大的收获。

    开始自定义 PDF 查看器

    写一个复杂的自定义 View,看起来比较复杂比较难,所有要实现功能交织在一起极其复杂,通常都会一头雾水没有思路,不知道该从哪里下手。其实如果换个角度去写,它实现起来也没那么难。先想想都要实现哪些功能,然后分步骤分阶段一步一步去做,等所有步骤都实现了,自定义 View 也就成了。

    总体规划、步骤分解

    我们需要这样一个 pdf 查看器,支持滑动、支持多点触控时缩放、支持放大后查看 pdf 高清页,支持添加水印。
    这里先来头脑风暴一下,考虑滑动流畅内存优化。pdf 页的渲染需要在滑动停止时进行,同时加载的 pdf 页的个数也需要限制(当前显示的页+预加载的页);pdf 页转换 Bitmap 的过程是个耗时操作,需要在后台线程处理;滑动状态和停止滑动状态会频繁的切换,会频繁的去渲染 pdf 页,因此需要线程池去管理 pdf 转换 Bitmap 的后台线程任务;可以把转换过的 pdf 页对应的 Bitmap 做一个三级缓存,这样再次加载时就不用再从 pdf 渲染了;考虑到运行内存是有限的,所以三级缓存中的内存缓存需要限制个数,就跟预加载 pdf 页的个数保持一致好了;放大后渲染高清 pdf,只渲染当前在屏幕上可见的部分就行。
    我们来总结一下上边提到的技术点:

    1. 支持设置 pdf 页的预加载个数
    2. 滑动状态为空闲时,开启 pdf 转 Bitmap 的渲染任务渲染当前页和预加载页
    3. 要有个线程池处理耗时任务
    4. pdf 页的 Bitmap 资源需要做个三级缓存
    5. 放大查看高清 pdf 时,只渲染屏幕上显示的内容的高清 Bitmap

    大致的需求和功能点都有了,接下来分步骤来完成:

    1. 绘制占位空白页
    2. 实现滑动
    3. 绘制 PDF 内容页
    4. 实现缩放
    5. 缩放后加载高清 PDF
    6. 绘制水印

    PdfRenderer 相关 API 介绍

    在开始之前,还需要了解一下需要用到的 PdfRenderer 的系统 api,看看它都能做些什么。

    PdfRenderer

    来看看官方文档对它的介绍:


    This class enables rendering a PDF document. This class is not thread safe.

    If you want to render a PDF, you create a renderer and for every page you want to render, you open the page, render it, and close the page. After you are done with rendering, you close the renderer. After the renderer is closed it should not be used anymore. Note that the pages are rendered one by one, i.e. you can have only a single page opened at any given time.


    翻译一下就是:


    这个类能渲染 pdf 文档,它不是线程安全的。

    如果想要渲染 pdf 文档,你需要创建一个渲染器,对每一个将要渲染的 pdf 页,你都要打开它,渲染它,然后关闭它。pdf 文档渲染完成后,还需要关闭渲染器。渲染器关闭后就不能再次使用这个渲染器了。有一点需要注意,所有的 pdf 页都是一个一个被渲染的,即同一时间只能打开一个 pdf 页


    构造函数:

    public PdfRenderer(@NonNull ParcelFileDescriptor input) throws IOException {
        ...
    }
    

    只有一个构造函数,传入 pdf 文件的文件描述符,获取 pdf 渲染器实例。

    方法名简介
    PdfRenderer#close()关闭当前 pdf 渲染器实例
    PdfRenderer#getPageCount()获取 pdf 页的总数
    PdfRenderer#openPage(index: Int)打开目标 pdf 页

    PdfRenderer.Page

    方法名简介
    Page#close()关闭当前 pdf 页
    Page#getHeight()获取当前 pdf 页的高度
    Page#getWidth()获取当前 pdf 页的宽度
    Page#getIndex()获取当前 pdf 页在 pdf 文档中的索引
    Page#render(destination: Bitmap, destClip: Rect?, transform: Matrix?, renderMode: Int)把 pdf 页渲染到 destination 这个 Bitmap 中

    Page#render(destination: Bitmap, destClip: Rect?, transform: Matrix?, renderMode: Int)
    这个方法很关键,它有4个参数:

    • destination:目标 Bitmap,pdf 页的内容会渲染到它里边
    • destClip:目标裁剪,是一个矩形,它作用在 destination 上,如果设置了这个参数,destination 只会显示 destClip 矩形区域内的 pdf 内容
    • transform:变换,可以在渲染到 Bitmap 前,对 pdf 源矩阵数据设置缩放和平移,使得到的 Bitmap 符合显示预期
    • renderMode:渲染模式,显示模式-RENDER_MODE_FOR_DISPLAY、打印模式-RENDER_MODE_FOR_PRINT

    参数还有需要注意的点:
    destination 目标 Bitmap 的格式必须是 Config#ARGB_8888
    transform 变换矩阵只能设置缩放、旋转、平移,不能设置透视变换
    详细的官方文档介绍看这里

    了解上边的渲染方法后,其实 PDF 文件的显示原理就是:首先把每一页 PDF 都转换成对应的 Bitmap;然后就是把 Bitmap 绘制到画布上,就完成了 PDF 文档的查看。

    到这里 PDF 显示的问题,已经转化成了 Bitmap 显示的问题了,那么问题来了,OOM它迎面走来了~。一个 PDF 文档在内存中相当于一个 Bitmap 对象的列表,一个几十上百页的 PDF 文档就相当于一个存了几十上百个 Bitmap 的列表,如果一股脑的把这个列表放到内存里,想象一下,有画面了吧。。

    所以我们需要优化这个过程,绝对不能一次性把全部的 PDF 页都渲染出来(直接崩了也渲染不出来)。

    Step1.绘制占位空白页

    一个 PDF 文档,我们可以获取到它的页数量,每页的宽高信息,一次性渲染所有 PDF 页到内存不可行,那就先一次性把所有 PDF 页将要渲染在画布上的位置宽高信息的列表创建出来。
    占位位置数据的创建,使用后台线程去处理,处理完后 Handler 发通知转到 UI 线程

    1. 定义创建pdf页框架数据的任务
      占位空白页的 Rect 数据,可以通过循环从 PDF 渲染器中取出的 Page 对象来创建。
      PDF 页的原始宽高不会刚好跟屏幕宽度一样的,所以还需要缩放到屏幕宽度。
      在循环保存 Page 宽高信息的时候,直接就把 Rect 在画布的位置规定好(通过累加的pdfTotalHeight
      每页保存宽高信息前,需要预留 Page 页分隔线的高度信息
      /**
       * 线程任务
       * 初始化pdf页框架数据
       */
      private class InitPdfFramesTask(pdfView: PDFView) : Runnable {
          private val mWeakReference = WeakReference(pdfView)
          override fun run() {
              val pdfView = mWeakReference.get() ?: return
              val pdfRenderer =
                  pdfView.mPdfRenderer ?: throw NullPointerException("pdfRenderer is null!")
              val tempPagePlaceHolders = arrayListOf<PageRect>()
      
              var pdfTotalHeight = 0f
              val left = pdfView.paddingLeft.toFloat() + pdfView.mDividerHeight
              val right =
                  pdfView.measuredWidth.toFloat() - pdfView.paddingRight.toFloat() - pdfView.mDividerHeight
              var fillWidthScale: Float
              var scaledHeight: Float
              for (index in 0 until pdfRenderer.pageCount) {
                  val page = pdfRenderer.openPage(index)
                  fillWidthScale = (right - left) / page.width.toFloat()
                  scaledHeight = page.height * fillWidthScale
                  //预留分割线的高度
                  if (index != 0)
                      pdfTotalHeight += pdfView.mDividerHeight
                  val rect = RectF(left, pdfTotalHeight, right, pdfTotalHeight + scaledHeight)
                  pdfTotalHeight = rect.bottom
                  tempPagePlaceHolders.add(
                      PageRect(
                          fillWidthScale,
                          rect
                      )
                  )
                  page.close()
              }
      
              val message = Message()
              message.what =
                  PDFHandler.MESSAGE_INIT_PDF_PLACE_HOLDER
              message.data.putParcelableArrayList("list", tempPagePlaceHolders)
              pdfView.mPDFHandler.sendMessage(message)
          }
      }
      
    2. 线程池执行创建pdf页框架数据的任务
      在 View 测量完成后去开启任务,这时 View 的宽高已经确定了,可以处理后续的数据
      override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
          ...
          //初始化pdf页框架
          if (mInitPageFramesFuture == null)
              mInitPageFramesFuture = EXECUTOR_SERVICE.submit(
                  InitPdfFramesTask(this)
              )
      }
      
    3. 画 pdf 页的占位框架
      override fun onDraw(canvas: Canvas) {
          super.onDraw(canvas)
          ...
          //画占位图和分隔线
          drawPlaceHolderAndDivider(canvas)
          ...
      }
      
      /**
       * 画page占位图和横向分隔线
       */
      private fun drawPlaceHolderAndDivider(canvas: Canvas) {
          mPagePlaceHolders.forEachIndexed { index, pageRect ->
              val fillWidthRect = pageRect.fillWidthRect
              //画占位页
              canvas.drawRect(fillWidthRect, mPDFPaint)
              //画页分隔
              if (index < mPagePlaceHolders.lastIndex)
                  canvas.drawRect(
                      paddingLeft.toFloat(),
                      fillWidthRect.bottom,
                      measuredWidth - paddingRight.toFloat(),
                      fillWidthRect.bottom + mDividerHeight,
                      mDividerPaint
                  )
          }
      }
      

    Step2.实现滑动

    完成第一步后,现在画布上已经有 PDF 页的框架了,先画框架是为了把画布给撑起来,虽然没有具体的 PDF 内容,但是有了占坑的宽高和坐标信息,可以接着往下做滑动了。

    滑动通常有两种:手指触摸滑动松开手指后的飞速滑动。要做到人性化的滑动体验,这两种滑动都要处理;还要注意滑动边界的处理。

    1. 滑动边界设置
      滑动是画布平移来实现的,即可平移的范围

      /**
       * 获取x轴可平移的间距
       */
      private fun getCanTranslateXRange(): Range<Float> {
          return Range(min(-(mCanvasScale * mPdfTotalWidth - width), 0f), 0f)
      }
      
      /**
       * 获取y轴可平移的间距
       */
      private fun getCanTranslateYRange(): Range<Float> {
          return Range(min(-(mCanvasScale * mPdfTotalHeight - height), 0f), 0f)
      }
      
    2. 创建 GestureDetector 处理滑动
      GestureDetector 已经为我们识别了滑动状态 onScroll 和飞速滑动 onFling
      onScroll 在触摸滑动时会一直回调,计算 transation 触发重绘就可以滑动
      onFling 在手指离开屏幕后,如果此时滑动速度到达飞速滑动的标准,就会回调一次,根据速度计算目标滑动位置,然后创建值动画,动画更新时计算 transation 触发重绘实现滑动

      //处理飞速滑动的手势识别器
      private val mGestureDetector by lazy {
          GestureDetector(
              context,
              OnPDFGestureListener(this)
          )
      }
      
      /**
       * 处理触摸滑动和飞速滑动
       * P.S. 只处理单点触摸的操作
       */
      private class OnPDFGestureListener(pdfView: PDFView) :
          GestureDetector.SimpleOnGestureListener() {
          private val mWeakReference = WeakReference(pdfView)
      
          override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
              mWeakReference.get()?.performClick()
              return true
          }
          
          //处理触摸滑动
          override fun onScroll(
              e1: MotionEvent?,
              e2: MotionEvent?,
              distanceX: Float,
              distanceY: Float
          ): Boolean {
              val pdfView = mWeakReference.get() ?: return false
      
              //判断滑动边界,重新设置滑动值
              val canTranslateXRange = pdfView.getCanTranslateXRange()
              val canTranslateYRange = pdfView.getCanTranslateYRange()
              val tempTranslateX = pdfView.mCanvasTranslate.x - distanceX
              val tempTranslateY = pdfView.mCanvasTranslate.y - distanceY
              val nextTranslateX = when {
                  tempTranslateX in canTranslateXRange -> tempTranslateX
                  tempTranslateX > canTranslateXRange.upper -> canTranslateXRange.upper
                  else -> canTranslateXRange.lower
              }
              val nextTranslateY = when {
                  tempTranslateY in canTranslateYRange -> tempTranslateY
                  tempTranslateY > canTranslateYRange.upper -> canTranslateYRange.upper
                  else -> canTranslateYRange.lower
              }
      
              //3.开始滑动,重绘
              pdfView.mCanvasTranslate.set(nextTranslateX, nextTranslateY)
              pdfView.invalidate()
              //4.重新计算当前页索引
              pdfView.calculateCurrentPageIndex()
              pdfView.debug("onScroll-distanceX:${distanceX}-distanceY:${distanceY}")
              //5. 滑动结束监听回调,创建page位图数据(需要再 onTouchEvent 中判断滑动结束,所以这里返回 false)
              return false
          }
          
          //处理松开手指飞速滑动
          override fun onFling(
              e1: MotionEvent?,
              e2: MotionEvent?,
              velocityX: Float,
              velocityY: Float
          ): Boolean {
              val pdfView = mWeakReference.get() ?: return false
              mWeakReference.get()?.debug("onFling-velocityX:${velocityX}-velocityY:${velocityY}")
              if (
                  e1 != null && e2 != null
                  && (abs(e1.x - e2.x) > 100 || abs(e1.y - e2.y) > 100)
                  && (abs(velocityX) > 500 || abs(velocityY) > 500)
              ) {
                  val canTranslateXRange = pdfView.getCanTranslateXRange()
                  val canTranslateYRange = pdfView.getCanTranslateYRange()
                  val tempTranslateX = pdfView.mCanvasTranslate.x + velocityX * 0.75f
                  val tempTranslateY = pdfView.mCanvasTranslate.y + velocityY * 0.75f
                  val endTranslateX = when {
                      tempTranslateX in canTranslateXRange -> tempTranslateX
                      tempTranslateX > canTranslateXRange.upper -> canTranslateXRange.upper
                      else -> canTranslateXRange.lower
                  }
                  val endTranslateY = when {
                      tempTranslateY in canTranslateYRange -> tempTranslateY
                      tempTranslateY > canTranslateYRange.upper -> canTranslateYRange.upper
                      else -> canTranslateYRange.lower
                  }
      
                  val distanceX = endTranslateX - pdfView.mCanvasTranslate.x
                  val distanceY = endTranslateY - pdfView.mCanvasTranslate.y
      
                  pdfView.startFlingAnim(distanceX, distanceY)
                  return true
              }
      
              return super.onFling(e1, e2, velocityX, velocityY)
          }
      }
      
    3. onTouchEvent 中合适的时机调用手势识别器

      override fun onTouchEvent(event: MotionEvent): Boolean {
          ...
          var handled = false
          when (event.actionMasked) {
              MotionEvent.ACTION_DOWN -> {
                  ...
                  mTouchState = TouchState.SINGLE_POINTER
                  mGestureDetector.onTouchEvent(event)
                  handled = true
              }
              ...
              MotionEvent.ACTION_MOVE -> {
                  ...
                  handled = when (mTouchState) {
                      TouchState.SINGLE_POINTER -> mGestureDetector.onTouchEvent(event)
                      ...
                      else -> false
                  }
              }
              MotionEvent.ACTION_UP -> {
                  ...
                  handled = when (mTouchState) {
                      TouchState.SINGLE_POINTER -> {
                          val isFling = mGestureDetector.onTouchEvent(event)
                          ...
                          true
                      }
                      ...
                      else -> false
                  }
                  mTouchState = TouchState.IDLE
              }
          }
          return handled || super.onTouchEvent(event)
      }
      
    4. 飞速滑动时的滑动动画
      在手势识别器中,识别到飞速滑动后,计算目标滑动位置,然后使用值动画平滑动滑动到目标位置

      /**
       * 开始飞速滑动的动画
       */
      private fun startFlingAnim(distanceX: Float, distanceY: Float) {
          //根据每毫秒20像素来计算动画需要的时间
          var animDuration = (max(abs(distanceX), abs(distanceY)) / 20).toLong()
          //时间最短不能小于100毫秒
          when (animDuration) {
              in 0 until 100 -> animDuration = 400
              in 100 until 600 -> animDuration = 600
          }
      
          debug("startFlingAnim--distanceX-$distanceX--distanceY-$distanceY--animDuration-$animDuration")
          mFlingAnim = ValueAnimator().apply {
              setFloatValues(0f, 1f)
              duration = animDuration
              interpolator = DecelerateInterpolator()
              addUpdateListener(
                  PDFFlingAnimUpdateListener(
                      this@PDFView,
                      distanceX,
                      distanceY
                  )
              )
              ...
              start()
          }
      }
      
      /**
       * 飞速滑动动画更新回调
       */
      private class PDFFlingAnimUpdateListener(
          pdfView: PDFView,
          private val distanceX: Float,
          private val distanceY: Float
      ) : ValueAnimator.AnimatorUpdateListener {
          private val mWeakReference = WeakReference(pdfView)
          private val lastCanvasTranslate = PointF(
              pdfView.mCanvasTranslate.x,
              pdfView.mCanvasTranslate.y
          )
      
          override fun onAnimationUpdate(animation: ValueAnimator) {
              val pdfView = mWeakReference.get() ?: return
      
              //飞速滑动时,不渲染缩放的 bitmap
              pdfView.clearScalingPages()
      
              val percent = animation.animatedValue as Float
              pdfView.mCanvasTranslate.x = lastCanvasTranslate.x + distanceX * percent
              pdfView.mCanvasTranslate.y = lastCanvasTranslate.y + distanceY * percent
              pdfView.invalidate()
              //重新计算当前页索引
              pdfView.calculateCurrentPageIndex()
          }
      }
      
    5. 绘制画布平移实现滑动

      override fun onDraw(canvas: Canvas) {
          super.onDraw(canvas)
          ...
          //平移缩放
          preDraw(canvas)
          ...
      }
      
      /**
       * 处理画布的平移缩放
       */
      private fun preDraw(canvas: Canvas) {
          canvas.translate(mCanvasTranslate.x, mCanvasTranslate.y)
          ...
      }
      

    至此,我们已经处理完滑动了,下一步就要真正的绘制 PDF 页的内容了,为了使 View 使用时流畅,需要在滑动和飞速滑动结束后,再去渲染要显示的 PDF 页。

    Step3.绘制 PDF 内容页

    当滑动停止时,需要创建后台任务去把要显示的 Page 渲染到 Bitmap。这个过程是个耗时操作,除了放在线程池里,还需要把转换的 Bitmap 缓存下来,内存缓存 LruCache 和磁盘缓存 DiskLruCache
    关于内存缓存,由于 Bitmap 比较占内存,内存缓存需要限制数量,即预加载的个数。
    预加载的逻辑按照 ViewPager 的预加载逻辑来做,即(当前显示页+当前页前预加载个数+当前页后预加载个数)

    1. 创建渲染 PDF 的任务

      /**
       * 线程任务
       * 创建要显示的pdf页的bitmap集合
       * 有本地缓存的话用本地缓存,没有缓存的话从pdf生成bitmap后缓存
       */
      private class PdfPageToBitmapTask(pdfView: PDFView) : Runnable {
          private val mWeakReference = WeakReference(pdfView)
          override fun run() {
              ...
              for (index in startLoadingIndex..endLoadingIndex) {
                  val pageRect = pagePlaceHolders[index]
                  val fillWidthRect = pageRect.fillWidthRect
                  var bitmap = pdfView.getLoadingPagesBitmapFromCache(index)
                  if (bitmap == null) {
                      //3.本地缓存没拿到,从pdf渲染器创建bitmap
                      val page = pdfRenderer.openPage(index)
                      bitmap = Bitmap.createBitmap(
                          (fillWidthRect.width() / pageRect.fillWidthScale).toInt(),
                          (fillWidthRect.height() / pageRect.fillWidthScale).toInt(),
                          Bitmap.Config.ARGB_8888
                      )
                      page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                      page.close()
                      //新创建的bitmap,存到内存缓存和本地缓存
                      pdfView.putLoadingPagesBitmapToCache(index, bitmap)
                  }
                  tempLoadingPages.add(
                      DrawingPage(
                          pageRect,
                          bitmap,
                          index
                      )
                  )
              }
              val message = Message()
              ...
              pdfView.mPDFHandler.sendMessage(message)
          }
      }
      
    2. 开启渲染 PDF 的任务

      override fun onTouchEvent(event: MotionEvent): Boolean {
          ...
          var handled = false
          when (event.actionMasked) {
              ...
              MotionEvent.ACTION_UP -> {
                  ...
                  handled = when (mTouchState) {
                      TouchState.SINGLE_POINTER -> {
                          val isFling = mGestureDetector.onTouchEvent(event)
                          if (!isFling) {
                              //单指滑动结束,处理滑动结束(无飞速滑动的情况)
                              submitCreateLoadingPagesTask()
                          }
                          true
                      }
                      ...
                      else -> false
                  }
                  mTouchState = TouchState.IDLE
              }
          }
          return handled || super.onTouchEvent(event)
      }
      
      /**
       * 提交创建pdf页的任务到线程池
       * 如果线程池里有未执行或正在执行的任务,取消那个任务
       */
      private fun submitCreateLoadingPagesTask() {
          if (mCreateLoadingPagesFuture?.isDone != true)
              mCreateLoadingPagesFuture?.cancel(true)
          mCreateLoadingPagesFuture =
              EXECUTOR_SERVICE.submit(
                  PdfPageToBitmapTask(this)
              )
      }
      
    3. 绘制 PDF 内容页

      override fun onDraw(canvas: Canvas) {
          super.onDraw(canvas)
          ...
          //画将要显示的完整page
          drawLoadingPages(canvas)
          ...
      }
      
      /**
       * 画完整显示的pdf页面
       */
      private fun drawLoadingPages(canvas: Canvas) {
          mLoadingPages.filter { page ->
              //即将缩放显示的页面,不绘制它的全页bitmap
              val isScaling = page.pageIndex in mScalingPages.map { it.pageIndex }
              page.pageRect?.fillWidthRect != null
                      && page.bitmap != null
                      && !isScaling
          }
              .forEach {
                  val fillWidthScale = it.pageRect!!.fillWidthScale
                  val fillWidthRect = it.pageRect.fillWidthRect
                  canvas.save()
                  canvas.translate(fillWidthRect.left, fillWidthRect.top)
                  canvas.scale(fillWidthScale, fillWidthScale)
                  canvas.drawBitmap(it.bitmap!!, 0f, 0f, mPDFPaint)
                  canvas.restore()
              }
      }
      

    此时一个 PDF 查看器已经基本完成了,支持滑动、三级缓存、滑动与Page渲染优化。但显然只是能用还是不够的,接下来继续添加缩放、水印的支持。

    Step4.实现缩放

    onTouchEvent 中,之前我们用的 GestureDetector 来处理的滑动事件,滑动事件已经从 onTouchEvent 中剥离;缩放是多点触控,还需要把多点触控的触摸事件从 onTouchEvent 中剥离出来。
    缩放的实现方式,是通过画布的平移+缩放来实现,根据双指的移动距离,计算出一个缩放倍数和缩放前需要平移的距离,然后在 onDraw 中去完成缩放。

    1. onTouchEvent 剥离多点触控触摸事件

      override fun onTouchEvent(event: MotionEvent): Boolean {
          ...
          var handled = false
          when (event.actionMasked) {
              MotionEvent.ACTION_POINTER_DOWN -> {
                  debug("onTouchEvent-ACTION_POINTER_DOWN")
                  mTouchState =
                      TouchState.MULTI_POINTER
                  //如果有正在执行的 fling 动画,就重置动画
                  stopFlingAnimIfNeeded()
                  handled = onZoomTouchEvent(event)
              }
              MotionEvent.ACTION_MOVE -> {
                  debug("onTouchEvent-ACTION_MOVE")
                  handled = when (mTouchState) {
                      ...
                      TouchState.MULTI_POINTER -> onZoomTouchEvent(event)
                      else -> false
                  }
              }
              MotionEvent.ACTION_UP -> {
                  debug("onTouchEvent-ACTION_UP")
                  handled = when (mTouchState) {
                      ...
                      TouchState.MULTI_POINTER -> onZoomTouchEvent(event)
                      else -> false
                  }
                  mTouchState = TouchState.IDLE
              }
          }
          return handled || super.onTouchEvent(event)
      }
      
    2. 处理多点触控事件,计算平移和缩放倍数

      /**
       * 多指触摸,处理缩放
       */
      private fun onZoomTouchEvent(event: MotionEvent): Boolean {
          //如果没开启缩放,就不处理多点触控
          if (!mCanZoom) return false
      
          when (event.actionMasked) {
              MotionEvent.ACTION_POINTER_DOWN -> {
                  debug("onZoomTouchEvent-ACTION_POINTER_DOWN")
                  //记录多点触控按下时的初始手指间距
                  mMultiFingerDistanceStart =
                      distance(
                          event.getX(0),
                          event.getX(1),
                          event.getY(0),
                          event.getY(1)
                      )
                  //记录按下时的缩放倍数
                  mScaleStart = mCanvasScale
                  //记录按下时的画布中心点
                  mMultiFingerCenterPointStart.set(
                      (event.getX(0) + event.getX(1)) / 2,
                      (event.getY(0) + event.getY(1)) / 2
                  )
                  mZoomTranslateStart.set(mCanvasTranslate)
      
                  return mCanZoom
              }
              MotionEvent.ACTION_MOVE -> {
                  debug("onZoomTouchEvent-ACTION_MOVE")
                  if (event.pointerCount < 2) return false
                  val multiFingerDistanceEnd =
                      distance(
                          event.getX(0),
                          event.getX(1),
                          event.getY(0),
                          event.getY(1)
                      )
                  val tempScale = (multiFingerDistanceEnd / mMultiFingerDistanceStart) * mScaleStart
                  mCanvasScale = when (tempScale) {
                      in 0f..mMinScale -> mMinScale
                      in mMinScale..mMaxScale -> tempScale
                      else -> mMaxScale
                  }
      
                  val centerPointEndX = (event.getX(0) + event.getX(1)) / 2
                  val centerPointEndY = (event.getY(0) + event.getY(1)) / 2
      
                  val vLeftStart: Float = mMultiFingerCenterPointStart.x - mZoomTranslateStart.x
                  val vTopStart: Float = mMultiFingerCenterPointStart.y - mZoomTranslateStart.y
                  val vLeftNow: Float = vLeftStart * (mCanvasScale / mScaleStart)
                  val vTopNow: Float = vTopStart * (mCanvasScale / mScaleStart)
      
                  //判断滑动边界,重新设置滑动值
                  val canTranslateXRange = getCanTranslateXRange()
                  val canTranslateYRange = getCanTranslateYRange()
                  val tempTranslateX = centerPointEndX - vLeftNow
                  val tempTranslateY = centerPointEndY - vTopNow
                  val nextTranslateX = when {
                      tempTranslateX in canTranslateXRange -> tempTranslateX
                      tempTranslateX > canTranslateXRange.upper -> canTranslateXRange.upper
                      else -> canTranslateXRange.lower
                  }
                  val nextTranslateY = when {
                      tempTranslateY in canTranslateYRange -> tempTranslateY
                      tempTranslateY > canTranslateYRange.upper -> canTranslateYRange.upper
                      else -> canTranslateYRange.lower
                  }
                  mCanvasTranslate.set(nextTranslateX, nextTranslateY)
                  invalidate()
                  //重新计算当前页索引
                  calculateCurrentPageIndex()
                  return true
              }
              MotionEvent.ACTION_UP -> {
                  debug("onZoomTouchEvent-ACTION_UP")
                  submitCreateLoadingPagesTask()
                  return true
              }
          }
          return false
      }
      
      
    3. 绘制缩放

      override fun onDraw(canvas: Canvas) {
          super.onDraw(canvas)
          ...
          //平移缩放
          preDraw(canvas)
          ...
      }
      
      /**
       * 处理画布的平移缩放
       */
      private fun preDraw(canvas: Canvas) {
          canvas.translate(mCanvasTranslate.x, mCanvasTranslate.y)
          canvas.scale(mCanvasScale, mCanvasScale)
      }
      

    缩放的逻辑中,在 ACTION_MOVE 时计算平移量和缩放倍数最为复杂,不好描述,需要自己参透。。

    到这里 PDFView已经支持缩放了,但是现在的缩放只是缩放了画布,查看的还是画布之前已有的内容缩放后的效果,这就不可避免的会很模糊。好在渲染 PDF 页面的方法可以设置缩放、平移的参数,这样就可以从 PDF 中重新渲染高清的 Bitmap 了。

    Step5.缩放后加载高清 PDF

    PDF 页的渲染是一个很耗时的操作,缩放后的 PDF 页的渲染,如果要渲染整页内容,会更加耗时,而且 Bitmap 也会很大,所以渲染高清 PDF 内容这个过程,也需要考虑内存优化。

    手机屏幕可显示的区域是固定的,其他不在显示区域内的 PDF 内容页,我们是看不见的,并且我们只需要在缩放结束后去加载高清 PDF 内容页,而在缩放或滑动过程中,继续显示之前画布放大的模糊内容。

    那么方案就来了:
    缩放结束后,根据此时画布 x/y 轴的平移量和屏幕的宽高,可以知道屏幕窗口相对于 PDF 内容的位置信息;
    根据当前显示的 PDF 页的位置信息和屏幕窗口的位置信息,可以知道屏幕窗口中各 PDF 页需要放大和平移部分的 Rect
    然后使用 PDF 渲染器提供的 API 去渲染出放大的 PDF 页矩形区域;
    把拿到的放大过得 PDF 矩形区域的 Bitmap 绘制到画布上,因为 Bitmap 已经是放大过得了,所以绘制前需要把画布重置为原始状态,清除画布的平移缩放后再画。

    1. 创建渲染高清 PDF 页的任务

      /**
       * 线程任务
       * 创建正在缩放的显示的pdf页的bitmap
       */
      private class CreateScalingPageBitmapTask(pdfView: PDFView) : Runnable {
          private val mWeakReference = WeakReference(pdfView)
          override fun run() {
              ...
              for (index in startIndex..endIndex) {
                  val placeHolderPageRect = pagePlaceHolders[index]
                  val fillWidthRect = placeHolderPageRect.fillWidthRect
      
                  //创建缩放bitmap的位置信息
                  val scalingRectTop =
                      max(fillWidthRect.top * pdfView.mCanvasScale - abs(currentTranslateY), 0f)
                  val scalingRectBottom =
                      min(
                          fillWidthRect.bottom * pdfView.mCanvasScale - abs(currentTranslateY),
                          pdfView.measuredHeight - pdfView.paddingTop - pdfView.paddingBottom.toFloat()
                      )
      
                  //处理滑动到分隔线停止的情况
                  if (scalingRectBottom <= scalingRectTop) continue
      
                  val scalingRect = RectF(
                      0f,
                      scalingRectTop,
                      pdfView.measuredWidth - pdfView.paddingLeft - pdfView.paddingRight.toFloat(),
                      scalingRectBottom
                  )
      
                  val bitmap = Bitmap.createBitmap(
                      scalingRect.width().toInt(),
                      scalingRect.height().toInt(),
                      Bitmap.Config.ARGB_8888
                  )
                  val matrix = Matrix()
                  //page页真实的缩放倍数=原始页缩放到屏幕宽度的缩放倍数*画布的缩放倍数
                  val scale = placeHolderPageRect.fillWidthScale * pdfView.mCanvasScale
                  matrix.postScale(scale, scale)
                  //平移,因为取的是已经缩放过的page页,所以平移量跟缩放后的画布平移量保持一致
                  matrix.postTranslate(
                      pdfView.mCanvasTranslate.x + (pdfView.paddingLeft + pdfView.mDividerHeight) * pdfView.mCanvasScale,
                      min(fillWidthRect.top * pdfView.mCanvasScale + currentTranslateY, 0f)
                  )
                  val page = pdfRenderer.openPage(index)
                  page.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                  page.close()
      
                  tempScalingPages.add(
                      DrawingPage(
                          PageRect(fillWidthRect = scalingRect),
                          bitmap,
                          index
                      )
                  )
              }
      
              val message = Message()
              message.what =
                  PDFHandler.MESSAGE_CREATE_SCALED_BITMAP
              message.data.putInt("index", currentPageIndex)
              message.data.putParcelableArrayList("list", tempScalingPages)
              pdfView.mPDFHandler.sendMessage(message)
          }
      }
      
    2. 开启渲染高清 PDF 的任务
      应该在缩放结束,手指离开屏幕后去开启加载高清的任务。这个思路没毛病,但是再回顾一下前边的步骤,绘制 PDF 内容页的时候,是先渲染的原始 PDF 页,然后在绘制时放大到屏幕的宽度的,这样在没有缩放的状态下,看到的 PDF 页内容可能是模糊的,体验会不那么完美。
      这里修改一下显示逻辑:滑动或缩放操作结束后,开启渲染 PDF 的任务去渲染预加载个数的 PDF 页;任务完成的同时再开启渲染高清 PDF 页 Rect 的任务。

      /**
       * 异步处理 pdf_page to bitmap
       */
      private class PDFHandler(pdfView: PDFView) : Handler() {
          ...
          private val mWeakReference = WeakReference(pdfView)
          override fun handleMessage(msg: Message) {
              super.handleMessage(msg)
              val pdfView = mWeakReference.get() ?: return
              when (msg.what) {
                  ...
                  MESSAGE_CREATE_LOADING_PDF_BITMAP -> {
                      pdfView.debug("handleMessage-MESSAGE_CREATE_LOADING_PDF_BITMAP-currentPageIndex:${pdfView.mCurrentPageIndex}")
                      val calculatedPageIndex = msg.data.getInt("index")
                      val tempLoadingPages = msg.data.getParcelableArrayList<DrawingPage>("list")
                      if (pdfView.mCurrentPageIndex != calculatedPageIndex) {
                          return
                      }
                      if (!tempLoadingPages.isNullOrEmpty()) {
                          pdfView.mLoadingPages.clear()
                          pdfView.mLoadingPages.addAll(tempLoadingPages)
                          pdfView.invalidate()
      
                          //渲染模糊页面成功后,再开始渲染屏幕上显示的pdf块的高清 bitmap
                          pdfView.submitCreateScalingPagesTask()
                      }
                  }
                  ...
              }
          }
      
      }
      
      /**
       * 提交创建pdf页的任务到线程池
       * 如果线程池里有未执行或正在执行的任务,取消那个任务
       */
      private fun submitCreateScalingPagesTask() {
          //只有在滑动状态为空闲的时候,才去创建缩放的 pdf 页的 bitmap
          if (mTouchState != TouchState.IDLE) return
      
          if (mCreateScalingPagesFuture?.isDone != true)
              mCreateScalingPagesFuture?.cancel(true)
          mCreateScalingPagesFuture =
              EXECUTOR_SERVICE.submit(
                  CreateScalingPageBitmapTask(this)
              )
      }
      

    Step6.绘制水印

    这里的加水印,是在 View 层面上去加,并不是添加到 PDF 文档里边了。
    由于是绘制在 View 层面,所以必须得绘制在 PDF 内容的顶层。因为如果 PDF 中有图片的话,绘制在底层就会被图片盖住(图片背景不透明)

    1. 初始化水印 Bitmap

      /**
       * 设置水印(暴露到外部的公开方法)
       */
      fun setWatermark(@DrawableRes waterMark: Int) {
          mWaterMark = BitmapFactory.decodeResource(resources, waterMark)
          mWaterMarkSrcRect.set(0, 0, mWaterMark!!.width, mWaterMark!!.height)
      }
      
      /**
       * 异步处理 pdf_page to bitmap
       */
      private class PDFHandler(pdfView: PDFView) : Handler() {
          ...
          private val mWeakReference = WeakReference(pdfView)
          override fun handleMessage(msg: Message) {
              super.handleMessage(msg)
              val pdfView = mWeakReference.get() ?: return
              when (msg.what) {
                  MESSAGE_INIT_PDF_PLACE_HOLDER -> {
                     pdfView.debug("handleMessage-MESSAGE_INIT_PDF_PLACE_HOLDER")
                      val tempPagePlaceHolders = msg.data.getParcelableArrayList<PageRect>("list")
                      if (!tempPagePlaceHolders.isNullOrEmpty()) {
                          ...
                          //初始化水印的目标绘制宽高
                          pdfView.initWatermarkDestRect()
                      }
                  }
              }
          }
      }
      
      private fun initWatermarkDestRect() {
          mWaterMark ?: return
          val destWidth = mPdfTotalWidth / 2f
          val scale = destWidth / mWaterMarkSrcRect.width()
          val destHeight = mWaterMarkSrcRect.height() * scale
          mWaterMarkDestRect.set(
              0f,
              0f,
              destWidth,
              destHeight
          )
      }
      
    2. 绘制水印
      绘制水印放在最后绘制,因为绘制高清 PDF 时,把平移缩放重置了,所以水印绘制前需要重新平移缩放到重置前的状态

      override fun onDraw(canvas: Canvas) {
          super.onDraw(canvas)
          ...
      
          //平移缩放
          preDraw(canvas)
          //画将要显示的完整page的水印
          drawLoadingWaterMarks(canvas)
      }
      
      /**
       * 画完整显示的pdf页面的水印
       */
      private fun drawLoadingWaterMarks(canvas: Canvas) {
          mWaterMark ?: return
      
          val left = (mPdfTotalWidth - mWaterMarkDestRect.width()) / 2
          mLoadingPages.filter { page ->
              page.pageRect?.fillWidthRect != null
                      && page.bitmap != null
          }
              .forEach {
                  val fillWidthRect = it.pageRect!!.fillWidthRect
                  mWaterMarkDestRect.offsetTo(
                      left,
                      fillWidthRect.centerY() - mWaterMarkDestRect.height() / 2
                  )
                  canvas.drawBitmap(mWaterMark!!, mWaterMarkSrcRect, mWaterMarkDestRect, mPDFPaint)
              }
      }
      

    源代码

    到这里,整个自定义 PDFView 的开发流程已经结束了。上边贴的代码只是为了配合编程思路和步骤介绍,并不是完整的代码。
    完整代码已开源,Github 传送门

    Q&A

    1. 用画布平移实现滑动而不是用View.scrollX/Y()

      最开始做的时候,我确实是用 View.scrollX/Y() 来实现滑动的,它来实现滑动很方便。但是到做缩放的时候,我觉得操作 View 好像不太合理。
      首先要了解一下 View.scrollX/Y()Canvas.translate() 分别操作的是什么。
      View.scrollX/Y() 是让 View 的内容去滚动,View 的内容都在画布上,即它是让画布的窗口去移动的;
      Canvas.translate() 是让画布窗口下层的画布去移动,从而使画布窗口上显示的内容发生移动的,在这个过程中画布窗口是固定不动的。
      再来说缩放后的画布处理,这里有两种思路:

      1. 滑动使用 View.scrollX/Y(),缩放使用 View.setScaleX/Y()
      2. 滑动使用 Canvas.translate(),缩放使用 Canvas.scale()

      有一点很重要,要么滑动和缩放都作用在 View 上,要么就都作用在 Canvas 上
      通常自定义 View 如果内容要移动或缩放的,都不会去更改 View 的宽高属性,而是在 onDraw 中以绘制来实现。也没见 ViewPager 的宽高根据数量不同而变化啊~
      所以在做到缩放时,我又回头重新用 Canvas.translate() 去现实滑动了。

    2. 缩放后绘制高清 PDF,为什么不把当前屏幕显示的 PDF 绘制到一个 Bitmap上?

      这个我本来是想只创建一个跟屏幕宽高一致的 Bitmap,然后循环渲染,使用 destClip 这个参数指定要渲染在 Bitmap 的位置,把缩放后显示在屏幕上的 PDF 页的矩形块绘制在同一个 Bitmap 上的。
      但是,这个参数其实并不是我当时理解的那个意思,它并不是作用在 PDF 上的,并不能指定要渲染 PDF 的哪一块内容。首先 PDF 的内容会先绘制在 Bitmap 上,然后 destClip 这个参数可以控制已经渲染过的 Bitmap 只显示哪一块内容。 destClip 是在渲染后控制显示的,这样的话循环完一遍后只会显示最后一个 PDF 块的内容,完成不了需求。。

    展开全文
  • 一直有个疑问,也是工作中遇到的。面对个别人发给你的PDF文件,现在想打印出来,能不能给各页加上页码,类似于Word中在页尾... 下面小编来解决大家的烦恼,小编知道一个PDF文件在线添加页码的网址,PDF添加页码htt...
  • 如何禁止pdf复制修改打印

    万次阅读 2018-07-12 16:54:52
    在很企事业单位,工作中都会形成一些比较重要的文件,处于商业机密保护的需要,有时候我们常常需要员工可以读取和查看这些文件,但不允许复制文件、禁止修改文件以及禁止打印文件。尤其是,很单位都是通过文件...
  • 比如提交审阅或交给甲方查看时层保障,也可以设置加密,限制查看、修改、打印,如果是不需要编辑,只需要查看、打印pdf 拥有体积小、所见即所得(打印也是如此)、查看方便(矢量)等优势。那怎么将设计好的...
  • 减小pdf大小 打印 低分辨率

    千次阅读 2013-05-31 20:07:14
    新闻 网页 贴吧 知道 音乐 图片 视频 地图 百科 文库 相册 吧内搜索|全吧搜索 pdf吧 图片 ...怎样缩小PDF文件的大小啊
  • CAD2020/2020 快速打印(批量打印PDF/DWF/JPG/PNG/TIFF-特色:自适应图纸尺寸 下载地址https://pan.baidu.com/s/12Ld9xdoqJL-zIi5Zj-haAQ 提取码: t6hu 仅支持CAD2019、CAD2020,命令栏输入 qw 启动打印程序; ...
  • 但在切換為整頁模式時,常造成了不少空白頁出來。第時間覺得不可思議,在ReportViewer看來正常,怎可能在整頁預覽時,格式會跑掉?如果在整頁預覽時格式跑掉,那在列印時也一定是這樣的情形。ReportViewer會將...
  • 2014年最新720套源码2.0GB免费一次性打包下载

    千次阅读 热门讨论 2014-07-07 22:35:44
    之前发过个帖子,但是那个帖子有点问题我就重新发个吧,下面的源码是我从今年3月份开始不断整理源码区和其他网站上的安卓例子源码,目前总共有720套左右,根据实现的功能被我分成了100多个类,总共2G多,还在...
  • 对于VBA的读者来说,这是本具有较强指导和实用的图书。 3图书前言 AutoCAD VBA二开发教程读者对象 本书适用于使用VBA和ActiveX技术对AutoCAD进行二开发的工程技术人员。 如果对AutoCAD VBA开发毫无概念...
  • 2014年最新810套源码2.46GB免费一次性打包下载

    千次阅读 热门讨论 2014-11-04 16:38:48
    之前发过个帖子,但是那个帖子有点问题我就重新发个吧,下面的源码是我从今年3月份开始不断整理源码区和其他网站上的安卓例子源码,目前总共有810套左右,根据实现的功能被我分成了100多个类,总共接近2.5G,还...
  • 之前发过个帖子,但是那个帖子有点问题我就重新发个吧,下面的源码是我从今年3月份开始不断整理源码区和其他网站上的安卓例子源码,目前总共有810套左右,根据实现的功能被我分成了100多个类,总共接近2.5G,还...
  • pdfFactory Pro精简优化特别版及注册机资源由挖软否网站精选收集互联网资源并整理相关Patch Crack KeyGen独家发布,pdfFactory Pro 中文破解版是款专业的PDF文件打印解决方案,使用它我们可以轻轻松松的打印PDF...
  • PDF能流行开来,功劳就在其兼容好,本世纪的设备基本都能打开、打印时所见即所得、传给别人不用担心格式丢失或出错……以至于我和同事曾经调侃:简单说,PDF 就是一张图片 不过,如果你想对 PDF 做一些超出「图片...
  • 由于我目前工作的特殊,在兼职这另外一家公司的筹建工作,一月下来,电子发票一大堆,报销一次时要打印十几页A4纸,感觉好浪费纸张。  不仅仅是我面临着这样的问题,如果是普通员工报销时打印发票,他们可能没...
  • 批量WORD转PDF转换器

    2018-07-02 08:48:36
    批量Word转PDF转换器是一款Windows平台下的Word转PDF转换工具,它可以方便快捷地一次性多个Word文件转换成PDF格式文件。由于软件抛弃了传统虚拟打印生成PDF的方法,而采用直接分析WORD文件并转换到PDF的技术,因此...
  • 批量打印发票

    2019-01-28 17:38:39
    这个是帮助那些一次性需要打多个发票的同志,会把所有的发票放到一个pdf文件上然后在进行打印
  • Cisdem PDF Password Remover for mac是一款功能强大的PDF解密工具。PDF Password Remover for mac可轻松...Cisdem PDF Password Remover还允许一次拖放最多可添加200个PDF文件。我们还高度优化了大型PDF文件的解...
  • 前言:有同学问我,如何把文件夹中的文件一次性完成打印,由于文件太,单个打印着实麻烦。这些文件主要有三种类型,分别为PDF,word(.doc和.docx),我决定把他们全部变为PDF文件,然后再合并所有的pdf文件为一...
  • 之前发过个帖子,但是那个帖子有点问题我就重新发个吧,下面的源码是我从今年开始不断整理源码区和其他网站上的安卓例子源码,目前总共有810套左右,根据实现的功能被我分成了100多个类,总共接近2.5G,还在不断...
  • [PDF文件全攻略]-PDF开发(.NET开发 C++开发 Java PHP) 2009-11-20 22:17:18 来源:参考在线 ...所谓PDF开发,无非就是根据PDF的文档标准,借助...这样你就可以编写一个自己用的PDF阅读器什么的,也可以将
  • IText生成PDF

    千次阅读 2017-02-06 17:17:42
    、场景在做单位OA项目的时候有功能,合同打印的功能,之前的想法是打印PDF。既然是打印PDF就需要用到PDF插件,java比较常用方便的插件有几种,我选择了IText,当然IText版本众多,也让我走了不少的弯路。二、...
  • JDK10 GC调优秘籍,一张PDF就能搞定的事情,还不快来看看!
  • 用数据说话:CAD批量打印的必要

    千次阅读 2018-09-11 09:09:13
    当然,有的朋友会说可以利用图纸集或者布局功能,这两功能在打印时确实比较方便,相对于普通的手动打印方式,其优势是设置好的纸张可以重复利用,但是前期要做的工作太复杂,所以现在设计院基本不使用布局和图纸集...
  • 程序员面试题精选100题:第1~37题集锦与总结,及免积分PDF下载前言程序员编程艺术第~三十七章集锦第章、左旋转字符串定义字符串的左旋转操作:把字符串前面的若干字符移动到字符串的尾部,如把字符串abcdef左...
  • 由于公司需要在业务系统展示文服的pdf,最后折腾发现pdf存在严重跨域问题。网上也是各种跨域解决方案,但是没有哪个真正合适或者说能实现的... 1、页面的body,这里放了个div,将会动态添加多个canvas,也就是显...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 20,844
精华内容 8,337
关键字:

一次性打印多个pdf