如何获取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