Android自定义截图

开发中遇到截取屏幕,但是还要从截图中排除特定View的图像,或者有些业务截图无法通过api截取到(比如WebGL渲染的游戏),因此不能简单的调用系统截图API,或者在截图时隐藏特定的View(那也太low了)。因此我开发了个自定义截图的功能,主要分以下几个步骤:

1、创建自定义View容器

如继承FrameLayout、ConstraintLayout等,总之业务里原来的容器是什么,扩展它,在里面要加入一点代码。

创建属性定义attrs_capture_wrapper.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CaptureWrapperFrameLayout">
        <!--
        one custom capture:<br/>
        customCaptures="[viewId]=[BaseCustomCapture's sub class]"

        multi:
        customCaptures="[viewId_1]=[BaseCustomCapture's sub class]:[viewId_2]=[BaseCustomCapture's sub class]"
        eg.

        meme player custom capture:
        customCaptures="kSYTextureView=chat.meme.inke.screenshot.IJKCustomCapture"
        -->
        <attr name="customCaptures" format="string" />
    </declare-styleable>
</resources>

customCaptures主要是用来定义自定义图像绘制器,key-value形式,key是子View的id,value位自定义绘制器的完整类名。(由于我开发时不喜欢频繁切换输入法,所以可能会出现英文注释,这也是个好习惯,避免出现乱码。其它国家的同学也勉强能看懂)

以下是一段工程中的示例:

包裹起来的是一个播放器的View,核心是:【app:customCaptures=”kSYTextureView=chat.meme.inke.screenshot.IJKCustomCapture”】,指定了子View(kSYTextureView)的图像截图器为chat.meme.inke.screenshot.IJKCustomCapture

    <chat.meme.inke.screenshot.CaptureWrapperFrameLayout
        android:id="@+id/kSYTextureViewPanel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
     app:customCaptures="kSYTextureView=chat.meme.inke.screenshot.IJKCustomCapture"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <chat.meme.mediaplayer.MeMediaTextureView
            android:id="@+id/kSYTextureView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </chat.meme.inke.screenshot.CaptureWrapperFrameLayout>

如果原有view的⽗容器是FrameLayout,那么就CaptureWrapperFrameLayou将其包裹;如果原有view的⽗容器是ConstraintLayout,则⽤CaptureWrapperConstraintLayout包裹。

注意!CaptureWrapper[***]容器不要用作过于根部的容器,因为拦截了其drawChild:

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        if (CaptureWrapperUtil.checkAndDrawSpecificChild(canvas, child, mCustomCaptureMap)) {
            //draw event handled by capture canvas
            return false;
        }
        return super.drawChild(canvas, child, drawingTime);
    }

2、自定义Canvas(以便拦截绘制操作)

这个canvas继承自Canvas,

public class CaptureCanvas extends Canvas {
    public CaptureCanvas(Bitmap bitmap) {
        super(bitmap);
    }
}

这里只是用来区分默认的Canvas和截图时的Canvas

3、拦截绘制操作,用CustomCapture绘制或拦截特定业务View

以下是CaptureWrapperUtil.java的代码,主要功能

解析CustomCapture:

从View属性定义中拿到自定义绘制器的类名,并放到一个ArrayMap中以便后续使用

    /**
     * parse view's customCaptures attributes
     *
     * @param context
     * @param attrs
     * @return map for custom captures.
     * key:view child view id
     * value:custom capture
     */
    public static ArrayMap<String, String> parseCustomCaptures(@NonNull Context context, @Nullable AttributeSet attrs) {
        TypedArray typedArray = null;
        try {
            typedArray = context.obtainStyledAttributes(attrs, R.styleable.CaptureWrapperFrameLayout);
            boolean hasValue = typedArray.hasValue(R.styleable.CaptureWrapperFrameLayout_customCaptures);
            if (hasValue) {
                String customCaptures = typedArray.getString(R.styleable.CaptureWrapperFrameLayout_customCaptures);
                if (TextUtils.isEmpty(customCaptures))
                    return null;
                String[] capturesGroup = customCaptures.split(":");
                ArrayMap<String, String> map = new ArrayMap<>();
                for (String captureKv : capturesGroup) {
                    String[] kv = captureKv.split("=");
                    map.put(kv[0], kv[1]);
                }
                if (map.isEmpty()) {
                    map.put("default", customCaptures);
                }
                return map;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (typedArray != null) {
                typedArray.recycle();
            }
        }
        return null;
    }

解析到容器里面的自定义绘制器后,赋值给成员变量mCustomCaptureMap。

在ViewGroup的drawChild方法中拦截、绘制:

public class CaptureWrapperConstraintLayout extends ConstraintLayout {

    private final ArrayMap<String, String> mCustomCaptureMap;

    public CaptureWrapperConstraintLayout(@NonNull Context context) {
        this(context, null);
    }

    public CaptureWrapperConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CaptureWrapperConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public CaptureWrapperConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mCustomCaptureMap = CaptureWrapperUtil.parseCustomCaptures(context, attrs);
    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        if (CaptureWrapperUtil.checkAndDrawSpecificChild(canvas, child, mCustomCaptureMap)) {
            //draw event handled by capture canvas
            return false;
        }
        return super.drawChild(canvas, child, drawingTime);
    }
}

CaptureWrapperUtil.checkAndDrawSpecificChild()代码:

 /**
     * check and draw visible child to CaptureCanvas.
     *
     * @param canvas           custom canvas
     * @param child            specific view to draw
     * @param customCaptureMap custom capture config.
     * @return true draw event has been handled.
     */
    public static boolean checkAndDrawSpecificChild(Canvas canvas, View child, ArrayMap<String, String> customCaptureMap) {
        if (canvas instanceof CaptureCanvas) {
            drawCustomCaptureView(canvas, child, customCaptureMap);
            return true;
        }
        return false;
    }

    private static void drawCustomCaptureView(Canvas canvas, View child, ArrayMap<String, String> customCaptureMap) {
        if (child == null || child.getVisibility() != View.VISIBLE)
            return;
        boolean matchCustomCapture = false;

        String entryName;
        if (child.getId() != -1) {
            entryName = child.getResources().getResourceEntryName(child.getId());
        } else {
            entryName = "default";
        }
        if (customCaptureMap != null && !TextUtils.isEmpty(entryName)
                && customCaptureMap.containsKey(entryName)) {
            try {
                String customCaptureClass = customCaptureMap.get(entryName);
                Class<?> name = Class.forName(customCaptureClass);
                BaseCustomCapture customCapture = (BaseCustomCapture) name.newInstance();//这里通过反射构造出自定义绘制器
                customCapture.doCapture(canvas, child);//调用绘制器的绘制方法(每个自定义绘制器的绘制方式各不相同)
                matchCustomCapture = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

//没有使用自定义绘制器的,使用默认绘制方式
        if (!matchCustomCapture) {
            if (child instanceof GLSurfaceView) {//如果是GLSurafaceView或者TextureView,需要特殊绘制方式
                try {
                    CaptureWrapperUtil.drawGLSurfaceView((GLSurfaceView) child, canvas);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else if (child instanceof TextureView) {
                try {
                    CaptureWrapperUtil.drawTextureView((TextureView) child, canvas);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {//普通View则还是常规操作:
                int save = canvas.save();
                canvas.translate(child.getLeft(), child.getTop());
                child.draw(canvas);
                canvas.restoreToCount(save);
            }
        }
    }

    /**
     * draw GLSurfaceView's bitmap to canvas
     *
     * @param surfaceView gl surface view
     * @param canvas      custom canvas
     */
    public static void drawGLSurfaceView(GLSurfaceView surfaceView, Canvas canvas) {
        if (surfaceView.getWindowToken() != null) {
            Bitmap bitmap = getBitmapFromGLSurfaceView(surfaceView);
            if (bitmap == null)
                return;
            drawBitmap(surfaceView, canvas, bitmap);
            try {
                if (!bitmap.isRecycled())
                    bitmap.recycle();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void drawTextureView(TextureView textureView, Canvas canvas) {
        if (textureView.getWindowToken() == null)
            return;
        Bitmap bitmap = textureView.getBitmap();
        drawBitmap(textureView, canvas, bitmap);
        try {
            if (!bitmap.isRecycled())
                bitmap.recycle();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * read bitmap from GL context.
     *
     * @param surfaceView gl surface view.
     * @return Bitmap from gl
     */
    public static Bitmap getBitmapFromGLSurfaceView(GLSurfaceView surfaceView) {
        final Bitmap[] bitmap = new Bitmap[1];
        //To wait for the async call to finish before going forward
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        surfaceView.queueEvent(new Runnable() {
            @Override
            public void run() {
                try {
                    EGL10 egl = (EGL10) EGLContext.getEGL();
                    egl.eglWaitGL();
                    GL10 gl = (GL10) egl.eglGetCurrentContext().getGL();
                    Log.d(TAG, "before glReadPixels()");
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    bitmap[0] = BitmapUtil.createBitmapFromGLSurface(0, 0,
                            surfaceView.getWidth(), surfaceView.getHeight(), gl);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                }
            }
        });

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return bitmap[0];
    }

    /**
     * draw bitmap to canvas.
     * position by view self
     *
     * @param view
     * @param canvas
     * @param bitmap
     */
    public static void drawBitmap(View view, Canvas canvas, Bitmap bitmap) {
        Paint paint = new Paint();
        if (bitmap == null) return;
        Rect rect = new Rect(view.getLeft() + view.getPaddingStart()
                , view.getTop() + view.getPaddingTop(),
                view.getWidth() + view.getLeft() - view.getPaddingEnd(),
                view.getHeight() + view.getTop() - view.getPaddingBottom());
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
        canvas.drawBitmap(bitmap, null, rect, paint);
    }

    static void drawDefault(Canvas canvas, View self) {
        try {
            int save = canvas.save();
            canvas.translate(self.getLeft(), self.getTop());
            self.draw(canvas);
            canvas.restoreToCount(save);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

4、调用截图功能

完整代码如下:

(由于是耗时操作,这里使用了RxJava进行线程切换)

public class ScreenshotTakerCompact {

    public static Observable<Bitmap> startScreenshot(@NonNull final Activity activity, final int drawable, View rootView) {
        return Observable.defer(new Func0<Observable<Bitmap>>() {
            @Override
            public Observable<Bitmap> call() {
                Bitmap screenBitmap = getScreenshotBitmap(activity, (ViewGroup) rootView, drawable);
                if (screenBitmap != null) {
                    return Observable.just(screenBitmap);
                } else {
                    return Observable.error(new RuntimeException());
                }
            }
        });
    }

    /**
     * generate screen shot
     * @param activity
     * @param rootView
     * @param drawable
     * @return
     */
    public static Bitmap getScreenshotBitmap(Activity activity, ViewGroup rootView, int drawable) {
        if (activity == null) {
            throw new IllegalArgumentException("Parameter activity cannot be null.");
        }

        View main = activity.getWindow().getDecorView();

//创建一个窗口大小的Bitmap
        final Bitmap bitmap;
        try {
            bitmap = Bitmap.createBitmap(main.getWidth(), main.getHeight(), Bitmap.Config.ARGB_8888);
        } catch (final Throwable e) {
            return null;
        }

//创建自定义Canvas
        CaptureCanvas canvas = new CaptureCanvas(bitmap);
        rootView.draw(canvas);//start draw bitmap

        //water mask
        if (drawable != -1){
            Paint paint = new Paint();
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
            Bitmap front = BitmapFactory.decodeResource(activity.getResources(), drawable);
            canvas.drawBitmap(front, 50, bitmap.getHeight() / 2f, paint);
        }

        return bitmap;
    }
}

5、附录:项目中用到的几个CustomCapture:

1、摄像头采集模块的自定义GLSurfaceView截图:

@SuppressWarnings("unused")
@Keep
public class CameraGLSurfaceCapture extends BaseCustomCapture<View> {
    public CameraGLSurfaceCapture() {
        super();
    }

    @Override
    public void doCapture(Canvas canvas, View self) {
        super.doCapture(canvas, self);

        try {
            if (self instanceof GLSurfaceView) {
                Object tag = self.getTag(R.id.sdk_capture_interface);
                if (tag instanceof SdkPictureCapture) {
                    CountDownLatch countDownLatch = new CountDownLatch(1);
                    final Bitmap[] bitmap = new Bitmap[1];
                    boolean result = ((SdkPictureCapture) tag).takePicture(bmp -> {
                        try {
                            bitmap[0] = bmp;
                        } finally {
                            countDownLatch.countDown();
                        }
                    });

                    if (!result) {
                        Log.w(TAG, "doCapture: takePicture failed.");
                        return;
                    }

                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    if (bitmap[0] == null)
                        return;
                    CaptureWrapperUtil.drawBitmap(self, canvas, bitmap[0]);
                    try {
                        if (!bitmap[0].isRecycled())
                            bitmap[0].recycle();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else {
                    try {
                        CaptureWrapperUtil.drawGLSurfaceView((GLSurfaceView) self, canvas);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            } else {
                drawDefault(canvas, self);
            }
        } catch (Exception e) {
            e.printStackTrace();
            drawDefault(canvas, self);
        }
    }

    private void drawDefault(Canvas canvas, View self) {
        try {
            int save = canvas.save();
            canvas.translate(self.getLeft(), self.getTop());
            self.draw(canvas);
            canvas.restoreToCount(save);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

2、礼物动画的截图(APNG需要特殊处理,其它的Webp等默认绘制即可)

/**
 * capture for gift animation.
 */
@Keep
@SuppressWarnings("unused")
public class GiftCustomCapture extends BaseCustomCapture<ImageView> {
    public GiftCustomCapture() {
        super();
    }

    @Override
    public void doCapture(Canvas canvas, ImageView self) {
        super.doCapture(canvas, self);
        Drawable drawable = self.getDrawable();
        if (drawable instanceof ApngDrawable) {
            try {
                int save = canvas.save();
                CaptureWrapperUtil.drawBitmap(self, canvas,
                        ((ApngDrawable) drawable).getCurrentBitmap(((ApngDrawable) drawable).getCurrentFrame()));
                canvas.restoreToCount(save);
            } catch (Exception e) {
                e.printStackTrace();
                self.draw(canvas);
            }
        } else {
            CaptureWrapperUtil.drawDefault(canvas, self);
        }
    }
}

3、IJK播放器截图

/**
 * custom capture for IJK player
 * @apiNote don't delete
 */
@Keep
@SuppressWarnings("unused")
public class IJKCustomCapture extends BaseCustomCapture<MeMediaTextureView> {
    public IJKCustomCapture() {
        super();
    }

    @Override
    public void doCapture(Canvas canvas, MeMediaTextureView self) {
        super.doCapture(canvas, self);
        Bitmap bitmap = null;
        int save = 0;
        try {
            save = canvas.save();
            Paint paint = new Paint();
            int[] outLocation = new int[2];
            self.getLocationOnScreen(outLocation);
            bitmap = self.getScreenShot();
            if (bitmap == null) return;
            int height = bitmap.getHeight();
            int width = bitmap.getWidth();
            int viewHeight = self.getHeight();
            int viewWidth = width * viewHeight / height;

            Rect rect = new Rect((self.getWidth() - viewWidth) / 2, self.getTop(),
                    (self.getWidth() - viewWidth) / 2 + viewWidth, viewHeight + self.getTop());
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
            canvas.drawBitmap(bitmap, null, rect, paint);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            canvas.restoreToCount(save);
            if (bitmap != null && !bitmap.isRecycled()) {
                try {
                    bitmap.recycle();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4、忽略View截图(截图时排除页面上指定的View)

/**
 * ignore specific view from screen shot
 */
@SuppressWarnings("unused")
@Keep
public class IgnoreCustomCapture extends BaseCustomCapture {
    public IgnoreCustomCapture() {
        super();
    }

    @Override
    public void doCapture(Canvas canvas, Object self) {
        Log.d(TAG, "doCapture: ignore this view's screen capture. " + self);
    }
}

5、直播间游戏截图(在WebView中,通过WebGL渲染的游戏)

/**
 * custom screen capture for InRoomGame
 */
@Keep
@SuppressWarnings("unused")
public class InRoomGameCapture extends BaseCustomCapture<FrameLayout> {
    public InRoomGameCapture() {
        super();
    }

    @Override
    public void doCapture(Canvas canvas, FrameLayout self) {
        super.doCapture(canvas, self);
        try {
            //1.find fragment by view id.
            Fragment fragment = null;
            try {
                fragment = ((FragmentActivity) ResourceUtils.getActivity(self.getContext()))
                        .getSupportFragmentManager()
                        .findFragmentById(R.id.drag_place_holder);
            } catch (Exception e) {
                e.printStackTrace();
            }

            //2.check parent.
            if (fragment == null
                    || fragment.getView() == null
                    || fragment.getView() != self.getParent()) {
                List<Fragment> fragments = ((FragmentActivity) ResourceUtils.getActivity(self.getContext()))
                        .getSupportFragmentManager().getFragments();
                for (Fragment subFragment : fragments) {
                    if (subFragment.getView() == self.getParent()) {
                        fragment = subFragment;
                        break;
                    }
                }
            }

            if (fragment instanceof RoomGameContainerFragment) {
                CountDownLatch countDownLatch = new CountDownLatch(1);
                final Bitmap[] bitmap = new Bitmap[1];
                AtomicBoolean hasReady = new AtomicBoolean(false);
                ((RoomGameContainerFragment) fragment).takeScreenShot(new ScreenShotCallback() {
                    @Override
                    public void onScreenShot(Bitmap gameCaptureBm) {
                        try {
                            if (gameCaptureBm == null) {
                                hasReady.set(false);
                                Bitmap bmp = Bitmap.createBitmap(
                                        self.getWidth() - self.getPaddingStart() - self.getPaddingEnd()
                                        , self.getHeight() - self.getPaddingEnd() - self.getPaddingStart()
                                        , Bitmap.Config.ARGB_8888);
                                Canvas canvas = new Canvas(bmp);
                                canvas.drawColor(0xff171717);
                                bitmap[0] = bmp;
                            } else {
                                hasReady.set(true);
                                bitmap[0] = gameCaptureBm;
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        } finally {
                            countDownLatch.countDown();
                        }
                    }
                });

                //waiting screen shot completed.
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (bitmap[0] == null)
                    return;
                if (hasReady.get()) {
                    //draw background
                    drawDefault(canvas, self);
                    //draw game screen shot on top of view.
                    CaptureWrapperUtil.drawBitmap(self, canvas, bitmap[0]);
                } else {
                    //draw background
                    drawDefault(canvas, self);
                }
                try {
                    if (!bitmap[0].isRecycled())
                        bitmap[0].recycle();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                drawDefault(canvas, self);
            }
        } catch (Exception e) {
            e.printStackTrace();
            drawDefault(canvas, self);
        }
    }

}

6、飞行棋截图

/**
 * custom screen capture for LudoGame
 */
@SuppressWarnings("unused")
@Keep
public class LudoGameCapture extends BaseCustomCapture<FrameLayout> {
    public LudoGameCapture() {
        super();
    }

    @Override
    public void doCapture(Canvas canvas, FrameLayout self) {
        super.doCapture(canvas, self);
        try {
            FragmentActivity activity = (FragmentActivity) ResourceUtils.getActivity(self.getContext());
            Fragment mLudoWebFragment = activity.getSupportFragmentManager().findFragmentByTag("ludo_web");
            if (mLudoWebFragment != null) {
                if (mLudoWebFragment instanceof LudoWebFragment) {
                    if (!((LudoWebFragment) mLudoWebFragment).isGameReady()) {
                        return;
                    }
                    //To wait for the async call to finish before going forward
                    CountDownLatch countDownLatch = new CountDownLatch(1);
                    final Bitmap[] bitmap = new Bitmap[1];
                    new Thread(new Runnable(){
                        @Override
                        public void run() {
                            ((LudoWebFragment) mLudoWebFragment).takeScreenShot(new ScreenShotCallback() {
                                @Override
                                public void onScreenShot(Bitmap gameCaptureBm) {
                                    try {
                                        if (!((LudoWebFragment) mLudoWebFragment).isGameReady()) {
                                            return;
                                        }
                                        bitmap[0] = gameCaptureBm;
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    } finally {
                                        countDownLatch.countDown();
                                    }
                                }
                            });
                        }
                    }).start();

                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    if (bitmap[0] == null)
                        return;
                    CaptureWrapperUtil.drawBitmap(self, canvas, bitmap[0]);
                    try {
                        if (!bitmap[0].isRecycled())
                            bitmap[0].recycle();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}