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

Android中基于视图生成含网络图片PDF的问题排查

解决Android PdfDocument中RecyclerView+Glide网络图片不显示的问题

我之前也踩过一模一样的坑,这事儿的核心矛盾其实是Glide的异步加载逻辑和PDF生成的同步渲染流程完全不兼容——你这边刚把View绘制到PDF的Canvas上,Glide那边才刚开始异步拉取图片,等图片加载完的时候,PDF早就生成完毕了,自然看不到任何图片。而且你之前的预加载操作只是把图片缓存到了Glide的内存池里,但并没有和RecyclerView里的ImageView绑定,所以onBind里的Glide请求根本没拿到现成的资源,回调自然也不会触发。

下面给你两个可行的解决方案,从应急到规范都有:

方案一:同步加载图片(应急用,不推荐主线程执行)

既然异步赶不上PDF生成的节奏,那咱们就改成同步加载。用Glide的submit()方法直接获取Bitmap,再手动设置到ImageView里。注意必须在子线程执行,不然会阻塞主线程导致ANR。

修改你的ViewHolder的setData方法:

public void setData(String url) {
    try {
        // 同步获取Bitmap,务必在子线程调用
        Bitmap bitmap = Glide.with(imageView.getContext())
                .asBitmap()
                .load(url)
                .submit()
                .get();
        imageView.setImageBitmap(bitmap);
    } catch (InterruptedException | ExecutionException e) {
        Timber.e(e, "加载图片失败");
        // 加载失败时设置占位图
        imageView.setImageResource(R.drawable.placeholder_image);
    }
}

然后在生成PDF的代码里,要确保RecyclerView的所有子View都完成图片加载后再执行绘制:

// 替换原来的recyclerViewImages.setAdapter(...)部分
PDFImageAdapter adapter = new PDFImageAdapter(postDB.getImagesUrls());
recyclerViewImages.setAdapter(adapter);
// 强制RecyclerView完成布局,等待所有图片加载
recyclerViewImages.post(() -> {
    // 执行后续的测量、布局、绘制操作
    int measureWidth = View.MeasureSpec.makeMeasureSpec(page.getCanvas().getWidth(), View.MeasureSpec.EXACTLY);
    // 动态计算RecyclerView高度,不要用固定页面高度
    int measuredHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    content.measure(measureWidth, measuredHeight);
    content.layout(0, 0, page.getCanvas().getWidth(), content.getMeasuredHeight());
    content.draw(canvas);
    // 完成PDF后续流程
    document.finishPage(page);
    document.writeTo(fOut);
    document.close();
    fOut.close();
});

方案二:预加载所有图片并等待完成(推荐)

先把所有需要的图片预加载到Glide的缓存里,等所有图片都加载完成后,再渲染整个View到PDF。这里用CountDownLatch来跟踪加载状态:

修改你的createPDF方法:

public File createPDF(PostDB postDB) {
    int addition = 0;
    String postDBContent = postDB.getContent();
    if (postDBContent != null) {
        int estimatedLineCount = postDBContent.length() / 43;
        String[] lines = postDBContent.split("\r\n|\r|\n");
        estimatedLineCount += lines.length;
        addition = (estimatedLineCount - 43) * 60;
    }
    List<String> imagesUrls = postDB.getImagesUrls();
    final CountDownLatch latch = new CountDownLatch(imagesUrls.size());

    // 预加载所有图片,每完成一个就countDown
    for (String url : imagesUrls) {
        Glide.with(context)
                .load(url)
                .listener(new RequestListener<Drawable>() {
                    @Override
                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                        Timber.d("预加载失败: %s", url);
                        latch.countDown();
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                        Timber.d("预加载完成: %s", url);
                        latch.countDown();
                        return false;
                    }
                })
                .preload();
    }

    try {
        // 等待所有图片预加载完成,设置超时避免无限等待
        latch.await(10, TimeUnit.SECONDS);

        FileOutputStream fOut = context.openFileOutput(LOCAL_PATH, Context.MODE_PRIVATE);
        PdfDocument document = new PdfDocument();
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View content = inflater.inflate(R.layout.pdf_layout, null);
        ButterKnife.bind(this, content);

        // 填充静态内容
        publisherTextView.setText(postDB.getAuthor());
        Date date = new Date(postDB.getDate());
        DateFormat dateFormat = DateFormat.getInstance();
        String format = dateFormat.format(date);
        dateTextView.setText(format);
        titleTextView.setText(postDB.getTitle());
        contentPreviewTextView.setText(postDBContent);
        String text = postDB.getCity() + ", " + postDB.getCountry();
        location.setText(text);

        // 设置RecyclerView
        recyclerViewImages.setLayoutManager(new GridLayoutManager(context, 2));
        recyclerViewImages.setAdapter(new PDFImageAdapter(postDB.getImagesUrls()));

        // 先测量View实际高度
        int measureWidth = View.MeasureSpec.makeMeasureSpec(1800, View.MeasureSpec.EXACTLY);
        int measuredHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        content.measure(measureWidth, measuredHeight);
        int actualHeight = content.getMeasuredHeight();

        // 创建对应高度的PDF页面
        PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(1800, actualHeight, 1).create();
        PdfDocument.Page page = document.startPage(pageInfo);
        Canvas canvas = page.getCanvas();

        // 布局并绘制View
        content.layout(0, 0, page.getCanvas().getWidth(), actualHeight);
        content.draw(canvas);

        document.finishPage(page);
        document.writeTo(fOut);
        document.close();
        fOut.close();
        return new File(context.getFilesDir().getPath() + "/" + LOCAL_PATH);
    } catch (IOException | InterruptedException e) {
        Log.i("error", e.getLocalizedMessage());
        return null;
    }
}

同时修改ViewHolder的setData方法,确保能命中缓存:

public void setData(String url) {
    Glide.with(imageView.getContext())
            .load(url)
            .diskCacheStrategy(DiskCacheStrategy.ALL) // 强制使用缓存
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                    Timber.d("onLoadFailed");
                    imageView.setImageResource(R.drawable.placeholder_image);
                    return true;
                }

                @Override
                public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                    Timber.d("onResourceReady");
                    return false;
                }
            })
            .into(imageView);
}

额外注意点

  • RecyclerView高度计算:不要直接用固定页面高度,先测量View实际高度再创建PDF页面,避免内容截断或留白过多。
  • Glide缓存策略:设置diskCacheStrategy(DiskCacheStrategy.ALL)可以提高预加载图片的命中率。
  • 线程安全:所有图片加载操作尽量放在子线程,避免阻塞主线程。

内容的提问来源于stack exchange,提问作者Vasili Fedotov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:09:16