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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM