如何获取View的Bitmap

如何获取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
原文地址:https://www.cnblogs.com/lsgxeva/p/13394403.html