如何下载含RecyclerView的Android布局PDF,捕获全部可见与不可见数据?
解决RecyclerView生成PDF仅捕获可见内容的问题
问题描述
生成PDF时只能捕获RecyclerView当前可见的条目,无法导出所有数据,同时PDF范围仅局限于RecyclerView本身,无法包含整个布局内的用户信息、Logo等内容。
现有代码
Activity 代码
private fun createPdf() { val pdfDocument = PdfDocument() val pageInfo = PdfDocument.PageInfo.Builder( binding.downloadRecyler.width, binding.downloadRecyler.height, 1 ).create() val page = pdfDocument.startPage(pageInfo) val canvas = page.canvas binding.downloadRecyler.draw(canvas) pdfDocument.finishPage(page) val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) val pdfFileName = "${userName}_PDF_$timeStamp.pdf" val directory = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Attendance app" ) if (!directory.exists()) { directory.mkdirs() } val pdfFile = File(directory, pdfFileName) try { pdfFile.createNewFile() val outputStream = FileOutputStream(pdfFile) pdfDocument.writeTo(outputStream) pdfDocument.close() outputStream.close() sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(pdfFile))) Toast.makeText(this, getString(R.string.pdf_downloaded_successfully), Toast.LENGTH_SHORT).show() showDownloadNotification(pdfFile) } catch (e: IOException) { e.printStackTrace() Toast.makeText(this, "Error creating PDF", Toast.LENGTH_SHORT).show() } }
XML 布局代码
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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=".DownloadActivity"> <!-- ActionBar --> <FrameLayout android:id="@+id/actionBar" android:layout_width="match_parent" android:layout_height="?actionBarSize" android:layout_centerHorizontal="true" android:gravity="center"> <!-- Back Button --> <ImageView android:id="@+id/btnBack" android:layout_width="@dimen/_20sdp" android:layout_height="30dp" android:layout_gravity="center|start" android:layout_marginStart="@dimen/_5sdp" android:src="@drawable/left_arrow" /> <!-- Download Button --> <ImageView android:id="@+id/btnDownload" android:layout_width="30dp" android:layout_height="30dp" android:layout_gravity="center|end" android:layout_marginEnd="@dimen/_8sdp" android:src="@drawable/icon_download" /> </FrameLayout> <!-- Main Layout --> <LinearLayout android:id="@+id/layoutDownload" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/actionBar" android:orientation="vertical" android:layout_marginHorizontal="@dimen/_10sdp"> <!-- Logo and App Name --> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"> <ImageView android:layout_width="@dimen/_50sdp" android:layout_height="@dimen/_50sdp" android:src="@drawable/icon_logo_transparent" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/app_name" android:textSize="@dimen/_18ssp" /> </LinearLayout> <!-- User Information --> <TextView android:id="@+id/txtName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/actionBar" android:layout_marginStart="@dimen/_10sdp" android:text="Shivam" android:textAllCaps="true" android:textColor="@color/black" android:textSize="@dimen/_12ssp" android:textStyle="bold" /> <!-- Additional User Details --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/_10sdp" android:layout_marginBottom="@dimen/_10sdp" android:orientation="vertical"> <TextView android:id="@+id/txtPhone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="Shivam" android:textSize="@dimen/_10ssp" /> <TextView android:id="@+id/txtAge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="Shivam" android:textSize="@dimen/_10ssp" /> <TextView android:id="@+id/txtYear" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text="Shivam" android:textStyle="bold" android:textColor="@color/black" android:layout_marginTop="@dimen/_10sdp" android:textSize="@dimen/_10ssp" /> </LinearLayout> <!-- RecyclerView --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/downloadRecyler" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="-5dp" android:layout_marginBottom="@dimen/_10sdp"/> </LinearLayout> <!-- Empty Data Image --> <ImageView android:id="@+id/emptyDataImage" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/data_empty" /> <!-- ProgressBar --> <ProgressBar android:id="@+id/progreesBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> </RelativeLayout>
解决方案
核心问题分析
RecyclerView.draw(canvas)仅绘制当前屏幕可见视图,因RecyclerView的复用机制,不可见条目未实例化或已被回收。- 原代码仅以RecyclerView的宽高创建PDF页面,无法包含顶部用户信息、Logo等完整布局内容。
步骤1:绘制完整主布局
将PDF页面的宽高改为整个主布局layoutDownload的尺寸,并绘制整个布局,确保捕获头部信息:
// 替换原PageInfo.Builder的宽高为layoutDownload的宽高 val pageInfo = PdfDocument.PageInfo.Builder( binding.layoutDownload.width, binding.layoutDownload.height, 1 ).create() // 绘制整个主布局 binding.layoutDownload.draw(canvas)
步骤2:强制绘制RecyclerView所有条目
手动遍历RecyclerView的所有条目,逐个测量、布局并绘制到Canvas:
private fun drawRecyclerView(recyclerView: RecyclerView, canvas: Canvas) { val adapter = recyclerView.adapter ?: return var topOffset = 0 for (i in 0 until adapter.itemCount) { val viewHolder = adapter.createViewHolder(recyclerView, adapter.getItemViewType(i)) adapter.bindViewHolder(viewHolder, i) // 测量条目尺寸 viewHolder.itemView.measure( View.MeasureSpec.makeMeasureSpec(recyclerView.width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) // 布局条目 viewHolder.itemView.layout(0, 0, viewHolder.itemView.measuredWidth, viewHolder.itemView.measuredHeight) // 绘制条目到Canvas canvas.save() canvas.translate(0f, topOffset.toFloat()) viewHolder.itemView.draw(canvas) canvas.restore() // 更新下一个条目的偏移量 topOffset += viewHolder.itemView.measuredHeight } }
步骤3:整合完整PDF生成逻辑
更新后的createPdf函数会处理分页(内容过长时自动分多页),先绘制头部布局,再绘制所有RecyclerView条目:
private fun createPdf() { val pdfDocument = PdfDocument() val mainLayout = binding.layoutDownload val recyclerView = binding.downloadRecyler val adapter = recyclerView.adapter ?: return // 计算头部区域高度(Logo、用户信息等) val headerHeight = mainLayout.height - recyclerView.height // 计算RecyclerView所有条目的总高度 var totalRecyclerHeight = 0 for (i in 0 until adapter.itemCount) { val viewHolder = adapter.createViewHolder(recyclerView, adapter.getItemViewType(i)) adapter.bindViewHolder(viewHolder, i) viewHolder.itemView.measure( View.MeasureSpec.makeMeasureSpec(recyclerView.width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) totalRecyclerHeight += viewHolder.itemView.measuredHeight } val totalContentHeight = headerHeight + totalRecyclerHeight val pageWidth = mainLayout.width val pageHeight = mainLayout.height var currentY = 0 while (currentY < totalContentHeight) { val pageInfo = PdfDocument.PageInfo.Builder(pageWidth, pageHeight, pdfDocument.pages.size + 1).create() val page = pdfDocument.startPage(pageInfo) val canvas = page.canvas // 第一页绘制头部布局 if (currentY == 0) { canvas.save() canvas.clipRect(0, 0, pageWidth, headerHeight) mainLayout.draw(canvas) canvas.restore() currentY += headerHeight } // 绘制RecyclerView条目,处理分页偏移 canvas.save() canvas.translate(0f, -(currentY - headerHeight).toFloat()) drawRecyclerView(recyclerView, canvas) canvas.restore() currentY += pageHeight pdfDocument.finishPage(page) } // 保存PDF的逻辑保持不变 val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) val pdfFileName = "${userName}_PDF_$timeStamp.pdf" val directory = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Attendance app" ) if (!directory.exists()) { directory.mkdirs() } val pdfFile = File(directory, pdfFileName) try { pdfFile.createNewFile() val outputStream = FileOutputStream(pdfFile) pdfDocument.writeTo(outputStream) pdfDocument.close() outputStream.close() sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(pdfFile))) Toast.makeText(this, getString(R.string.pdf_downloaded_successfully), Toast.LENGTH_SHORT).show() showDownloadNotification(pdfFile) } catch (e: IOException) { e.printStackTrace() Toast.makeText(this, "Error creating PDF", Toast.LENGTH_SHORT).show() } }
注意事项
- 确保RecyclerView的Adapter已加载全部数据,避免绘制空条目。
- 复杂条目布局需检查测量逻辑,保证渲染尺寸正确。
- 若内容远超单页高度,分页逻辑会自动拆分内容到多页。
内容的提问来源于stack exchange,提问作者shivamj
相关产品推荐
相关产品推荐

