Test dynamic surface switch (#61918)

This commit is contained in:
Emmanuel Garcia 2020-07-22 20:00:07 -07:00 committed by GitHub
parent b8df8a8368
commit ddb8e6e3bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 43 deletions

View file

@ -12,9 +12,14 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import java.lang.StringBuilder;
import java.util.HashMap;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.android.FlutterImageView;
import io.flutter.embedding.android.FlutterSurfaceView;
import io.flutter.embedding.android.FlutterTextureView;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodCall;
@ -36,6 +41,63 @@ public class MainActivity extends FlutterActivity implements MethodChannel.Metho
return ((ViewGroup)root.getChildAt(0)).getChildAt(0);
}
private String getViewName(View view) {
if (view instanceof FlutterImageView) {
return "FlutterImageView";
}
if (view instanceof FlutterSurfaceView) {
return "FlutterSurfaceView";
}
if (view instanceof FlutterTextureView) {
return "FlutterTextureView";
}
if (view instanceof FlutterView) {
return "FlutterView";
}
if (view instanceof ViewGroup) {
return "ViewGroup";
}
return "View";
}
private void recurseViewHierarchy(View current, String padding, StringBuilder builder) {
if (current.getVisibility() != View.VISIBLE || current.getAlpha() == 0) {
return;
}
String name = getViewName(current);
builder.append(padding);
builder.append("|-");
builder.append(name);
builder.append("\n");
if (current instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) current;
for (int index = 0; index < viewGroup.getChildCount(); index++) {
recurseViewHierarchy(viewGroup.getChildAt(index), padding + " ", builder);
}
}
}
/**
* Serializes the view hierarchy, so it can be sent to Dart over the method channel.
*
* Notation:
* |- <view-name>
* |- ... child view ordered by z order.
*
* Example output:
* |- FlutterView
* |- FlutterImageView
* |- ViewGroup
* |- View
*/
private String getSerializedViewHierarchy() {
View root = getFlutterView();
StringBuilder builder = new StringBuilder();
recurseViewHierarchy(root, "", builder);
return builder.toString();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -65,19 +127,23 @@ public class MainActivity extends FlutterActivity implements MethodChannel.Metho
getExternalStoragePermissions();
return;
case "synthesizeEvent":
synthesizeEvent(methodCall, result);
synthesizeEvent(methodCall);
result.success(null);
return;
case "getViewHierarchy":
String viewHierarchy = getSerializedViewHierarchy();
result.success(viewHierarchy);
return;
}
result.notImplemented();
}
@SuppressWarnings("unchecked")
public void synthesizeEvent(MethodCall methodCall, MethodChannel.Result result) {
public void synthesizeEvent(MethodCall methodCall) {
MotionEvent event = MotionEventCodec.decode((HashMap<String, Object>) methodCall.arguments());
getFlutterView().dispatchTouchEvent(event);
// TODO(egarciad): This can be cleaned up.
mMethodChannel.invokeMethod("onTouch", MotionEventCodec.encode(event));
result.success(null);
}
@Override

View file

@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
MethodChannel channel = const MethodChannel('android_views_integration');
class AndroidPlatformView extends StatelessWidget {
/// Creates a platform view for Android, which is rendered as a
/// native view.

View file

@ -0,0 +1,35 @@
// 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 'dart:async';
import 'package:flutter_driver/driver_extension.dart';
typedef DriverHandler = Future<String> Function();
/// Wraps a flutter driver [DataHandler] with one that waits until a delegate is set.
///
/// This allows the driver test to call [FlutterDriver.requestData] before the handler was
/// set by the app in which case the requestData call will only complete once the app is ready
/// for it.
class FutureDataHandler {
final Map<String, Completer<DriverHandler>> _handlers = <String, Completer<DriverHandler>>{};
/// Registers a lazy handler that will be invoked on the next message from the driver.
Completer<DriverHandler> registerHandler(String key) {
_handlers[key] = Completer<DriverHandler>();
return _handlers[key];
}
Future<String> handleMessage(String message) async {
if (_handlers[message] == null) {
return 'Unsupported driver message: $message.\n'
'Supported messages are: ${_handlers.keys}.';
}
final DriverHandler handler = await _handlers[message].future;
return handler();
}
}
FutureDataHandler driverDataHandler = FutureDataHandler();

View file

@ -5,6 +5,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'future_data_handler.dart';
import 'motion_events_page.dart';
import 'nested_view_event_page.dart';
import 'page.dart';

View file

@ -8,15 +8,13 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:path_provider/path_provider.dart';
import 'android_platform_view.dart';
import 'future_data_handler.dart';
import 'motion_event_diff.dart';
import 'page.dart';
MethodChannel channel = const MethodChannel('android_views_integration');
const String kEventsFileName = 'touchEvents';
class MotionEventsPage extends PageWidget {
@ -29,22 +27,6 @@ class MotionEventsPage extends PageWidget {
}
}
/// Wraps a flutter driver [DataHandler] with one that waits until a delegate is set.
///
/// This allows the driver test to call [FlutterDriver.requestData] before the handler was
/// set by the app in which case the requestData call will only complete once the app is ready
/// for it.
class FutureDataHandler {
final Completer<DataHandler> handlerCompleter = Completer<DataHandler>();
Future<String> handleMessage(String message) async {
final DataHandler handler = await handlerCompleter.future;
return handler(message);
}
}
FutureDataHandler driverDataHandler = FutureDataHandler();
class MotionEventsBody extends StatefulWidget {
@override
State createState() => MotionEventsBodyState();
@ -196,7 +178,7 @@ class MotionEventsBodyState extends State<MotionEventsBody> {
void onPlatformViewCreated(int id) {
viewChannel = MethodChannel('simple_view/$id');
viewChannel.setMethodCallHandler(onViewMethodChannelCall);
driverDataHandler.handlerCompleter.complete(handleDriverMessage);
driverDataHandler.registerHandler('run test').complete(playEventsFile);
}
void listenToFlutterViewEvents() {
@ -206,14 +188,6 @@ class MotionEventsBodyState extends State<MotionEventsBody> {
});
}
Future<String> handleDriverMessage(String message) async {
switch (message) {
case 'run test':
return playEventsFile();
}
return 'unknown message: "$message"';
}
Future<dynamic> onMethodChannelCall(MethodCall call) {
switch (call.method) {
case 'onTouch':

View file

@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'android_platform_view.dart';
import 'future_data_handler.dart';
import 'page.dart';
class NestedViewEventPage extends PageWidget {
@ -38,6 +39,7 @@ class NestedViewEventBodyState extends State<NestedViewEventBody> {
String lastError;
int id;
int nestedViewClickCount = 0;
bool showPlatformView = true;
@override
Widget build(BuildContext context) {
@ -49,10 +51,12 @@ class NestedViewEventBodyState extends State<NestedViewEventBody> {
children: <Widget>[
SizedBox(
height: 300,
child: AndroidPlatformView(
viewType: 'simple_view',
onPlatformViewCreated: onPlatformViewCreated,
),
child: showPlatformView ?
AndroidPlatformView(
key: const ValueKey<String>('PlatformView'),
viewType: 'simple_view',
onPlatformViewCreated: onPlatformViewCreated,
) : null,
),
if (lastTestStatus != _LastTestStatus.pending) _statusWidget(),
if (viewChannel != null) ... <Widget>[
@ -61,6 +65,11 @@ class NestedViewEventBodyState extends State<NestedViewEventBody> {
child: const Text('SHOW ALERT DIALOG'),
onPressed: onShowAlertDialogPressed,
),
RaisedButton(
key: const ValueKey<String>('TogglePlatformView'),
child: const Text('TOGGLE PLATFORM VIEW'),
onPressed: onTogglePlatformView,
),
Row(
children: <Widget>[
RaisedButton(
@ -120,6 +129,12 @@ class NestedViewEventBodyState extends State<NestedViewEventBody> {
}
}
Future<void> onTogglePlatformView() async {
setState(() {
showPlatformView = !showPlatformView;
});
}
Future<void> onChildViewPressed() async {
try {
await viewChannel.invokeMethod<void>('addChildViewAndWaitForClick');
@ -152,5 +167,7 @@ class NestedViewEventBodyState extends State<NestedViewEventBody> {
setState(() {
viewChannel = MethodChannel('simple_view/$id');
});
driverDataHandler.registerHandler('hierarchy')
.complete(() => channel.invokeMethod<String>('getViewHierarchy'));
}
}

View file

@ -18,10 +18,8 @@ Future<void> main() async {
});
// Each test below must return back to the home page after finishing.
test('MotionEvent recomposition', () async {
final SerializableFinder motionEventsListTile =
find.byValueKey('MotionEventsListTile');
final SerializableFinder motionEventsListTile = find.byValueKey('MotionEventsListTile');
await driver.tap(motionEventsListTile);
await driver.waitFor(find.byValueKey('PlatformView'));
final String errorMessage = await driver.requestData('run test');
@ -30,8 +28,7 @@ Future<void> main() async {
await driver.tap(backButton);
});
group('Nested View Event', ()
{
group('Nested View Event', () {
setUpAll(() async {
final SerializableFinder wmListTile =
find.byValueKey('NestedViewEventTile');
@ -44,8 +41,7 @@ Future<void> main() async {
});
test('AlertDialog from platform view context', () async {
final SerializableFinder showAlertDialog = find.byValueKey(
'ShowAlertDialog');
final SerializableFinder showAlertDialog = find.byValueKey('ShowAlertDialog');
await driver.waitFor(showAlertDialog);
await driver.tap(showAlertDialog);
final String status = await driver.getText(find.byValueKey('Status'));
@ -58,8 +54,60 @@ Future<void> main() async {
await driver.tap(addChildView);
final SerializableFinder tapChildView = find.byValueKey('TapChildView');
await driver.tap(tapChildView);
final String nestedViewClickCount = await driver.getText(find.byValueKey('NestedViewClickCount'));
final String nestedViewClickCount =
await driver.getText(find.byValueKey('NestedViewClickCount'));
expect(nestedViewClickCount, 'Click count: 1');
});
});
group('Flutter surface switch', () {
setUpAll(() async {
final SerializableFinder wmListTile = find.byValueKey('NestedViewEventTile');
await driver.tap(wmListTile);
});
tearDownAll(() async {
await driver.waitFor(find.pageBack());
await driver.tap(find.pageBack());
});
test('Uses FlutterImageView when Android view is on the screen', () async {
await driver.waitFor(find.byValueKey('PlatformView'));
expect(
await driver.requestData('hierarchy'),
'|-FlutterView\n'
' |-FlutterSurfaceView\n' // Flutter UI (hidden)
' |-FlutterImageView\n' // Flutter UI (background surface)
' |-ViewGroup\n' // Platform View
' |-ViewGroup\n'
' |-FlutterImageView\n' // Flutter UI (overlay surface)
);
// Hide platform view.
final SerializableFinder togglePlatformView = find.byValueKey('TogglePlatformView');
await driver.tap(togglePlatformView);
await driver.waitForAbsent(find.byValueKey('PlatformView'));
expect(
await driver.requestData('hierarchy'),
'|-FlutterView\n'
' |-FlutterSurfaceView\n' // Just the Flutter UI
);
// Show platform view again.
await driver.tap(togglePlatformView);
await driver.waitFor(find.byValueKey('PlatformView'));
expect(
await driver.requestData('hierarchy'),
'|-FlutterView\n'
' |-FlutterSurfaceView\n' // Flutter UI (hidden)
' |-FlutterImageView\n' // Flutter UI (background surface)
' |-ViewGroup\n' // Platform View
' |-ViewGroup\n'
' |-FlutterImageView\n' // Flutter UI (overlay surface)
);
});
});
}