RecyclerView+BRVAH框架搭配使用时,如何进行长截
项目中经常会使用到截图分享功能,很多情况下,我们不仅仅需要截取当前屏幕,而是要截取整个可滚动的页面。像是普通的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
}