如何获取View的Bitmap
来源 https://www.jianshu.com/p/d22aa98f6e38
我们这里份两种情况进行讨论。
第一种情况,直接从布局文件生成Bitmap
举个例子。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvNumber"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
在这个例子中,布局文件中有一个TextView,我们每次在生成Bitmap之前,改变一下TextView的text。然后把生成的Bitmap设置给一个ImageView做背景。
//布局文件对应的view
private View view;
private TextView tvNumber;
private int number = 0;
//用来显示生成的bitmap
private ImageView ivTop;
private Button btnGetBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_get_drawing_caching);
//首先加载布局文件
view = LayoutInflater.from(this).inflate(R.layout.layout_drawing_cache, null);
tvNumber = view.findViewById(R.id.tvNumber);
ivTop = findViewById(R.id.ivTop);
btnGetBitmap = findViewById(R.id.btnGetBitmap);
//点击事件
btnGetBitmap.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
number++;
//每次生成bitmap之前改变一下tvNumber的text
tvNumber.setText(String.valueOf(number));
ivTop.setBackgroundDrawable(new BitmapDrawable(copyByCanvas(view)));
}
});
}
//...
第一种方法
/**
* 通过canvas复制view的bitmap
*
* @param view
* @return
*/
private Bitmap copyByCanvas(View view) {
view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
Bitmap bp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bp);
view.draw(canvas);
canvas.save();
return bp;
}
第二种方法
/**
* 通过drawingCache获取bitmap
*
* @param view
* @return
*/
private Bitmap convertViewToBitmap(View view) {
view.setDrawingCacheEnabled(true);
view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
//注释1处
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
//如果不调用这个方法,每次生成的bitmap相同
view.setDrawingCacheEnabled(false);
return bitmap;
}
在上面方法的注释1处要注意一下,这里我们没有直接返回view.getDrawingCache()
方法返回的bitmap,也就是我们 没有这样写。
/**
* 通过drawingCache获取bitmap
*
* @param view
* @return
*/
private Bitmap convertViewToBitmap(View view) {
view.setDrawingCacheEnabled(true);
view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
Bitmap mBitmap = view.getDrawingCache();
//如果不调用这个方法,每次生成的bitmap相同
view.setDrawingCacheEnabled(false);
return bitmap;
}
因为发现这样写的话,每次获取的bitmap都是不起作用
Bitmap bitmap = convertViewToBitmap2(tvNumber);
ivTop.setBackgroundDrawable(new BitmapDrawable(bitmap));
使用获取的bitmap构建一个BitmapDrawable对象作为ImageView的背景不起作用 。
有的机型会给出一个警告
BitmapDrawable: Canvas: trying to use a recycled bitmap
而有的机型会直接抛出一个运行时异常
java.lang.RuntimeException:
Canvas: trying to use a recycled bitmap android.graphics.Bitmap@31c9a68
at android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:62)
at android.view.DisplayListCanvas.throwIfCannotDraw(DisplayListCanvas.java:226)
at android.view.RecordingCanvas.drawBitmap(RecordingCanvas.java:98)
at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:545)
at android.view.View.getDrawableRenderNode(View.java:20463)
at android.view.View.drawBackground(View.java:20399)
at android.view.View.draw(View.java:20198)
//...
接下来我们先分析一下原因。
首先看下
Bitmap mBitmap = view.getDrawingCache();
我们的mBitmap引用指向了view.getDrawingCache()
方法返回的对象。
View 的getDrawingCache方法
@Deprecated
public Bitmap getDrawingCache() {
//调用重载方法
return getDrawingCache(false);
}
@Deprecated
public Bitmap getDrawingCache(boolean autoScale) {
if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) {
return null;
}
if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) {
//注释1处
buildDrawingCache(autoScale);
}
return autoScale ? mDrawingCache : mUnscaledDrawingCache;
}
首先在注释1处,会根据传入的autoScale变量生成bitmap对象。如果autoScale为false,则将生成的bitmap对象赋值给mUnscaledDrawingCache。
View的buildDrawingCache方法精简版
@Deprecated
public void buildDrawingCache(boolean autoScale) {
//...
buildDrawingCacheImpl(autoScale);
}
View的buildDrawingCacheImpl方法精简版
private void buildDrawingCacheImpl(boolean autoScale) {
//...
try {
bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
width, height, quality);
bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
//根据传入的autoScale决定将生成的bitmap对象赋值给mDrawingCache还是mUnscaledDrawingCache
if (autoScale) {
mDrawingCache = bitmap;
} else {
mUnscaledDrawingCache = bitmap;
}
if (opaque && use32BitCache) bitmap.setHasAlpha(false);
}
//...
}
到现在我们应该知道了,在这个例子中,最终mBitmap和mUnscaledDrawingCache指向了同一个对象。
然后在生成了bitmap对象以后,我们调用了
view.setDrawingCacheEnabled(false);
View的setDrawingCacheEnabled方法
@Deprecated
public void setDrawingCacheEnabled(boolean enabled) {
mCachingFailed = false;
//注释1处
setFlags(enabled ? DRAWING_CACHE_ENABLED : 0, DRAWING_CACHE_ENABLED);
}
在上面的注释1处,我们传入的参数enabled
是false,所以我们调用setFlags方法最终传入的参数是 setFlags(0, DRAWING_CACHE_ENABLED);
在这种情况下,setFlags方法内部会调用一个分支判断
void setFlags(int flags, int mask) {
//...
if ((changed & DRAWING_CACHE_ENABLED) != 0) {
//注释1处
destroyDrawingCache();
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
invalidateParentCaches();
}
//...
}
在注释1处会调用destroyDrawingCache方法
@Deprecated
public void destroyDrawingCache() {
if (mDrawingCache != null) {
mDrawingCache.recycle();
mDrawingCache = null;
}
//mUnscaledDrawingCache不为null
if (mUnscaledDrawingCache != null) {
//注释1处
mUnscaledDrawingCache.recycle();
mUnscaledDrawingCache = null;
}
}
在注释1处,将我们刚生成的mUnscaledDrawingCache所指向的bitmap对象给回收了。在这里我们可以认为将bitmap对象回收就获取不到bitmap对象上的像素信息了,并且不会绘制任何信息。
然后我们使用返回的bitmap对象构建了一个BitmapDrawable对象,并将BitmapDrawable对象设置为ImageView的背景。
ivTop.setBackgroundDrawable(new BitmapDrawable(bitmap));
@Deprecated
public void setBackgroundDrawable(Drawable background) {
//...
requestLayout();
mBackgroundSizeChanged = true;
//注释1处
invalidate(true);
}
在注释1处调用了invalidate方法,这个方法最终会导致view 重新绘制。
View的draw方法
public void draw(Canvas canvas) {
//...
drawBackground(canvas);
}
View的drawBackground方法
private void drawBackground(Canvas canvas) {
//...
final Drawable background = mBackground;
//注释1处,我们传入的是BitmapDrawable
background.draw(canvas);
}
在上面的注释1处,会调用BitmapDrawable的draw方法
@Override
public void draw(Canvas canvas) {
//...
//这里就是报异常的代码
canvas.drawBitmap(bitmap, null, mDstRect, paint);
}
到这里,我们知道了不能使用一个被回收的bitmap的原因所在。接下来轻松一点,看看获取View的bitmap的第二种情况。
第二种情况,在获取Bitmap之前,View显示在屏幕上了已经
这种情况下就比较简单了。不需要view的measure和layout过程了。
方法一
/**
* 通过drawingCache获取bitmap
*
* @param view
* @return
*/
private Bitmap convertViewToBitmap2(View view) {
view.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
//如果不调用这个方法,每次生成的bitmap相同
view.setDrawingCacheEnabled(false);
return bitmap;
}
方法二
/**
* 通过canvas复制view的bitmap
*
* @param view
* @return
*/
private Bitmap copyByCanvas2(View view) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Log.d(TAG, "copyByCanvas: width=" + width + ",height=" + height);
Bitmap bp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bp);
view.draw(canvas);
canvas.save();
return bp;
}
参考链接:
[1]:两种获取view的bitmap的方法
[2]:drawingcache解析 通过view的绘制缓存得到bitmap,从而实现view内容截图
================ End