SurfaceView截图

23

我正在开发一个简单的相机应用程序。我有一些代码可以截取整个活动界面并将其写入SD卡。问题是SurfaceView返回的是黑屏。

我想知道如何独立地仅截取SurfaceView的屏幕截图。以下是可截取整个活动界面的代码。

findViewById(R.id.screen).setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        final RelativeLayout layout = (RelativeLayout) findViewById(R.id.RelativeLayout1);
        layout.setVisibility(RelativeLayout.GONE);
        Bitmap bitmap = takeScreenshot();
        Toast.makeText(getApplicationContext(),"Please Wait", Toast.LENGTH_LONG).show();
        saveBitmap(bitmap);
    }
});

public Bitmap takeScreenshot() {
    View rootView = findViewById(android.R.id.content).getRootView();
    rootView.setDrawingCacheEnabled(true);
    return rootView.getDrawingCache();
}


public void saveBitmap(Bitmap bitmap) {
    final MediaPlayer cheer = MediaPlayer.create(PicShot.this, R.raw.shutter);
    cheer.start();
    
    Random generator = new Random();
    int n = 10000;
    n = generator.nextInt(n);
    String fname = "Image-"+ n +".png";

    final RelativeLayout layout = (RelativeLayout) findViewById(R.id.RelativeLayout1);
    File imagePath = new File(Environment.getExternalStorageDirectory() + "/" + fname);
    FileOutputStream fos;

    try {
        fos = new FileOutputStream(imagePath);
        bitmap.compress(CompressFormat.PNG, 100, fos);
        fos.flush();
        fos.close();
        layout.setVisibility(RelativeLayout.VISIBLE);
        Intent share = new Intent(Intent.ACTION_SEND);
        share.setType("image/*");
        Uri uri = Uri.fromFile(imagePath);
        share.putExtra(Intent.EXTRA_STREAM,uri);
        startActivity(Intent.createChooser(share, "Share Image"));
    } catch (FileNotFoundException e) {
        Log.e("GREC", e.getMessage(), e);
    } catch (IOException e) {
        Log.e("GREC", e.getMessage(), e);
    }
}

是的,最后一个答案对我有效,但需要API级别>=24。 - urs32
6个回答

19
SurfaceView的表面独立于绘制View元素的表面。因此,捕获View内容将不包括SurfaceView。
您需要单独捕获SurfaceView内容并执行自己的合成步骤。最简单的捕获方法可能是重新渲染内容,但将离屏位图用作目标,而不是表面。如果您正在使用GLES渲染到离屏pbuffer,则可以在交换缓冲区之前使用glReadPixels()。
更新:Grafika的“摄像头纹理”活动演示了如何使用OpenGL ES处理来自摄像头的实时视频。EglSurfaceBase#saveFrame()显示了如何将GLES渲染捕获到位图中。
更新:另请参见this answer,其中提供了更多背景信息。

1
假设您所有的渲染都是在从Surface获取的Canvas上完成的,只需创建一个由Bitmap支持的Canvas(请参见例如http://developer.android.com/guide/topics/graphics/2d-graphics.html)。根据表面的组合方式,您可以将一个表面复制到另一个表面上,或将这对表面合成到第三个表面上。 - fadden
你好,你提供的文章似乎没有涵盖从相机显示帧的SurfaceView保存为位图的情况。你有一些示例代码可以分享吗? - ocramot
像素数据源并不重要 - 一旦它在表面上,它只是像素。 - fadden
1
...代码示例或“需要做什么”也很有趣。对于grafika的提示加一。 - user1767754
你如何在SurfaceView中使用glReadPixels?这有点令人困惑,你能提供代码示例吗? - chia yongkang
显示剩余4条评论

14

只需复制并粘贴代码

注意:仅适用于API级别 >= 24

private void takePhoto() {

        // Create a bitmap the size of the scene view.
        final Bitmap bitmap = Bitmap.createBitmap(surfaceView.getWidth(), surfaceView.getHeight(),
                Bitmap.Config.ARGB_8888);



        // Create a handler thread to offload the processing of the image.
        final HandlerThread handlerThread = new HandlerThread("PixelCopier");
        handlerThread.start();
        // Make the request to copy.
        PixelCopy.request(holder.videoView, bitmap, (copyResult) -> {
            if (copyResult == PixelCopy.SUCCESS) {
                Log.e(TAG,bitmap.toString());
                String name = String.valueOf(System.currentTimeMillis() + ".jpg");
                imageFile = ScreenshotUtils.store(bitmap,name);

            } else {
                Toast toast = Toast.makeText(getViewActivity(),
                        "Failed to copyPixels: " + copyResult, Toast.LENGTH_LONG);
                toast.show();
            }
            handlerThread.quitSafely();
        }, new Handler(handlerThread.getLooper()));
    }

是的,这个函数适用于 Android 版本 24 及以上。请查看此链接:-->https://developer.android.com/reference/android/view/PixelCopy - ND1010_
API 24以下的想法?因为没有一个答案对我有用。 - David
如果您想捕获指定视图,您应该使用PixelCopy类。 - ND1010_
1
@chiayongkang,你只需要传递你的视图而不是我的holder.videoView - ND1010_
1
@ND1010_,请问Screenshot.utils类的存储函数代码在哪里?我很好奇你是如何存储位图的。 - chia yongkang
显示剩余3条评论

4

我的情况与 ExoPlayer 相关,需要获取当前帧的 bitmap

适用于 API >= 24

private val copyFrameHandler = Handler()

fun getFrameBitmap(callback: FrameBitmapCallback) {
    when(val view = videoSurfaceView) {
        is TextureView -> callback.onResult(view.bitmap)
        is SurfaceView -> {
            val bitmap = Bitmap.createBitmap(
                    videoSurfaceView.getWidth(),
                    videoSurfaceView.getHeight(),
                    Bitmap.Config.ARGB_8888
            )
            
            copyFrameHandler.removeCallbacksAndMessages(null)
            
            PixelCopy.request(view, bitmap, { copyResult: Int ->
                if (copyResult == PixelCopy.SUCCESS) {
                    callback.onResult(bitmap)
                } else {
                    callback.onResult(null)
                }
            }, copyFrameHandler)
        }
        else -> callback.onResult(null)
    }
}

fun onDestroy() {
    copyFrameHandler.removeCallbacksAndMessages(null)
}

interface FrameBitmapCallback {
    fun onResult(bitmap: Bitmap?)
}

3
如果您的情况允许,使用 TextureView 而不是 SurfaceView 将大大容易解决此问题。它有一个方法 getBitmap() 返回当前帧在 TextureView 上的 Bitmap

1

这是我的做法。将这个方法放到一个名为Util的类中:

/**
 * Pixel copy to copy SurfaceView/VideoView into BitMap
 */
fun usePixelCopy(videoView: SurfaceView,  callback: (Bitmap?) -> Unit) {
    val bitmap: Bitmap = Bitmap.createBitmap(
        videoView.width,
        videoView.height,
        Bitmap.Config.ARGB_8888
    );
    try {
        // Create a handler thread to offload the processing of the image.
        val handlerThread = HandlerThread("PixelCopier");
        handlerThread.start();
        PixelCopy.request(
            videoView, bitmap,
            PixelCopy.OnPixelCopyFinishedListener { copyResult ->
                if (copyResult == PixelCopy.SUCCESS) {
                    callback(bitmap)
                }
                handlerThread.quitSafely();
            },
            Handler(handlerThread.looper)
        )
    } catch (e: IllegalArgumentException) {
        callback(null)
        // PixelCopy may throw IllegalArgumentException, make sure to handle it
        e.printStackTrace()
    }
}

使用方法:

usePixelCopy(videoView) { bitmap: Bitmap? ->
    processBitMp(bitmap)
}

注意: VideoViewSurfaceView 的子类,因此该方法也可以截取视频视图的屏幕截图。


为什么 Canvas canvas = new Canvas(bitmap); surfaceView.draw(canvas); 不能正常工作? - Ayyappa

1

对于那些寻找 API <= 24 解决方案的人,这是我作为一种解决方法所做的。当用户点击捕获按钮时,关闭预览 captureSession 并创建一个新的 captureSession 以进行静态图像捕获。

mCaptureSession.stopRepeating();
mCaptureSession.abortCaptures();
mCaptureSession.close();
mCaptureSession = null;

mPreviewReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), ImageFormat.JPEG, MAX_IMAGES);
mPreviewReader.setOnImageAvailableListener(mImageAvailableListener, mBackgroundHandler);

mCameraDevice.createCaptureSession(
    Arrays.asList(mPreviewReader.getSurface()),
    //Arrays.asList(mSurface),
    new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
            mCaptureSession = cameraCaptureSession;
            try {
                final CaptureRequest.Builder captureBuilder =
                mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
                captureBuilder.addTarget(mPreviewReader.getSurface());
                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
                captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(mOrientation));
                Log.d(TAG, "Capture request created.");
                mCaptureSession.capture(captureBuilder.build(), mCaptureCallback, mBackgroundHandler);
            } catch (CameraAccessException cae) {
                Log.d(TAG, cae.toString());
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {}
    },
    mBackgroundHandler
);

然后在onImageAvailableListener中,您可以使用imageReader.acquireNextImage()获取已捕获的静态图像。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接