mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add a virtual-display (VD) platform view test, and refactor tests a bit. (#161349)
Towards https://github.com/flutter/flutter/issues/161261. Still need to add a HC (Hybrid Composition) variant, but figured I'd do this incrementally to make it easier to review.
This commit is contained in:
parent
99fe90a05e
commit
89b336109f
@ -12,6 +12,7 @@ import androidx.core.view.WindowInsetsCompat
|
|||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import com.example.android_engine_test.extensions.NativeDriverSupportPlugin
|
import com.example.android_engine_test.extensions.NativeDriverSupportPlugin
|
||||||
import com.example.android_engine_test.fixtures.BlueOrangeGradientPlatformViewFactory
|
import com.example.android_engine_test.fixtures.BlueOrangeGradientPlatformViewFactory
|
||||||
|
import com.example.android_engine_test.fixtures.BlueOrangeGradientSurfaceViewPlatformViewFactory
|
||||||
import com.example.android_engine_test.fixtures.ChangingColorButtonPlatformViewFactory
|
import com.example.android_engine_test.fixtures.ChangingColorButtonPlatformViewFactory
|
||||||
import com.example.android_engine_test.fixtures.OtherFaceTexturePlugin
|
import com.example.android_engine_test.fixtures.OtherFaceTexturePlugin
|
||||||
import com.example.android_engine_test.fixtures.SmileyFaceTexturePlugin
|
import com.example.android_engine_test.fixtures.SmileyFaceTexturePlugin
|
||||||
@ -35,6 +36,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
.registry
|
.registry
|
||||||
.apply {
|
.apply {
|
||||||
registerViewFactory("blue_orange_gradient_platform_view", BlueOrangeGradientPlatformViewFactory())
|
registerViewFactory("blue_orange_gradient_platform_view", BlueOrangeGradientPlatformViewFactory())
|
||||||
|
registerViewFactory("blue_orange_gradient_surface_view_platform_view", BlueOrangeGradientSurfaceViewPlatformViewFactory())
|
||||||
registerViewFactory("changing_color_button_platform_view", ChangingColorButtonPlatformViewFactory())
|
registerViewFactory("changing_color_button_platform_view", ChangingColorButtonPlatformViewFactory())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
package com.example.android_engine_test.extensions
|
package com.example.android_engine_test.extensions
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.os.Build
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import io.flutter.Log
|
import io.flutter.Log
|
||||||
@ -44,6 +45,10 @@ class NativeDriverSupportPlugin :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
|
"sdk_version" -> {
|
||||||
|
val versionMap = mapOf("version" to Build.VERSION.SDK_INT)
|
||||||
|
result.success(versionMap)
|
||||||
|
}
|
||||||
"ping" -> {
|
"ping" -> {
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
@file:Suppress("PackageName")
|
||||||
|
|
||||||
|
package com.example.android_engine_test.fixtures
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.LinearGradient
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Shader
|
||||||
|
import android.view.SurfaceHolder
|
||||||
|
import android.view.SurfaceView
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import io.flutter.plugin.platform.PlatformView
|
||||||
|
import io.flutter.plugin.platform.PlatformViewFactory
|
||||||
|
|
||||||
|
class BlueOrangeGradientSurfaceViewPlatformViewFactory : PlatformViewFactory(null) {
|
||||||
|
override fun create(
|
||||||
|
context: Context,
|
||||||
|
viewId: Int,
|
||||||
|
args: Any?
|
||||||
|
): PlatformView = GradientSurfaceViewPlatformView(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GradientSurfaceViewPlatformView(
|
||||||
|
context: Context
|
||||||
|
) : SurfaceView(context),
|
||||||
|
PlatformView,
|
||||||
|
SurfaceHolder.Callback {
|
||||||
|
val paint = Paint()
|
||||||
|
|
||||||
|
init {
|
||||||
|
layoutParams =
|
||||||
|
ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
holder.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getView(): View = this
|
||||||
|
|
||||||
|
override fun dispose() {}
|
||||||
|
|
||||||
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
|
val canvas = holder.lockCanvas()
|
||||||
|
if (canvas != null) {
|
||||||
|
drawGradient(canvas)
|
||||||
|
holder.unlockCanvasAndPost(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceChanged(
|
||||||
|
holder: SurfaceHolder,
|
||||||
|
format: Int,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
) {
|
||||||
|
val canvas = holder.lockCanvas()
|
||||||
|
if (canvas != null) {
|
||||||
|
drawGradient(canvas)
|
||||||
|
holder.unlockCanvasAndPost(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceDestroyed(holder: SurfaceHolder) {}
|
||||||
|
|
||||||
|
private fun drawGradient(canvas: Canvas) {
|
||||||
|
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(
|
||||||
|
w: Int,
|
||||||
|
h: Int,
|
||||||
|
oldw: Int,
|
||||||
|
oldh: Int
|
||||||
|
) {
|
||||||
|
paint.shader =
|
||||||
|
LinearGradient(
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
w.toFloat(),
|
||||||
|
h.toFloat(),
|
||||||
|
intArrayOf(
|
||||||
|
Color.rgb(0x41, 0x69, 0xE1),
|
||||||
|
Color.rgb(0xFF, 0xA5, 0x00)
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
)
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,7 @@ Future<int> _fetchTexture(int width, int height) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ensureAndroidOrIosDevice();
|
ensureAndroidDevice();
|
||||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||||
|
|
||||||
// Run on full screen.
|
// Run on full screen.
|
||||||
|
@ -10,7 +10,7 @@ import 'package:flutter_driver/driver_extension.dart';
|
|||||||
import 'src/allow_list_devices.dart';
|
import 'src/allow_list_devices.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
ensureAndroidOrIosDevice();
|
ensureAndroidDevice();
|
||||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||||
|
|
||||||
// Run on full screen.
|
// Run on full screen.
|
||||||
|
@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_driver/driver_extension.dart';
|
import 'package:flutter_driver/driver_extension.dart';
|
||||||
|
|
||||||
import 'src/allow_list_devices.dart';
|
import '../src/allow_list_devices.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ensureAndroidOrIosDevice();
|
ensureAndroidDevice();
|
||||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||||
|
|
||||||
// Run on full screen.
|
// Run on full screen.
|
||||||
@ -25,6 +25,11 @@ final class MainApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const MaterialApp(
|
return const MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
// It is assumed:
|
||||||
|
// - The Android SDK version is >= 23 (the test driver checks)
|
||||||
|
// - This view does NOT use a SurfaceView
|
||||||
|
//
|
||||||
|
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||||
home: AndroidView(viewType: 'blue_orange_gradient_platform_view'),
|
home: AndroidView(viewType: 'blue_orange_gradient_platform_view'),
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:android_driver_extensions/extension.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_driver/driver_extension.dart';
|
||||||
|
|
||||||
|
import '../src/allow_list_devices.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
ensureAndroidDevice();
|
||||||
|
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||||
|
|
||||||
|
// Run on full screen.
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
runApp(const MainApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MainApp extends StatelessWidget {
|
||||||
|
const MainApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
// It is assumed:
|
||||||
|
// - The Android SDK version is >= 23 (the test driver checks)
|
||||||
|
// - This view DOES use a SurfaceView
|
||||||
|
//
|
||||||
|
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||||
|
home: AndroidView(viewType: 'blue_orange_gradient_surface_view_platform_view'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ import 'package:flutter_driver/driver_extension.dart';
|
|||||||
import 'src/allow_list_devices.dart';
|
import 'src/allow_list_devices.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ensureAndroidOrIosDevice();
|
ensureAndroidDevice();
|
||||||
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
|
||||||
|
|
||||||
// Run on full screen.
|
// Run on full screen.
|
||||||
|
@ -6,11 +6,11 @@ import 'dart:io' as io;
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Throws an [UnsupportedError] if the current platform is not Android or iOS.
|
/// Throws an [UnsupportedError] if the current platform is not Android.
|
||||||
void ensureAndroidOrIosDevice() {
|
void ensureAndroidDevice() {
|
||||||
if (kIsWeb || (!io.Platform.isAndroid && !io.Platform.isIOS)) {
|
if (kIsWeb || !io.Platform.isAndroid) {
|
||||||
throw UnsupportedError(
|
throw UnsupportedError(
|
||||||
'This app should only run on Android or iOS devices. It uses native '
|
'This app should only run on Android devices. It uses native Android '
|
||||||
'plugins that are not developed for other platforms, and would need to '
|
'plugins that are not developed for other platforms, and would need to '
|
||||||
'be adapted to run on other platforms. See the README.md for details.',
|
'be adapted to run on other platforms. See the README.md for details.',
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:android_driver_extensions/native_driver.dart';
|
||||||
|
import 'package:android_driver_extensions/skia_gold.dart';
|
||||||
|
import 'package:flutter_driver/flutter_driver.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../_luci_skia_gold_prelude.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
// To test the golden file generation locally, comment out the following line.
|
||||||
|
// autoUpdateGoldenFiles = true;
|
||||||
|
|
||||||
|
const String goldenPrefix = 'texture_layer_hybrid_composition_platform_view';
|
||||||
|
|
||||||
|
late final FlutterDriver flutterDriver;
|
||||||
|
late final NativeDriver nativeDriver;
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
if (isLuci) {
|
||||||
|
await enableSkiaGoldComparator();
|
||||||
|
}
|
||||||
|
flutterDriver = await FlutterDriver.connect();
|
||||||
|
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
|
||||||
|
await nativeDriver.configureForScreenshotTesting();
|
||||||
|
await flutterDriver.waitUntilFirstFrameRasterized();
|
||||||
|
|
||||||
|
// Double check that we are really probably testing using TLHC.
|
||||||
|
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||||
|
if (await nativeDriver.sdkVersion case final int version when version < 23) {
|
||||||
|
fail('Requires SDK >= 23, got $version');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await nativeDriver.close();
|
||||||
|
await flutterDriver.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should screenshot and match a blue -> orange gradient', () async {
|
||||||
|
await flutterDriver.waitFor(find.byType('AndroidView'));
|
||||||
|
await expectLater(
|
||||||
|
nativeDriver.screenshot(),
|
||||||
|
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portrait.android.png'),
|
||||||
|
);
|
||||||
|
}, timeout: Timeout.none);
|
||||||
|
|
||||||
|
test('should rotate landscape and screenshot the gradient', () async {
|
||||||
|
await flutterDriver.waitFor(find.byType('AndroidView'));
|
||||||
|
await nativeDriver.rotateToLandscape();
|
||||||
|
await expectLater(
|
||||||
|
nativeDriver.screenshot(),
|
||||||
|
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_landscape_rotated.android.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await nativeDriver.rotateResetDefault();
|
||||||
|
await expectLater(
|
||||||
|
nativeDriver.screenshot(),
|
||||||
|
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portait_rotated_back.android.png'),
|
||||||
|
);
|
||||||
|
}, timeout: Timeout.none);
|
||||||
|
}
|
@ -7,12 +7,14 @@ import 'package:android_driver_extensions/skia_gold.dart';
|
|||||||
import 'package:flutter_driver/flutter_driver.dart';
|
import 'package:flutter_driver/flutter_driver.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '_luci_skia_gold_prelude.dart';
|
import '../_luci_skia_gold_prelude.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
// To test the golden file generation locally, comment out the following line.
|
// To test the golden file generation locally, comment out the following line.
|
||||||
// autoUpdateGoldenFiles = true;
|
// autoUpdateGoldenFiles = true;
|
||||||
|
|
||||||
|
const String goldenPrefix = 'virtual_display_platform_view';
|
||||||
|
|
||||||
late final FlutterDriver flutterDriver;
|
late final FlutterDriver flutterDriver;
|
||||||
late final NativeDriver nativeDriver;
|
late final NativeDriver nativeDriver;
|
||||||
|
|
||||||
@ -24,6 +26,12 @@ void main() async {
|
|||||||
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
|
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
|
||||||
await nativeDriver.configureForScreenshotTesting();
|
await nativeDriver.configureForScreenshotTesting();
|
||||||
await flutterDriver.waitUntilFirstFrameRasterized();
|
await flutterDriver.waitUntilFirstFrameRasterized();
|
||||||
|
|
||||||
|
// Double check that we are really probably testing using Virtual Display.
|
||||||
|
// See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md.
|
||||||
|
if (await nativeDriver.sdkVersion case final int version when version < 23) {
|
||||||
|
fail('Requires SDK >= 23, got $version');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() async {
|
tearDownAll(() async {
|
||||||
@ -35,7 +43,7 @@ void main() async {
|
|||||||
await flutterDriver.waitFor(find.byType('AndroidView'));
|
await flutterDriver.waitFor(find.byType('AndroidView'));
|
||||||
await expectLater(
|
await expectLater(
|
||||||
nativeDriver.screenshot(),
|
nativeDriver.screenshot(),
|
||||||
matchesGoldenFile('platform_view_blue_orange_gradient_portrait.android.png'),
|
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portrait.android.png'),
|
||||||
);
|
);
|
||||||
}, timeout: Timeout.none);
|
}, timeout: Timeout.none);
|
||||||
|
|
||||||
@ -44,13 +52,13 @@ void main() async {
|
|||||||
await nativeDriver.rotateToLandscape();
|
await nativeDriver.rotateToLandscape();
|
||||||
await expectLater(
|
await expectLater(
|
||||||
nativeDriver.screenshot(),
|
nativeDriver.screenshot(),
|
||||||
matchesGoldenFile('platform_view_blue_orange_gradient_landscape.android.png'),
|
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_landscape_rotated.android.png'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await nativeDriver.rotateResetDefault();
|
await nativeDriver.rotateResetDefault();
|
||||||
await expectLater(
|
await expectLater(
|
||||||
nativeDriver.screenshot(),
|
nativeDriver.screenshot(),
|
||||||
matchesGoldenFile('platform_view_blue_orange_gradient_portrait_post_rotation.android.png'),
|
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portait_rotated_back.android.png'),
|
||||||
);
|
);
|
||||||
}, timeout: Timeout.none);
|
}, timeout: Timeout.none);
|
||||||
}
|
}
|
@ -49,10 +49,14 @@ final class NativeDriverCommandExtension implements CommandExtension {
|
|||||||
if (result == null) {
|
if (result == null) {
|
||||||
return const _MethodChannelResult(<String, Object?>{});
|
return const _MethodChannelResult(<String, Object?>{});
|
||||||
}
|
}
|
||||||
if (result is! Map<String, Object?>) {
|
if (result is! Map<Object?, Object?>) {
|
||||||
throw ArgumentError.value(result, 'result', 'Expected a Map<String, Object?>');
|
throw ArgumentError.value(
|
||||||
|
result,
|
||||||
|
'result',
|
||||||
|
'Expected a Map<String, Object?>, got ${result.runtimeType}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return _MethodChannelResult(result);
|
return _MethodChannelResult(result.cast());
|
||||||
}
|
}
|
||||||
|
|
||||||
// While these could have been implemented in native code, they are already
|
// While these could have been implemented in native code, they are already
|
||||||
|
@ -71,6 +71,12 @@ final class AndroidNativeDriver implements NativeDriver {
|
|||||||
await _driver.sendCommand(NativeCommand.tap(finder));
|
await _driver.sendCommand(NativeCommand.tap(finder));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> get sdkVersion async {
|
||||||
|
final Map<String, Object?> result = await _driver.sendCommand(NativeCommand.getSdkVersion);
|
||||||
|
return result['version']! as int;
|
||||||
|
}
|
||||||
|
|
||||||
/// Waits for 2 seconds before completing.
|
/// Waits for 2 seconds before completing.
|
||||||
///
|
///
|
||||||
/// There is no perfect way, outside of polling, to know when the device is
|
/// There is no perfect way, outside of polling, to know when the device is
|
||||||
|
@ -27,6 +27,9 @@ final class NativeCommand extends Command {
|
|||||||
/// Pings the device to ensure it is responsive.
|
/// Pings the device to ensure it is responsive.
|
||||||
static const NativeCommand ping = NativeCommand('ping');
|
static const NativeCommand ping = NativeCommand('ping');
|
||||||
|
|
||||||
|
/// Gets the SDK version code.
|
||||||
|
static const NativeCommand getSdkVersion = NativeCommand('sdk_version');
|
||||||
|
|
||||||
/// The method to call on the plugin.
|
/// The method to call on the plugin.
|
||||||
final String method;
|
final String method;
|
||||||
|
|
||||||
|
@ -45,6 +45,9 @@ abstract interface class NativeDriver {
|
|||||||
/// ```
|
/// ```
|
||||||
Future<Duration> ping();
|
Future<Duration> ping();
|
||||||
|
|
||||||
|
/// Returns the SDK version.
|
||||||
|
Future<int> get sdkVersion;
|
||||||
|
|
||||||
/// Take a screenshot using a platform-specific mechanism.
|
/// Take a screenshot using a platform-specific mechanism.
|
||||||
///
|
///
|
||||||
/// The image is returned as an opaque handle that can be used to retrieve
|
/// The image is returned as an opaque handle that can be used to retrieve
|
||||||
|
Loading…
Reference in New Issue
Block a user