Android截屏总结

Android截屏的原理:获取具体需要截屏的区域的Bitmap,然后绘制在画布上,保存为图片后进行分享或者其它用途。
在截屏功能中,有时需要截取全屏的内容,有时需要截取超过一屏的内容(比如:Listview,Scrollview,RecyclerView)。下面介绍各种场景获取Bitmap的方法
普通截屏的实现
获取当前Window的DrawingCache的方式,即decorView的DrawingCache

    /**
     * shot the current screen ,with the status but the status is trans
     * * @param ctx current activity
     */
    public static Bitmap shotActivity(Activity ctx) {
        View view = ctx.getWindow().getDecorView();
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        Bitmap bp = Bitmap.createBitmap(view.getDrawingCache(), 0, 0,
                view.getMeasuredWidth(), view.getMeasuredHeight());
        view.setDrawingCacheEnabled(false);
        view.destroyDrawingCache();
        return bp;
    }
获取当前View的DrawingCache

    public static Bitmap getViewBp(View v) {
        if (null == v) {
            return null;
        }
        v.setDrawingCacheEnabled(true);
        v.buildDrawingCache();
        if (Build.VERSION.SDK_INT >= 11) {
            v.measure(MeasureSpec.makeMeasureSpec(v.getWidth(),
                    MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(v.getHeight(), MeasureSpec.EXACTLY));
            v.layout((int) v.getX(), (int) v.getY(), (int) v.getX() + v.getMeasuredWidth(), (int) v.getY() + v.getMeasuredHeight());
        } else {
            v.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
        }
        Bitmap b = Bitmap.createBitmap(v.getDrawingCache(), 0, 0, v.getMeasuredWidth(),
                v.getMeasuredHeight());
        v.setDrawingCacheEnabled(false);
        v.destroyDrawingCache();
        return b;
    }

开源方案
在滚动视图中,如果当前View并没有在视图中全部绘制出来,我们可以利用View的ScrollTo()和ScrollBy()方法来移动画布,同时获取当前View的可视部分的DrawingCache,最后进行拼接得到其Bitmap,参考:PGSSoft/scrollscreenshot@[Github]
Scrollview截屏
三个截屏中,ScrollView最简单,因为ScrollView只有一个childView,虽然没有全部显示在界面上,但是已经全部渲染绘制,因此可以直接 调用scrollView.draw(canvas)
来完成截图

    public static Bitmap shotScrollView(ScrollView scrollView) {
        int h = 0;
        Bitmap bitmap = null;
        for (int i = 0; i < scrollView.getChildCount(); i++) {
            h += scrollView.getChildAt(i).getHeight();
            scrollView.getChildAt(i).setBackgroundColor(Color.parseColor("#ffffff"));
        }
        bitmap = Bitmap.createBitmap(scrollView.getWidth(), h, Bitmap.Config.RGB_565);
        final Canvas canvas = new Canvas(bitmap);
        scrollView.draw(canvas);
        return bitmap;
    }

listview截屏
而ListView就是会回收与重用Item,并且只会绘制在屏幕上显示的ItemView,根据stackoverflow上大神的建议,采用一个List来存储Item的视图,这种方案依然不够好,当Item足够多的时候,可能会发生oom。

public static Bitmap shotListView(ListView listview) {
        ListAdapter adapter = listview.getAdapter();
        int itemscount = adapter.getCount();
        int allitemsheight = 0;
        List<Bitmap> bmps = new ArrayList<Bitmap>();
        for (int i = 0; i < itemscount; i++) {
            View childView = adapter.getView(i, null, listview);
            childView.measure(View.MeasureSpec.makeMeasureSpec(listview.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
            childView.setDrawingCacheEnabled(true);
            childView.buildDrawingCache();
            bmps.add(childView.getDrawingCache());
            allitemsheight += childView.getMeasuredHeight();
        }
        Bitmap bigbitmap = Bitmap.createBitmap(listview.getMeasuredWidth(), allitemsheight, Bitmap.Config.ARGB_8888);
        Canvas bigcanvas = new Canvas(bigbitmap);
        Paint paint = new Paint();
        int iHeight = 0;
        for (int i = 0; i < bmps.size(); i++) {
            Bitmap bmp = bmps.get(i);
            bigcanvas.drawBitmap(bmp, 0, iHeight, paint);
            iHeight += bmp.getHeight();
            bmp.recycle();
            bmp = null;
        }
        return bigbitmap;
    }

RecyclerView截屏
我们都知道,在新的Android版本中,已经可以用RecyclerView来代替使用ListView的场景,相比较ListView,RecyclerView对Item View的缓存支持的更好。可以采用和ListView相同的方案,这里也是在stackoverflow上看到的方案。

public static Bitmap shotRecyclerView(RecyclerView view) {
        RecyclerView.Adapter adapter = view.getAdapter();
        Bitmap bigBitmap = null;
        if (adapter != null) {
            int size = adapter.getItemCount();
            int height = 0;
            Paint paint = new Paint();
            int iHeight = 0;
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
            // Use 1/8th of the available memory for this memory cache. 
            final int cacheSize = maxMemory / 8;
            LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
            for (int i = 0; i < size; i++) {
                RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));
                adapter.onBindViewHolder(holder, i);
                holder.itemView.measure(View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
                holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(), holder.itemView.getMeasuredHeight());
                holder.itemView.setDrawingCacheEnabled(true);
                holder.itemView.buildDrawingCache();
                Bitmap drawingCache = holder.itemView.getDrawingCache();
                if (drawingCache != null) {
                    bitmaCache.put(String.valueOf(i), drawingCache);
                }
                height += holder.itemView.getMeasuredHeight();
            }
            bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
            Canvas bigCanvas = new Canvas(bigBitmap);
            Drawable lBackground = view.getBackground();
            if (lBackground instanceof ColorDrawable) {
                ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
                int lColor = lColorDrawable.getColor();
                bigCanvas.drawColor(lColor);
            }
            for (int i = 0; i < size; i++) {
                Bitmap bitmap = bitmaCache.get(String.valueOf(i));
                bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
                iHeight += bitmap.getHeight();
                bitmap.recycle();
            }
        }
        return bigBitmap;
    }

相信有不少小伙伴用BRVH第三方库来做recycleview的适配器的。使用这个库的话再用上面的方法会报角标越界的错误,看了BRVH的源码

public void onBindViewHolder(ViewHolder holder, int positions) {
        int viewType = holder.getItemViewType();
        switch (viewType) {
            case 0:
                this.convert((BaseViewHolder) holder, this.mData.get(holder.getLayoutPosition() - this.getHeaderLayoutCount()));
            case 273:
            case 819:
            case 1365:
                break;
            case 546:
                this.addLoadMore(holder);
                break;
            default:
                this.convert((BaseViewHolder) holder, this.mData.get(holder.getLayoutPosition() - this.getHeaderLayoutCount()));
                this.onBindDefViewHolder((BaseViewHolder) holder, this.mData.get(holder.getLayoutPosition() - this.getHeaderLayoutCount()));
        }
    }

在调用adapter.onBindViewHolder
时,因为里面的position
参数未使用,里面用的计算holder.getLayoutPosition() - this.getHeaderLayoutCount()
的值一直是-1导致角标越界报错。
本人理解,RecyclerView的截屏原理是,首先构造每个item的ViewHolder,然后调用具体设置数据到每个item的方法,此时cache中就存有item的内容,此时绘制就能获取到完整的内容。采用v7包中的onBindViewHolder
方法即可,或者是BRVH的convert
方法,可以看到BRVH中没有暴露出这个方法,而且唯一暴露出的onBindViewHolder
还会报角标越界错误,此时我们就需要在BRVH的基础上暴露出convert
即可,代码如下

public class MyAdapter extends BaseQuickAdapter<T> {
        public MyAdapter() {
            super(getItemLayoutResId(), datas);
        }

        /**
         * 用于对外暴露convert方法,构造缓存视图(截屏用) * @param viewHolder * @param t
         */
        public void startConvert(BaseViewHolder viewHolder, T t) {
            convert(viewHolder, t);
        }

        @Override
        protected void convert(BaseViewHolder viewHolder, T t) {
            bindView(viewHolder, t);
        }
    }

然后将上面所述的获取Bitmap方法修改一下

 /**
     * 截取recycler view
     */
    public static Bitmap getRecyclerViewScreenshot(RecyclerView view) {
        BaseListFragment.MyAdapter adapter = (BaseListFragment.MyAdapter) view.getAdapter();
        Bitmap bigBitmap = null;
        if (adapter != null) {
            int size = adapter.getData().size();
            int height = 0;
            Paint paint = new Paint();
            int iHeight = 0;
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
            // Use 1/8th of the available memory for this memory cache.
            final int cacheSize = maxMemory / 8;
            LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
            for (int i = 0; i < size; i++) {
                BaseViewHolder holder = (BaseViewHolder) adapter.createViewHolder(view, adapter.getItemViewType(i));
                //此处需要调用convert方法,否则绘制出来的都是空的
                item adapter.startConvert(holder, adapter.getData().get(i));
                holder.itemView.measure(View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
                holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(), holder.itemView.getMeasuredHeight());
                holder.itemView.setDrawingCacheEnabled(true);
                holder.itemView.buildDrawingCache();
                Bitmap drawingCache = holder.itemView.getDrawingCache();
                if (drawingCache != null) {
                    bitmaCache.put(String.valueOf(i), drawingCache);
                }
                height += holder.itemView.getMeasuredHeight();
            }
            bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
            Canvas bigCanvas = new Canvas(bigBitmap);
            Drawable lBackground = view.getBackground();
            if (lBackground instanceof ColorDrawable) {
                ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
                int lColor = lColorDrawable.getColor();
                bigCanvas.drawColor(lColor);
            }
            for (int i = 0; i < size; i++) {
                Bitmap bitmap = bitmaCache.get(String.valueOf(i));
                bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
                iHeight += bitmap.getHeight();
                bitmap.recycle();
            }
        } return bigBitmap;
    }

合成Bitmap
比如四张合成一张

 /**
     * 将四张图拼成一张 * * @param pic1 图一 * @param pic2 图二 * @param pic3 图三 * @param pic4 图四 * @return only_bitmap * 详情见说明:{@link com.bertadata.qxb.util.ScreenShotUtils}
     */
    public static Bitmap combineBitmapsIntoOnlyOne(Bitmap pic1, Bitmap pic2, Bitmap pic3, Bitmap pic4, Activity context) {
        int w_total = pic2.getWidth();
        int h_total = pic1.getHeight() + pic2.getHeight() + pic3.getHeight() + pic4.getHeight();
        int h_pic1 = pic1.getHeight();
        int h_pic4 = pic4.getHeight();
        int h_pic12 = pic1.getHeight() + pic2.getHeight();
        //此处为防止OOM需要对高度做限制 
        if (h_total > HEIGHTLIMIT) {
            return null;
        }
        Bitmap only_bitmap = Bitmap.createBitmap(w_total, h_total, Bitmap.Config.ARGB_4444);
        Canvas canvas = new Canvas(only_bitmap);
        canvas.drawColor(ContextCompat.getColor(context, R.color.color_content_bg));
        canvas.drawBitmap(pic1, 0, 0, null);
        canvas.drawBitmap(pic2, 0, h_pic1, null);
        canvas.drawBitmap(pic3, 0, h_pic12, null);
        canvas.drawBitmap(pic4, 0, h_total - h_pic4, null);
        return only_bitmap;
    }

图片后期处理

 /** * 将传入的Bitmap合理压缩后输出到系统截屏目录下 
     * 命名格式为:Screenshot+时间戳+启信宝报名.jpg
     * 同时通知系统重新扫描系统文件 
     * @param pic1 图一 标题栏截图 
     * @param pic2 图二 scrollview截图 
     * @param context 用于通知重新扫描文件系统,为提升性能可去掉 
     * 详情见说明:{@link com.bertadata.qxb.util.ScreenShotUtils}
     */

public static void savingBitmapIntoFile(final Bitmap pic1, final Bitmap pic2, final Activity context, final BitmapAndFileCallBack callBack) {
        if (context == null || context.isFinishing()) {
            return;
        }
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                String fileReturnPath = "";
                int w = pic1.getWidth();
                Bitmap bottom = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_picture_combine_bottom);
                Bitmap top_banner = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_picture_combine_top);
                Bitmap bitmap_bottom = anyRatioCompressing(bottom, (float) w / bottom.getWidth(), (float) w / bottom.getWidth());
                Bitmap bitmap_top = anyRatioCompressing(top_banner, (float) w / bottom.getWidth(), (float) w / bottom.getWidth());
                final Bitmap only_bitmap = combineBitmapsIntoOnlyOne(bitmap_top, pic1, pic2, bitmap_bottom, context);
                // 获取当前时间 
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-ms", Locale.getDefault());
                String data = sdf.format(new Date());
                // 获取内存路径 // 设置图片路径+命名规范 // 声明输出文件
                String storagePath = Environment.getExternalStorageDirectory().getAbsolutePath();
                String fileTitle = "Screenshot_" + data + "_com.bertadata.qxb.biz_info.jpg";
                String filePath = storagePath + "/DCIM/";
                final String fileAbsolutePath = filePath + fileTitle;
                File file = new File(fileAbsolutePath);
                /** * 质压与比压结合 
                * 分级压缩 
                * 输出文件 
                */
                if (only_bitmap != null) {
                    try {
                        // 首先,对原图进行一步质量压缩,形成初步文件 
                        FileOutputStream fos = new FileOutputStream(file);
                        only_bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos);
                        // 另建一个文件other_file预备输出 
                        String other_fileTitle = "Screenshot_" + data + "_com.bertadata.qxb.jpg";
                        String other_fileAbsolutePath = filePath + other_fileTitle;
                        File other_file = new File(other_fileAbsolutePath);
                        FileOutputStream other_fos = new FileOutputStream(other_file);
                            // 其次,要判断质压之后的文件大小,按文件大小分级进行处理 
                        long file_size = file.length() / 1024;
                        // size of file(KB)
                        if (file_size < 0 || !(file.exists())) {
                            // 零级: 文件判空 throw new NullPointerException();
                        } else if (file_size > 0 && file_size <= 256) {
                            // 一级: 直接输出 deleteFile(other_file); 
                            // 通知刷新文件系统,显示最新截取的图文件 fileReturnPath = fileAbsolutePath; context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + fileAbsolutePath))); 
                        } else if (file_size > 256 && file_size <= 768) {
                            // 二级: 简单压缩:压缩为原比例的3/4,质压为50% 
                            anyRatioCompressing(only_bitmap, (float) 3 / 4, (float) 3 / 4).compress(Bitmap.CompressFormat.JPEG, 40, other_fos);
                            deleteFile(file);
                            // 通知刷新文件系统,显示最新截取的图文件 
                            fileReturnPath = other_fileAbsolutePath;
                            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
                        } else if (file_size > 768 && file_size <= 1280) {
                            // 三级: 中度压缩:压缩为原比例的1/2,质压为40% anyRatioCompressing(only_bitmap, (float) 1 / 2, (float) 1 / 2).compress(Bitmap.CompressFormat.JPEG, 40, other_fos); 
                            deleteFile(file);
                            // 通知刷新文件系统,显示最新截取的图文件 
                            fileReturnPath = other_fileAbsolutePath;
                            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
                        } else if (file_size > 1280 && file_size <= 2048) {
                            // 四级: 大幅压缩:压缩为原比例的1/3,质压为40% anyRatioCompressing(only_bitmap, (float) 1 / 3, (float) 1 / 3).compress(Bitmap.CompressFormat.JPEG, 40, other_fos);
                            deleteFile(file);
                            // 通知刷新文件系统,显示最新截取的图文件 
                            fileReturnPath = other_fileAbsolutePath;
                            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
                        } else if (file_size > 2048) {
                            // 五级: 中度压缩:压缩为原比例的1/2,质压为40% anyRatioCompressing(only_bitmap, (float) 1 / 2, (float) 1 / 2).compress(Bitmap.CompressFormat.JPEG, 40, other_fos); 
                            deleteFile(file);
                            // 通知刷新文件系统,显示最新截取的图文件 
                            fileReturnPath = other_fileAbsolutePath;
                            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
                        }
                        // 注销fos;
                        fos.flush();
                        other_fos.flush();
                        other_fos.close();
                        fos.close();
                        //callback用于回传保存成功的路径以及
                        Bitmap callBack.onSuccess(only_bitmap, fileReturnPath);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else
                    callBack.onSuccess(null, "");
            }
        });
        thread.start();
    }

/** * 可实现任意宽高比例压缩(宽高压比可不同)的压缩方法(主要用于微压) 
     * * @param bitmap 源图 
     * @param width_ratio 宽压比(float)(0<&&<1) 
     * @param height_ratio 高压比(float)(0<&&<1) 
     * @return 目标图片
     * <p> 
     */
    public static Bitmap anyRatioCompressing(Bitmap bitmap, float width_ratio, float height_ratio) {
        Matrix matrix = new Matrix();
        matrix.postScale(width_ratio, height_ratio);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
    }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容