安卓开发博客

RecyclerView+BRVAH框架搭配使用时,如何进行长截

2019-02-14  本文已影响147人  吉原拉面

  项目中经常会使用到截图分享功能,很多情况下,我们不仅仅需要截取当前屏幕,而是要截取整个可滚动的页面。像是普通的View、ScroolView之类的,即使不在屏幕内,内容也已经全部渲染好了,所以是可以直接截屏的;但是像RecyclerView、ListView之类的,因为涉及到屏幕外item的复用,截取的时候就会麻烦一些了。
  普通RecyclerView、ListView的截图,大家网上随便搜搜就能有一大堆,我就不赘述了。今天要讲的是,RecyclerView+BRVAH框架搭配使用时,网上说的方法就需要好好修改一下了。

  先贴下网上流行的方法:

/**
   * https://gist.github.com/PrashamTrivedi/809d2541776c8c141d9a
   */
  public static Bitmap shotRecyclerView(RecyclerView view) {
    RecyclerView.Adapter adapter = view.getAdapter();
    Bitmap bigBitmap = null;
    if (adapter != null) {
      int size = adapter.getItemCount();
      int height = 0;
      Paint paint = new Paint();
      int iHeight = 0;
      final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 
      // Use 1/8th of the available memory for this memory cache.
      final int cacheSize = maxMemory / 8;
      LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
      for (int i = 0; i < size; i++) {
        RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));
        adapter.onBindViewHolder(holder, i);
        holder.itemView.measure(
            View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(),
            holder.itemView.getMeasuredHeight());
        holder.itemView.setDrawingCacheEnabled(true);
        holder.itemView.buildDrawingCache();
        Bitmap drawingCache = holder.itemView.getDrawingCache();
        if (drawingCache != null) {
 
          bitmaCache.put(String.valueOf(i), drawingCache);
        }
        height += holder.itemView.getMeasuredHeight();
      }
 
      bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
      Canvas bigCanvas = new Canvas(bigBitmap);
      Drawable lBackground = view.getBackground();
      if (lBackground instanceof ColorDrawable) {
        ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
        int lColor = lColorDrawable.getColor();
        bigCanvas.drawColor(lColor);
      }
 
      for (int i = 0; i < size; i++) {
        Bitmap bitmap = bitmaCache.get(String.valueOf(i));
        bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
        iHeight += bitmap.getHeight();
        bitmap.recycle();
      }
    }
    return bigBitmap;
  }

  思路其实很简单,就是使用adapter.createViewHolder(view, adapter.getItemViewType(i))方法来找到每一个item的ViewHolder(这步可以为item塞数据),然后拿到holder.itemView进行测量、布局等工作,让这个item渲染出来,这样就可以使用getDrawingCache()来获取到view的截图了。
  但是,当你搭配了BRVAH框架时,就会出现一些奇奇怪怪的问题了。

坑1:在有HeaderView的时候,会报错崩溃

  如果你添加了HeaderView,那么恭喜你,你会得到如下报错:

ViewHolder views must not be attached when created. Ensure that you are not passing ‘true’ to the attachToRoot parameter of LayoutInflate.

  大概意思就是你的HeaderView在Inflate的时候,你将attachToRoot属性设置为了true,这种情况下你是不能强行地去adapter.createViewHolder()的。但是,这个HeaderView,不是我Inflate的啊,是BRVAH框架帮我加的啊,哭。
  看源码,HeaderView在inflate的时候,attachToRoot属性已经是false了,所以到底哪里出了问题我还没看出来。但是不管怎样,你不可能去改源码的,所以试着从其他角度去解决这个问题吧。

@Override
    public K onCreateViewHolder(ViewGroup parent, int viewType) {
        K baseViewHolder = null;
        this.mContext = parent.getContext();
        this.mLayoutInflater = LayoutInflater.from(mContext);
        switch (viewType) {
            case LOADING_VIEW:
                baseViewHolder = getLoadingView(parent);
                break;
            case HEADER_VIEW:
                baseViewHolder = createBaseViewHolder(mHeaderLayout);
                break;
            case EMPTY_VIEW:
                baseViewHolder = createBaseViewHolder(mEmptyLayout);
                break;
            case FOOTER_VIEW:
                baseViewHolder = createBaseViewHolder(mFooterLayout);
                break;
            default:
                baseViewHolder = onCreateDefViewHolder(parent, viewType);
                bindViewClickListener(baseViewHolder);
        }
        baseViewHolder.setAdapter(this);
        return baseViewHolder;
    }

protected K createBaseViewHolder(ViewGroup parent, int layoutResId) {
        return createBaseViewHolder(getItemView(layoutResId, parent));
    }

protected View getItemView(@LayoutRes int layoutResId, ViewGroup parent) {
        return mLayoutInflater.inflate(layoutResId, parent, false);
    }

  我们知道,adapter有个getHeaderLayout()方法,可以获取HeaderView,而且,这个HeaderView在BRVAH框架里面其实就是个成员变量mHeaderLayout

public LinearLayout getHeaderLayout() {
        return mHeaderLayout;
    }

  既然是成员变量,那我就不用担心什么复用不复用的了,这个mHeaderLayout肯定是不管你HeaderView在不在屏幕里,它都是有值的,所以,如果是header,那么我们直接根据getHeaderLayout()来获取view,普通item才去用createViewHolder()的方式获取。我们可以这么改:

for (i in 0 until size) {
        var itemView: View?
        itemView = when (getItemViewType(i)) {
            BaseQuickAdapter.HEADER_VIEW -> headerLayout
            else -> {
                val holder = createViewHolder(recyclerView, getItemViewType(i))
                onBindViewHolder(holder, i)
                holder.itemView
            }
        }
        itemView?.run {
            measure(
                View.MeasureSpec.makeMeasureSpec(recyclerView.width, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
            layout(0, 0, measuredWidth, measuredHeight)
            isDrawingCacheEnabled = true
            buildDrawingCache()
            var bitmap: Bitmap? = drawingCache
            ······
        }
    }

坑2:如果HeaderView被部分移出屏幕,layout(0, 0, measuredWidth, measuredHeight)之后,位置就不对了。

  如果HeaderView被部分移出屏幕,那么你在layout的时候,top肯定不可以设置为0的,你可以在滚动的时候打印log看下,HeaderView的top值在部分移出屏幕之后就会变成一个负值(这里要注意,每一个item的top依然是0,所以item并不会错位)。
  解决方法很简单,layout的时候不要将top写死为0,而是写成view.getTop()

headerView?.run {
            ······
            layout(left, top, measuredWidth, measuredHeight)
            ······
        }
  你以为这样就解决了?图样图森破,运行代码会出现这样的情况:

  HeaderView和第一个item之间会留有一段空隙,仔细观察会发现,这段空隙正好是HeaderView的偏移量。也就是说,layout的时候将HeaderView向上偏移了,虽然measuredHeight还是对的,但是我们拿到的drawingCache这个Bitmap的高度是不对的,要比measuredHeight高出了一个偏移量。所以,我们需要将Bitmap进行裁剪,将多余的部分裁掉就可以:

var bitmap: Bitmap? = drawingCache
if (getItemViewType(i) == BaseQuickAdapter.HEADER_VIEW) {
// header需要特殊处理,因为在滑动时header的top会变成负数,导致截图其和第一条item之间会有空隙,所以要把空隙裁掉
      fun cropBitmap(bmp: Bitmap?): Bitmap? {
              return bmp?.let {
                  Bitmap.createBitmap(bmp, 0, 0, width, it.height + headerLayout.top, null, false)
              }
      }
      bitmap = cropBitmap(bitmap)
}

坑3:单独截取HeaderView的时候,如果HeaderView移出屏幕的部分过多,会报错

Canvas: trying to use a recycled bitmap android.graphics.Bitmap@XXX

  如果依然使用drawingCache截图,这个报错我暂时没有找到合适的解决方法。所以决定换个思路,直接将View绘制到Canvas上面:

fun BaseQuickAdapter<*, BaseViewHolder>.shotRecyclerViewHeader(): Bitmap {
  val bigBitmap = Bitmap.createBitmap(
  headerLayout.measuredWidth, headerLayout.measuredHeight,  Bitmap.Config.ARGB_8888)
  val bigCanvas = Canvas(bigBitmap)
  ······
  headerLayout.draw(bigCanvas)
  return bigBitmap
}

结语

  完整截图代码如下(添加了截图背景色设置,不需要的可以删除drawColor部分代码):

fun BaseQuickAdapter<*, BaseViewHolder>.shotRecyclerView(
    recyclerView: RecyclerView, bgColor: Int? = null): Bitmap {
    val size = itemCount
    var height = 0
    val maxMemory: Int = ((Runtime.getRuntime().maxMemory() / 1024).toInt())
    // Use 1/8th of the available memory for this memory cache.
    val cacheSize = maxMemory / 8
    val bitmapCache: LruCache<String, Bitmap> = LruCache(cacheSize)
    for (i in 0 until size) {
        var itemView: View?
        itemView = when (getItemViewType(i)) {
            BaseQuickAdapter.HEADER_VIEW -> headerLayout
            else -> {
                val holder = createViewHolder(recyclerView, getItemViewType(i))
                onBindViewHolder(holder, i)
                holder.itemView
            }
        }
        itemView?.run {
            measure(
                View.MeasureSpec.makeMeasureSpec(recyclerView.width, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
            layout(left, top, measuredWidth, measuredHeight)
            isDrawingCacheEnabled = true
            buildDrawingCache()
            var bitmap: Bitmap? = drawingCache
            if (getItemViewType(i) == BaseQuickAdapter.HEADER_VIEW) {
                // header需要特殊处理,因为在滑动时header的top会变成负数,导致截图其和第一条item之间会有空隙,所以要把空隙裁掉
                fun cropBitmap(bmp: Bitmap?): Bitmap? {
                    return bmp?.let {
                        Bitmap.createBitmap(
                            bmp, 0, 0, width, it.height + headerLayout.top, null, false)
                    }
                }
                bitmap = cropBitmap(bitmap)
            }
            bitmapCache.put("$i", bitmap)
            height += measuredHeight
//            isDrawingCacheEnabled = false
//            destroyDrawingCache()
        }
    }
    val bigBitmap = Bitmap.createBitmap(recyclerView.measuredWidth, height, Bitmap.Config.ARGB_8888)
    val bigCanvas = Canvas(bigBitmap)
    // draw background if necessary
    val lBackground: Drawable? = recyclerView.background
    if (bgColor == null && lBackground == null) {
        bigCanvas.drawColor(Color.WHITE)
    } else if (lBackground != null && lBackground is ColorDrawable) {
        bigCanvas.drawColor(lBackground.color)
    } else {
        bgColor?.let { bigCanvas.drawColor(it) }
    }
    // drawBitmap
    var iHeight = 0f
    for (i in 0 until size) {
        val bitmap: Bitmap? = bitmapCache.get("$i")
        bitmap?.run {
            bigCanvas.drawBitmap(bitmap, 0f, iHeight, Paint())
            iHeight += this.height
        }
        // 如果recycle掉了,第二次点击截图的时候会报错Canvas: trying to use a recycled bitmap android.graphics.Bitmap@xxx
//        bitmap.recycle()
    }
    return bigBitmap
}

fun BaseQuickAdapter<*, BaseViewHolder>.shotRecyclerView(recyclerView: RecyclerView): Bitmap {
    return shotRecyclerView(recyclerView, null)
}

fun BaseQuickAdapter<*, BaseViewHolder>.shotRecyclerViewHeader(): Bitmap {
    return shotRecyclerViewHeader(null)
}

fun BaseQuickAdapter<*, BaseViewHolder>.shotRecyclerViewHeader(bgColor: Int? = null): Bitmap {
    val bigBitmap = Bitmap.createBitmap(
        headerLayout.measuredWidth, headerLayout.measuredHeight, Bitmap.Config.ARGB_8888)
    headerLayout?.run {
        val bigCanvas = Canvas(bigBitmap)
        // draw background if necessary
        val lBackground: Drawable? = background
        if (bgColor == null && lBackground == null) {
            bigCanvas.drawColor(Color.WHITE)
        } else if (lBackground != null && lBackground is ColorDrawable) {
            bigCanvas.drawColor(lBackground.color)
        } else {
            bgColor?.let { bigCanvas.drawColor(it) }
        }
        // drawBitmap
        draw(bigCanvas)
    }
    return bigBitmap
}
上一篇下一篇

猜你喜欢

热点阅读