You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何下载含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>

解决方案

核心问题分析

  1. RecyclerView.draw(canvas) 仅绘制当前屏幕可见视图,因RecyclerView的复用机制,不可见条目未实例化或已被回收。
  2. 原代码仅以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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 16:25:54