• 如何获取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
  • 相关阅读:
    全站导航
    常用模块
    模块的引用的路径的查找
    类的魔术方法
    包装和授权
    类内置的attr属性
    反射
    三大特性之多态
    三大特性之封装
    python应用:爬虫框架Scrapy系统学习第二篇——windows下安装scrapy
  • 原文地址:https://www.cnblogs.com/lsgxeva/p/13394403.html
Copyright © 2020-2023  润新知