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
相关产品推荐
相关产品推荐

