Скриншот Android Take из Surface View показывает черный экран
Я пытаюсь сделать снимок экрана моей игры с помощью кода и поделиться им через намерение. Я могу сделать из этих вещей, однако скриншот всегда выглядит черным. Вот код, связанный с обменом снимками экрана:
View view = MainActivity.getView();
view.setDrawingCacheEnabled(true);
Bitmap screen = Bitmap.createBitmap(view.getDrawingCache(true));
.. save Bitmap
Это в MainActivity:
view = new GameView(this);
view.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT,
RelativeLayout.LayoutParams.FILL_PARENT));
public static SurfaceView getView() {
return view;
}
И сам взгляд:
public class GameView extends SurfaceView implements SurfaceHolder.Callback {
private static SurfaceHolder surfaceHolder;
...etc
И вот как я рисую все:
Canvas canvas = surfaceHolder.lockCanvas(null);
if (canvas != null) {
Game.draw(canvas);
...
Хорошо, основываясь на некоторых ответах, я построил это:
public static void share() {
Bitmap screen = GameView.SavePixels(0, 0, Screen.width, Screen.height);
Calendar c = Calendar.getInstance();
Date d = c.getTime();
String path = Images.Media.insertImage(
Game.context.getContentResolver(), screen, "screenShotBJ" + d
+ ".png", null);
System.out.println(path + " PATH");
Uri screenshotUri = Uri.parse(path);
final Intent emailIntent = new Intent(
android.content.Intent.ACTION_SEND);
emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
emailIntent.putExtra(Intent.EXTRA_STREAM, screenshotUri);
emailIntent.setType("image/png");
Game.context.startActivity(Intent.createChooser(emailIntent,
"Share High Score:"));
}
Gameview содержит следующий метод:
public static Bitmap SavePixels(int x, int y, int w, int h) {
EGL10 egl = (EGL10) EGLContext.getEGL();
GL10 gl = (GL10) egl.eglGetCurrentContext().getGL();
int b[] = new int[w * (y + h)];
int bt[] = new int[w * h];
IntBuffer ib = IntBuffer.wrap(b);
ib.position(0);
gl.glReadPixels(x, 0, w, y + h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, ib);
for (int i = 0, k = 0; i < h; i++, k++) {
for (int j = 0; j < w; j++) {
int pix = b[i * w + j];
int pb = (pix >> 16) & 0xff;
int pr = (pix << 16) & 0x00ff0000;
int pix1 = (pix & 0xff00ff00) | pr | pb;
bt[(h - k - 1) * w + j] = pix1;
}
}
Bitmap sb = Bitmap.createBitmap(bt, w, h, Bitmap.Config.ARGB_8888);
return sb;
}
Снимок экрана по-прежнему черный. Возможно, что-то не так с тем, как я это сохраняю?
Я попытался сделать несколько разных способов сделать снимок экрана, но ни один из них не сработал: тот, который показан в приведенном выше коде, был наиболее рекомендуемым. Но это не похоже на работу. Это проблема с использованием SurfaceView? И если да, то почему view.getDrawingCache (true) даже существует, если я не могу его использовать и как это исправить?
Мой код:
public static void share() {
// GIVES BLACK SCREENSHOT:
Calendar c = Calendar.getInstance();
Date d = c.getTime();
Game.update();
Bitmap.Config conf = Bitmap.Config.RGB_565;
Bitmap image = Bitmap.createBitmap(Screen.width, Screen.height, conf);
Canvas canvas = GameThread.surfaceHolder.lockCanvas(null);
canvas.setBitmap(image);
Paint backgroundPaint = new Paint();
backgroundPaint.setARGB(255, 40, 40, 40);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
backgroundPaint);
Game.draw(canvas);
Bitmap screen = Bitmap.createBitmap(image, 0, 0, Screen.width,
Screen.height);
canvas.setBitmap(null);
GameThread.surfaceHolder.unlockCanvasAndPost(canvas);
String path = Images.Media.insertImage(
Game.context.getContentResolver(), screen, "screenShotBJ" + d
+ ".png", null);
System.out.println(path + " PATH");
Uri screenshotUri = Uri.parse(path);
final Intent emailIntent = new Intent(
android.content.Intent.ACTION_SEND);
emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
emailIntent.putExtra(Intent.EXTRA_STREAM, screenshotUri);
emailIntent.setType("image/png");
Game.context.startActivity(Intent.createChooser(emailIntent,
"Share High Score:"));
}
Спасибо.
6 ответов
В этом много путаницы и несколько правильных ответов.
Вот сделка:
SurfaceView состоит из двух частей: Surface и View. Поверхность находится на совершенно отдельном слое от всех элементов пользовательского интерфейса View.
getDrawingCache()
подход работает только на слое вида, поэтому он ничего не захватывает на поверхности.Буферная очередь имеет API производителя-потребителя и может иметь только одного производителя. Canvas - один производитель, GLES - другой. Вы не можете рисовать с помощью Canvas и читать пиксели с помощью GLES. (Технически, вы могли бы это сделать, если Canvas использовал GLES и правильный контекст EGL действовал, когда вы читали пиксели, но это не гарантировано. Рендеринг Canvas на поверхность не ускоряется ни в одной выпущенной версии Android, поэтому сейчас есть нет надежды на то, что это работает.)
(Не относится к вашему случаю, но я упомяну это для полноты:) Поверхность не является буфером кадров, это очередь буферов. Когда вы отправляете буфер с GLES, он исчезает, и вы больше не можете читать из него. Поэтому, если вы выполняете рендеринг с помощью GLES и захватываете с помощью GLES, вам необходимо прочитать пиксели обратно перед вызовом
eglSwapBuffers()
,
С рендерингом Canvas, самый простой способ "захватить" содержимое Surface - просто нарисовать его дважды. Создайте растровое изображение размером с экран, создайте Canvas из растрового изображения и передайте его draw()
функция.
С рендерингом GLES вы можете использовать glReadPixels()
до замены буфера, чтобы захватить пиксели. В Grafika есть (менее дорогая, чем код в вопросе) реализация кода захвата; увидеть saveFrame()
в EglSurfaceBase.
Если бы вы отправляли видео непосредственно на Surface (через MediaPlayer), не было бы возможности захватить кадры, потому что ваше приложение никогда не имеет к ним доступа - они напрямую переходят с медиасервера на композитор (SurfaceFlinger). Однако вы можете направить входящие кадры через SurfaceTexture и дважды отобразить их из вашего приложения, один раз для отображения и один раз для захвата. Смотрите этот вопрос для получения дополнительной информации.
Один из вариантов - заменить SurfaceView на TextureView, который можно нарисовать как любую другую поверхность. Затем вы можете использовать один из getBitmap()
призывает захватить кадр. TextureView менее эффективно, чем SurfaceView, так что это не рекомендуется для всех ситуаций, но сделать это просто.
Если вы надеялись получить составной снимок экрана, содержащий как содержимое Surface, так и содержимое пользовательского интерфейса View, вам нужно будет захватить Canvas, как описано выше, захватить View с помощью обычного трюка с кэшем чертежа, а затем объединить их вручную. Обратите внимание, что это не подхватит части системы (строка состояния, панель навигации).
Обновление: в Lollipop и более поздних версиях (API 21+) вы можете использовать класс MediaProjection для захвата всего экрана виртуальным дисплеем. При таком подходе есть некоторые компромиссы, например, вы снимаете визуализированный экран, а не кадр, который был отправлен на поверхность, поэтому то, что вы получаете, может быть увеличено или уменьшено в соответствии с размером окна. Кроме того, в этом подходе используется переключатель Activity, поскольку необходимо создать намерение (путем вызова createScreenCaptureIntent для объекта ProjectionManager) и дождаться его результата.
Если вы хотите узнать больше о том, как все это работает, обратитесь к документации по графической архитектуре на уровне системы Android.
Я знаю, что это поздний ответ, но для тех, кто сталкивается с той же проблемой,
мы можем использовать PixelCopy для получения снимка.
PixelCopy.request(surfaceViewObject,BitmapDest,listener,new Handler());
где,
surfaceViewObject - это объект вида поверхности
BitmapDest - объект растрового изображения, в котором изображение будет сохранено, и оно не может быть нулевым
слушатель OnPixelCopyFinishedListener
за дополнительной информацией обращайтесь - https://developer.android.com/reference/android/view/PixelCopy
Вот полный способ сделать снимок экрана вида поверхности с помощью PixelCopy . Требуется API 24(Android N).
@RequiresApi(api = Build.VERSION_CODES.N)
private void capturePicture() {
Bitmap bmp = Bitmap.createBitmap(surfaceView.getWidth(), surfaceView.getHeight(), Bitmap.Config.ARGB_8888);
PixelCopy.request(surfaceView, bmp, i -> {
iv_Result.setImageBitmap(bmp); //"iv_Result" is the image view
}, new Handler(Looper.getMainLooper()));
}
Обновление 2020 view.setDrawingCacheEnabled(true) устарело в API 28
Если вы используете обычный просмотр, вы можете создать холст с указанным растровым изображением для рисования. Затем попросите представление нарисовать этот холст и вернуть растровое изображение, заполненное Canvas.
/**
* Copy View to Canvas and return bitMap
*/
fun getBitmapFromView(view: View): Bitmap? {
var bitmap =
Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}
Или вы можете заполнить холст цветом по умолчанию, прежде чем рисовать его с помощью вида:
/**
* Copy View to Canvas and return bitMap and fill it with default color
*/
fun getBitmapFromView(view: View, defaultColor: Int): Bitmap? {
var bitmap =
Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
var canvas = Canvas(bitmap)
canvas.drawColor(defaultColor)
view.draw(canvas)
return bitmap
}
Вышеупомянутый подход не будет работать для вида поверхности, который они будут рисовать как отверстие на снимке экрана.
Для просмотра поверхности с Android 24 вам необходимо использовать Pixel Copy.
/**
* 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()
}
}
Этот подход позволяет сделать снимок экрана любого подкласса Surface Vie, например VideoView.
Screenshot.usePixelCopy(videoView) { bitmap: Bitmap? ->
processBitMap(bitmap)
}
Это потому, что SurfaceView использует поток OpenGL для рисования и рисует непосредственно в аппаратный буфер. Вы должны использовать glReadPixels (и, возможно, GLWrapper).
Смотрите ветку: Скриншот Android OpenGL
У меня есть вопрос, если я скрою экран в другом приложении или в фоновом режиме устройства, этот код для снятия снимков в поле зрения