开发中遇到截取屏幕,但是还要从截图中排除特定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();
}
}
}