Android长截屏(滚动截屏)实现原理

Google原生Android系统到目前为止均没有长截屏这一功能,而对于用户而言,这是一个非常实用的功能,如聊天记录,新闻页面等较长的页面想通过一张图片的形式保存起来.好在国内主流手机厂商均已实现了该功能,接下来聊聊我们长截屏的实现原理.

长截屏原理概述:

我们简单的把一个屏幕分成三分,上中下,中间区域最大,中间区域也就是滑动区域;长截屏开始,我们截取顶部的图片保存到集合中,截取长度如下,然后每次滑动图片前截取中间部分图片保存到集合中,也就是滑动多少次就会有多少张中间区域图片,最后保存时,截取底部图片保存到集合中;随后把集合中的图片拼接起来就完成了长截屏

中间部分为滑动区域,也就是我们模拟手指在屏幕上滑动一个指定长度,这个长度也就是中间图片的长度.模拟滑动代码如下:

    // 模拟Down事件
    MotionEvent tempDown = MotionEvent.obtain(downTime, eventTime, 0, toX, toY, 0);
    tempDown.setSource(4098);
    InputManager.getInstance().injectInputEvent(tempDown, Integer.valueOf(2));
     // 模拟Move事件
    MotionEvent tempMove = MotionEvent.obtain(downTime, eventTime, 2, toX, toY, 0);
    tempMove.setSource(4098);
    InputManager.getInstance().injectInputEvent(tempMove, Integer.valueOf(2));
    // 模拟Up事件
    MotionEvent tempUP = MotionEvent.obtain(downTime, eventTime, 1, toX, toY, 0);
    tempUP.setSource(4098);
    InputManager.getInstance().injectInputEvent(tempUP, Integer.valueOf(2));
    

injectInputEvent方法为@hide,没环境调用的可以通过反射调用:

    try {
        Class<?> ClassInputManager = Class.forName("android.hardware.input.InputManager");
        Method[] methods = ClassInputManager.getMethods();
        Method methodGetInstance = null;
        Method methodInjectInputEvent = null;
        Method method = methods[0];

        for (int i = 0; i < methods.length; i++) {
            method = methods[i];
            if (method.getName().equals("getInstance")) {
                methodGetInstance = method;
            }
            if (method.getName().equals("injectInputEvent")) {
                methodInjectInputEvent = method;
            }
        }
        Object mInstance = methodGetInstance.invoke(ClassInputManager, new Object[0]);
        long downTime = SystemClock.uptimeMillis();
        long eventTime = SystemClock.uptimeMillis();
        float y = fromY;

        if (fromY > toY) {
            // action down
            MotionEvent eventDown = MotionEvent.obtain(downTime, eventTime, 0, fromX, y, 0);
            eventDown.setSource(4098);
            Object[] arrayOfObject2 = new Object[2];
            arrayOfObject2[0] = eventDown;
            arrayOfObject2[1] = Integer.valueOf(2);
            methodInjectInputEvent.invoke(mInstance, arrayOfObject2);
            eventDown.recycle();
        }
        // action move
        MotionEvent eventMove2 = MotionEvent.obtain(downTime, eventTime, 2, toX, y, 0);
        eventMove2.setSource(4098);
        Object[] objMove2 = new Object[2];
        objMove2[0] = eventMove2;
        objMove2[1] = Integer.valueOf(2);
        methodInjectInputEvent.invoke(mInstance, objMove2);
        eventMove2.recycle();

        // action up
        MotionEvent eventUp = MotionEvent.obtain(downTime, eventTime, 1, toX, toY, 0);
        eventUp.setSource(4098);
        Object[] objUp = new Object[2];
        objUp[0] = eventUp;
        objUp[1] = Integer.valueOf(2);
        methodInjectInputEvent.invoke(mInstance, objUp);
        eventUp.recycle();
    } catch (Exception e) {
        e.printStackTrace();
    }

细节(核心):

以上方式完成长截屏理论上没有问题,但是还有一个细节,也是核心;比如每次模拟滑动10厘米,但是最后一次滑动已经接近底部了,实际距离不到10厘米,因此最后一张图片的实际长度我们无法知道.(通俗的说是我们默认滑动10厘米,但是约2厘米的时候到底划不动了,而默认截取的长度还是10厘米,因此我们需要去掉多余的8厘米长度图片)

左边为中间区域倒数第二张图片 右边为中间区域倒数第一张图片:

如上图所示,蓝色矩形部分为最后一张图片的实际滑动距离,红叉区域由于滑动到底,滑不动导致的重叠部分.

因此,我们需要截取最后一张图的实际长度,如下图:

怎么样去掉重叠部分的图片?

如下图,截取上述两张图片的左侧区域(2号图的左下红色区域,1号图的左侧红色区域),接下进行图片匹配裁剪,排除部分滑动控件有SeekBar的情况.

如上图,获取裁减后的图2,从底部开始,取一个像素高的图片,所得图片的每个像素颜色转为int值,存入pixels2,函数如下:

target.getPixels(pixels2, 0, this.mCropW, 0, (target.getHeight() - 1), this.mCropW, 1);

从底部开始依次遍历图1一个像素为高度的图片,同理获取所得图片的每个像素颜色转为int值,存入pixels1;如果匹配,将匹配的图片高度存入lineList集合

    for (int i = 0; i < source.getHeight(); i = i + 1) {
        source.getPixels(pixels1, 0, this.mCropW, 0, i, this.mCropW, 1);
        if (Arrays.equals(pixels2, pixels1)) {
            lineList.add(Integer.valueOf(i));
        }
    }

由于我们取的是一个像素为高度的样本图片,因此匹配到相等的情况比较常见.

随后,遍历lineList,对匹配相识度最高的图片的高度进行裁剪.

匹配方法和上述方法类似,遍历裁剪后的图2的高度,遍历累计增加一个高度;图1裁剪后的图片取底部一个像素的高度和图2遍历的高度取一个像素的高度对比,如果相等记录一下,随后依次取倒数第二个像素开始的一个像素的高度进行对比,通过总次数和匹配相等的次数算是一个匹配概率.最后取概率最高的lineList中的高度.

    private float compareLastBitmap(Bitmap bitmap2, Bitmap bitmap1, int index) {
        int[] pixels_target = new int[mCropW];
        int[] pixels_source = new int[mCropW];
        int count = 0;
        int height = Math.min((bitmap2.getHeight() - 1), index);
        height = Math.max(height, 1);
        for (int i = 0x0; i < height; i = i + 1) {
            // 倒数第二张图片的底部  一个像素高度的图片  (遍历累计减少高度)
            bitmap2.getPixels(pixels_target, 0, mCropW, 0, ((bitmap2.getHeight() - 1) - i), mCropW, 1);
            // 最后一张图片  指定高度开始(遍历累计减少高度)   一个像素高度的图片
            bitmap1.getPixels(pixels_source, 0, mCropW, 0, (index - i), mCropW, 1);
            if (Arrays.equals(pixels_target, pixels_source)) {
                count = count + 1;
            }
        }
        return (float) count / (float) height;
    }
    
    // 取概率最高的lineList中的高度
    int line_index = -1;
    for (int i = 0; i < lineList.size(); i = i + 1) {
        float percent = compareLastBitmap(target, source, lineList.get(i).intValue());
        if (percent == 1.0) {
            list.add(lineList.get(i).intValue());
            if (list.size() > 80) {    // 底部图片相同导致size过大循环过长的情况
                line_index = lineList.get(i).intValue();
                list.clear();
                break;
            }
        }
        if (percent > bigger) {
            bigger = percent;
            line_index = lineList.get(i).intValue();
        }
    }

貌似找到匹配度为100%然后对对应图片高度进行裁剪就完了,但是匹配度为100%情况有时候会出现多个,这是为什么?

原来当一张图片中 相似的部分很多的时候(例如图片中重复空白区域很多),我们每次取一个像素进行对比很容易出现匹配度为100%的情况,因此接下我们还得为这种情况进行处理.

通过递归的方式每次累计增加样本图片的高度(之前每次都是取一个像素为高度,递归累计增加图片的高度进行匹对)

    // space为取对比图片的高度为几个像素
    private int compare(Bitmap target, Bitmap source, ArrayList<Integer> list, int space) {
        int count = 0;
        int line_height = 0;
        for (int j = 0; j < list.size(); j++) {

            int[] pixels_target = new int[mCropW * space];
            int[] pixels_source = new int[mCropW * space];
            line_height = list.get(j).intValue();
            if ((line_height - space) < 0) {
                break;
            }
            if ((target.getHeight() - space) < 0) {
                break;
            }
            // 取space高度的样本对两张图片进行比较,遍历累计增加样本图片的高度
            target.getPixels(pixels_target, 0, this.mCropW, 0, (target.getHeight() - space), this.mCropW, space);
            source.getPixels(pixels_source, 0, this.mCropW, 0, line_height - space, this.mCropW, space);
            if (Arrays.equals(pixels_target, pixels_source)) {
                count++;
            }
        }
        // 递归增加图片高度进行对比
        if (count > 1) {
            return compare(target, source, list, ++space);
        }
        return line_height;
    }

获取需要裁剪的高度后,裁剪图片,裁剪后的图片如下:

随后将如上图片替换到图片集合中最后一张图片(有重复区域的图片).

最后将如下底部的图片保存到集合,然后进行图片拼接,完成长截屏.

图片拼接: 首先创建一个 所有集合中图片长之和为长,屏宽为宽度的图片;遍历集合,依次将集合中的图片按坐标绘制到刚刚所创建的图片上

Android屏幕截图实现方式 & 系统截屏源码分析和三指截屏

Android屏幕录制源码Demo下载