美文网首页安卓开发博客
RecyclerView+BRVAH框架搭配使用时,如何进行长截

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

作者: 吉原拉面 | 来源:发表于2019-02-14 10:02 被阅读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
    }
    

    相关文章

      网友评论

        本文标题:RecyclerView+BRVAH框架搭配使用时,如何进行长截

        本文链接:https://www.haomeiwen.com/subject/pvbdeqtx.html