mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Android views using hybrid composition e2e driver test (#61507)
This commit is contained in:
parent
b7b60a2d2c
commit
293a2bf8e8
@ -0,0 +1,14 @@
|
|||||||
|
// 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_devicelab/framework/adb.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
import 'package:flutter_devicelab/tasks/integration_tests.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
deviceOperatingSystem = DeviceOperatingSystem.android;
|
||||||
|
await task(createHybridAndroidViewsIntegrationTest());
|
||||||
|
}
|
@ -60,6 +60,13 @@ TaskFunction createEmbeddedAndroidViewsIntegrationTest() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskFunction createHybridAndroidViewsIntegrationTest() {
|
||||||
|
return DriverTest(
|
||||||
|
'${flutterDirectory.path}/dev/integration_tests/hybrid_android_views',
|
||||||
|
'lib/main.dart',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
TaskFunction createAndroidSemanticsIntegrationTest() {
|
TaskFunction createAndroidSemanticsIntegrationTest() {
|
||||||
return DriverTest(
|
return DriverTest(
|
||||||
'${flutterDirectory.path}/dev/integration_tests/android_semantics_testing',
|
'${flutterDirectory.path}/dev/integration_tests/android_semantics_testing',
|
||||||
|
@ -249,6 +249,12 @@ tasks:
|
|||||||
stage: devicelab
|
stage: devicelab
|
||||||
required_agent_capabilities: ["mac/android"]
|
required_agent_capabilities: ["mac/android"]
|
||||||
|
|
||||||
|
hybrid_android_views_integration_test:
|
||||||
|
description: >
|
||||||
|
Tests hybrid Android views.
|
||||||
|
stage: devicelab
|
||||||
|
required_agent_capabilities: ["mac/android"]
|
||||||
|
|
||||||
android_semantics_integration_test:
|
android_semantics_integration_test:
|
||||||
description: >
|
description: >
|
||||||
Tests that the Android accessibility bridge produces correct semantics.
|
Tests that the Android accessibility bridge produces correct semantics.
|
||||||
|
31
dev/integration_tests/hybrid_android_views/README.md
Normal file
31
dev/integration_tests/hybrid_android_views/README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Integration test for hybrid composition on Android
|
||||||
|
|
||||||
|
This test verifies that the synthesized motion events that get to embedded
|
||||||
|
Android view are equal to the motion events that originally hit the FlutterView.
|
||||||
|
|
||||||
|
The test app's Android code listens to MotionEvents that get to FlutterView and
|
||||||
|
to an embedded Android view and sends them over a platform channel to the Dart
|
||||||
|
code where the events are matched.
|
||||||
|
|
||||||
|
This is what the app looks like:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The blue part is the embedded Android view, because it is positioned at the top
|
||||||
|
left corner, the coordinate systems for FlutterView and for the embedded view's
|
||||||
|
virtual display has the same origin (this makes the MotionEvent comparison
|
||||||
|
easier as we don't need to translate the coordinates).
|
||||||
|
|
||||||
|
The app includes the following control buttons:
|
||||||
|
* RECORD - Start listening for MotionEvents for 3 seconds, matched/unmatched events are
|
||||||
|
displayed in the listview as they arrive.
|
||||||
|
* CLEAR - Clears the events that were recorded so far.
|
||||||
|
* SAVE - Saves the events that hit FlutterView to a file.
|
||||||
|
* PLAY FILE - Send a list of events from a bundled asset file to FlutterView.
|
||||||
|
|
||||||
|
A recorded touch events sequence is bundled as an asset in the
|
||||||
|
assets_for_android_view package which lives in the goldens repository.
|
||||||
|
|
||||||
|
When running this test with `flutter drive` the record touch sequences is
|
||||||
|
replayed and the test asserts that the events that got to FlutterView are
|
||||||
|
equivalent to the ones that got to the embedded view.
|
@ -0,0 +1,58 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||||
|
if (flutterRoot == null) {
|
||||||
|
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 28
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'InvalidPackage'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId "io.flutter.integration.platformviews"
|
||||||
|
minSdkVersion 16
|
||||||
|
targetSdkVersion 28
|
||||||
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
versionName flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
@ -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. -->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="io.flutter.integration.platformviews">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name="io.flutter.app.FlutterApplication"
|
||||||
|
android:label="platform_views">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||||
|
android:value="true" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
<!-- Hybrid composition -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedded_views_preview"
|
||||||
|
android:value="true" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@ -0,0 +1,110 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package io.flutter.integration.platformviews;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
|
import io.flutter.embedding.engine.dart.DartExecutor;
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
|
import io.flutter.plugin.common.MethodCall;
|
||||||
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
|
||||||
|
public class MainActivity extends FlutterActivity implements MethodChannel.MethodCallHandler {
|
||||||
|
final static int STORAGE_PERMISSION_CODE = 1;
|
||||||
|
|
||||||
|
MethodChannel mMethodChannel;
|
||||||
|
|
||||||
|
// The method result to complete with the Android permission request result.
|
||||||
|
// This is null when not waiting for the Android permission request;
|
||||||
|
private MethodChannel.Result permissionResult;
|
||||||
|
|
||||||
|
private View getFlutterView() {
|
||||||
|
// TODO(egarciad): Set an unique ID in FlutterView, so it's easier to look it up.
|
||||||
|
ViewGroup root = (ViewGroup)findViewById(android.R.id.content);
|
||||||
|
return ((ViewGroup)root.getChildAt(0)).getChildAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureFlutterEngine(FlutterEngine flutterEngine) {
|
||||||
|
DartExecutor executor = flutterEngine.getDartExecutor();
|
||||||
|
flutterEngine
|
||||||
|
.getPlatformViewsController()
|
||||||
|
.getRegistry()
|
||||||
|
.registerViewFactory("simple_view", new SimpleViewFactory(executor));
|
||||||
|
mMethodChannel = new MethodChannel(executor, "android_views_integration");
|
||||||
|
mMethodChannel.setMethodCallHandler(this);
|
||||||
|
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
|
||||||
|
switch(methodCall.method) {
|
||||||
|
case "getStoragePermission":
|
||||||
|
if (permissionResult != null) {
|
||||||
|
result.error("error", "already waiting for permissions", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
permissionResult = result;
|
||||||
|
getExternalStoragePermissions();
|
||||||
|
return;
|
||||||
|
case "synthesizeEvent":
|
||||||
|
synthesizeEvent(methodCall, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.notImplemented();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void synthesizeEvent(MethodCall methodCall, MethodChannel.Result result) {
|
||||||
|
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
|
||||||
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
|
if (requestCode != STORAGE_PERMISSION_CODE || permissionResult == null)
|
||||||
|
return;
|
||||||
|
boolean permisisonGranted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||||
|
sendPermissionResult(permisisonGranted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getExternalStoragePermissions() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
== PackageManager.PERMISSION_GRANTED) {
|
||||||
|
sendPermissionResult(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPermissionResult(boolean result) {
|
||||||
|
if (permissionResult == null)
|
||||||
|
return;
|
||||||
|
permissionResult.success(result);
|
||||||
|
permissionResult = null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package io.flutter.integration.platformviews;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static android.view.MotionEvent.PointerCoords;
|
||||||
|
import static android.view.MotionEvent.PointerProperties;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||||
|
public class MotionEventCodec {
|
||||||
|
public static HashMap<String, Object> encode(MotionEvent event) {
|
||||||
|
ArrayList<HashMap<String,Object>> pointerProperties = new ArrayList<>();
|
||||||
|
ArrayList<HashMap<String,Object>> pointerCoords = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < event.getPointerCount(); i++) {
|
||||||
|
MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
|
||||||
|
event.getPointerProperties(i, properties);
|
||||||
|
pointerProperties.add(encodePointerProperties(properties));
|
||||||
|
|
||||||
|
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||||||
|
event.getPointerCoords(i, coords);
|
||||||
|
pointerCoords.add(encodePointerCoords(coords));
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap<String, Object> eventMap = new HashMap<>();
|
||||||
|
eventMap.put("downTime", event.getDownTime());
|
||||||
|
eventMap.put("eventTime", event.getEventTime());
|
||||||
|
eventMap.put("action", event.getAction());
|
||||||
|
eventMap.put("pointerCount", event.getPointerCount());
|
||||||
|
eventMap.put("pointerProperties", pointerProperties);
|
||||||
|
eventMap.put("pointerCoords", pointerCoords);
|
||||||
|
eventMap.put("metaState", event.getMetaState());
|
||||||
|
eventMap.put("buttonState", event.getButtonState());
|
||||||
|
eventMap.put("xPrecision", event.getXPrecision());
|
||||||
|
eventMap.put("yPrecision", event.getYPrecision());
|
||||||
|
eventMap.put("deviceId", event.getDeviceId());
|
||||||
|
eventMap.put("edgeFlags", event.getEdgeFlags());
|
||||||
|
eventMap.put("source", event.getSource());
|
||||||
|
eventMap.put("flags", event.getFlags());
|
||||||
|
|
||||||
|
return eventMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashMap<String, Object> encodePointerProperties(PointerProperties properties) {
|
||||||
|
HashMap<String, Object> map = new HashMap<>();
|
||||||
|
map.put("id", properties.id);
|
||||||
|
map.put("toolType", properties.toolType);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashMap<String, Object> encodePointerCoords(PointerCoords coords) {
|
||||||
|
HashMap<String, Object> map = new HashMap<>();
|
||||||
|
map.put("orientation", coords.orientation);
|
||||||
|
map.put("pressure", coords.pressure);
|
||||||
|
map.put("size", coords.size);
|
||||||
|
map.put("toolMajor", coords.toolMajor);
|
||||||
|
map.put("toolMinor", coords.toolMinor);
|
||||||
|
map.put("touchMajor", coords.touchMajor);
|
||||||
|
map.put("touchMinor", coords.touchMinor);
|
||||||
|
map.put("x", coords.x);
|
||||||
|
map.put("y", coords.y);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static MotionEvent decode(HashMap<String, Object> data) {
|
||||||
|
List<PointerProperties> pointerProperties = new ArrayList<>();
|
||||||
|
List<PointerCoords> pointerCoords = new ArrayList<>();
|
||||||
|
|
||||||
|
for (HashMap<String, Object> property : (List<HashMap<String, Object>>) data.get("pointerProperties")) {
|
||||||
|
pointerProperties.add(decodePointerProperties(property)) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (HashMap<String, Object> coord : (List<HashMap<String, Object>>) data.get("pointerCoords")) {
|
||||||
|
pointerCoords.add(decodePointerCoords(coord)) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MotionEvent.obtain(
|
||||||
|
(int) data.get("downTime"),
|
||||||
|
(int) data.get("eventTime"),
|
||||||
|
(int) data.get("action"),
|
||||||
|
(int) data.get("pointerCount"),
|
||||||
|
pointerProperties.toArray(new PointerProperties[pointerProperties.size()]),
|
||||||
|
pointerCoords.toArray(new PointerCoords[pointerCoords.size()]),
|
||||||
|
(int) data.get("metaState"),
|
||||||
|
(int) data.get("buttonState"),
|
||||||
|
(float) (double) data.get("xPrecision"),
|
||||||
|
(float) (double) data.get("yPrecision"),
|
||||||
|
(int) data.get("deviceId"),
|
||||||
|
(int) data.get("edgeFlags"),
|
||||||
|
(int) data.get("source"),
|
||||||
|
(int) data.get("flags")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PointerProperties decodePointerProperties(HashMap<String, Object> data) {
|
||||||
|
PointerProperties properties = new PointerProperties();
|
||||||
|
properties.id = (int) data.get("id");
|
||||||
|
properties.toolType = (int) data.get("toolType");
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PointerCoords decodePointerCoords(HashMap<String, Object> data) {
|
||||||
|
PointerCoords coords = new PointerCoords();
|
||||||
|
coords.orientation = (float) (double) data.get("orientation");
|
||||||
|
coords.pressure = (float) (double) data.get("pressure");
|
||||||
|
coords.size = (float) (double) data.get("size");
|
||||||
|
coords.toolMajor = (float) (double) data.get("toolMajor");
|
||||||
|
coords.toolMinor = (float) (double) data.get("toolMinor");
|
||||||
|
coords.touchMajor = (float) (double) data.get("touchMajor");
|
||||||
|
coords.touchMinor = (float) (double) data.get("touchMinor");
|
||||||
|
coords.x = (float) (double) data.get("x");
|
||||||
|
coords.y = (float) (double) data.get("y");
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package io.flutter.integration.platformviews;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import io.flutter.plugin.common.MethodCall;
|
||||||
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
import io.flutter.plugin.platform.PlatformView;
|
||||||
|
|
||||||
|
public class SimplePlatformView implements PlatformView, MethodChannel.MethodCallHandler {
|
||||||
|
private final FrameLayout view;
|
||||||
|
private final MethodChannel methodChannel;
|
||||||
|
private final io.flutter.integration.platformviews.TouchPipe touchPipe;
|
||||||
|
|
||||||
|
SimplePlatformView(Context context, MethodChannel methodChannel) {
|
||||||
|
this.methodChannel = methodChannel;
|
||||||
|
this.methodChannel.setMethodCallHandler(this);
|
||||||
|
|
||||||
|
view = new FrameLayout(context) {
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.setBackgroundColor(0xff0000ff);
|
||||||
|
|
||||||
|
touchPipe = new TouchPipe(this.methodChannel, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView() {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
|
||||||
|
switch(methodCall.method) {
|
||||||
|
case "pipeTouchEvents":
|
||||||
|
touchPipe.enable();
|
||||||
|
result.success(null);
|
||||||
|
return;
|
||||||
|
case "stopTouchEvents":
|
||||||
|
touchPipe.disable();
|
||||||
|
result.success(null);
|
||||||
|
return;
|
||||||
|
case "showAndHideAlertDialog":
|
||||||
|
showAndHideAlertDialog(result);
|
||||||
|
return;
|
||||||
|
case "addChildViewAndWaitForClick":
|
||||||
|
addWindow(result);
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
result.notImplemented();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showAndHideAlertDialog(MethodChannel.Result result) {
|
||||||
|
Context context = view.getContext();
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
TextView textView = new TextView(context);
|
||||||
|
textView.setText("This alert dialog will close in 1 second");
|
||||||
|
builder.setView(textView);
|
||||||
|
final AlertDialog alertDialog = builder.show();
|
||||||
|
result.success(null);
|
||||||
|
view.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
alertDialog.hide();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addWindow(final MethodChannel.Result result) {
|
||||||
|
Context context = view.getContext();
|
||||||
|
final Button button = new Button(context);
|
||||||
|
button.setText("This view was added to the Android view");
|
||||||
|
view.addView(button);
|
||||||
|
button.setOnClickListener(v -> {
|
||||||
|
view.removeView(button);
|
||||||
|
result.success(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package io.flutter.integration.platformviews;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import io.flutter.embedding.engine.dart.DartExecutor;
|
||||||
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
import io.flutter.plugin.platform.PlatformView;
|
||||||
|
import io.flutter.plugin.platform.PlatformViewFactory;
|
||||||
|
|
||||||
|
public class SimpleViewFactory extends PlatformViewFactory {
|
||||||
|
final DartExecutor executor;
|
||||||
|
|
||||||
|
public SimpleViewFactory(DartExecutor executor) {
|
||||||
|
super(null);
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlatformView create(Context context, int id, Object params) {
|
||||||
|
MethodChannel methodChannel = new MethodChannel(executor, "simple_view/" + id);
|
||||||
|
return new SimplePlatformView(context, methodChannel);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package io.flutter.integration.platformviews;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||||
|
class TouchPipe implements View.OnTouchListener {
|
||||||
|
private final MethodChannel mMethodChannel;
|
||||||
|
private final View mView;
|
||||||
|
|
||||||
|
private boolean mEnabled;
|
||||||
|
|
||||||
|
TouchPipe(MethodChannel methodChannel, View view) {
|
||||||
|
mMethodChannel = methodChannel;
|
||||||
|
mView = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enable() {
|
||||||
|
if (mEnabled)
|
||||||
|
return;
|
||||||
|
mEnabled = true;
|
||||||
|
mView.setOnTouchListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disable() {
|
||||||
|
if(!mEnabled)
|
||||||
|
return;
|
||||||
|
mEnabled = false;
|
||||||
|
mView.setOnTouchListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
mMethodChannel.invokeMethod("onTouch", MotionEventCodec.encode(event));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
android.enableR8=true
|
6
dev/integration_tests/hybrid_android_views/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
dev/integration_tests/hybrid_android_views/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Fri Jun 23 08:50:38 CEST 2017
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
@ -0,0 +1,15 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
include ':app'
|
||||||
|
|
||||||
|
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||||
|
def properties = new Properties()
|
||||||
|
|
||||||
|
assert localPropertiesFile.exists()
|
||||||
|
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||||
|
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
@ -0,0 +1,61 @@
|
|||||||
|
// 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:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class AndroidPlatformView extends StatelessWidget {
|
||||||
|
/// Creates a platform view for Android, which is rendered as a
|
||||||
|
/// native view.
|
||||||
|
/// `viewType` identifies the type of Android view to create.
|
||||||
|
const AndroidPlatformView({
|
||||||
|
Key key,
|
||||||
|
this.onPlatformViewCreated,
|
||||||
|
@required this.viewType,
|
||||||
|
}) : assert(viewType != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The unique identifier for the view type to be embedded by this widget.
|
||||||
|
///
|
||||||
|
/// A PlatformViewFactory for this type must have been registered.
|
||||||
|
final String viewType;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.platformViews.createdParam}
|
||||||
|
/// Callback to invoke after the platform view has been created.
|
||||||
|
///
|
||||||
|
/// May be null.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final PlatformViewCreatedCallback onPlatformViewCreated;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PlatformViewLink(
|
||||||
|
viewType: viewType,
|
||||||
|
surfaceFactory:
|
||||||
|
(BuildContext context, PlatformViewController controller) {
|
||||||
|
return AndroidViewSurface(
|
||||||
|
controller: controller as AndroidViewController,
|
||||||
|
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
|
||||||
|
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onCreatePlatformView: (PlatformViewCreationParams params) {
|
||||||
|
final AndroidViewController controller =
|
||||||
|
PlatformViewsService.initSurfaceAndroidView(
|
||||||
|
id: params.id,
|
||||||
|
viewType: params.viewType,
|
||||||
|
layoutDirection: TextDirection.ltr,
|
||||||
|
)
|
||||||
|
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated);
|
||||||
|
if (onPlatformViewCreated != null) {
|
||||||
|
controller.addOnPlatformViewCreatedListener(onPlatformViewCreated);
|
||||||
|
}
|
||||||
|
return controller..create();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
dev/integration_tests/hybrid_android_views/lib/main.dart
Normal file
46
dev/integration_tests/hybrid_android_views/lib/main.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// 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:flutter/material.dart';
|
||||||
|
import 'package:flutter_driver/driver_extension.dart';
|
||||||
|
|
||||||
|
import 'motion_events_page.dart';
|
||||||
|
import 'nested_view_event_page.dart';
|
||||||
|
import 'page.dart';
|
||||||
|
|
||||||
|
final List<PageWidget> _allPages = <PageWidget>[
|
||||||
|
const MotionEventsPage(),
|
||||||
|
const NestedViewEventPage(),
|
||||||
|
];
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
enableFlutterDriverExtension(handler: driverDataHandler.handleMessage);
|
||||||
|
runApp(MaterialApp(home: Home()));
|
||||||
|
}
|
||||||
|
|
||||||
|
class Home extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: ListView(
|
||||||
|
children: _allPages.map((PageWidget p) => _buildPageListTile(context, p)).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPageListTile(BuildContext context, PageWidget page) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(page.title),
|
||||||
|
key: page.tileKey,
|
||||||
|
onTap: () { _pushPage(context, page); },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pushPage(BuildContext context, PageWidget page) {
|
||||||
|
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
body: page,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,164 @@
|
|||||||
|
// 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:collection/collection.dart';
|
||||||
|
|
||||||
|
// Android MotionEvent actions for which a pointer index is encoded in the
|
||||||
|
// unmasked action code.
|
||||||
|
const List<int> kPointerActions = <int>[
|
||||||
|
0, // DOWN
|
||||||
|
1, // UP
|
||||||
|
5, // POINTER_DOWN
|
||||||
|
6, // POINTER_UP
|
||||||
|
];
|
||||||
|
|
||||||
|
const double kDoubleErrorMargin = 1e-4;
|
||||||
|
|
||||||
|
String diffMotionEvents(
|
||||||
|
Map<String, dynamic> originalEvent,
|
||||||
|
Map<String, dynamic> synthesizedEvent,
|
||||||
|
) {
|
||||||
|
final StringBuffer diff = StringBuffer();
|
||||||
|
|
||||||
|
diffMaps(originalEvent, synthesizedEvent, diff, excludeKeys: const <String>[
|
||||||
|
'pointerProperties', // Compared separately.
|
||||||
|
'pointerCoords', // Compared separately.
|
||||||
|
'source', // Unused by Flutter.
|
||||||
|
'deviceId', // Android documentation says that's an arbitrary number that shouldn't be depended on.
|
||||||
|
'action', // Compared separately.
|
||||||
|
]);
|
||||||
|
|
||||||
|
diffActions(diff, originalEvent, synthesizedEvent);
|
||||||
|
diffPointerProperties(diff, originalEvent, synthesizedEvent);
|
||||||
|
diffPointerCoordsList(diff, originalEvent, synthesizedEvent);
|
||||||
|
|
||||||
|
return diff.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void diffActions(StringBuffer diffBuffer, Map<String, dynamic> originalEvent,
|
||||||
|
Map<String, dynamic> synthesizedEvent) {
|
||||||
|
final int synthesizedActionMasked =
|
||||||
|
getActionMasked(synthesizedEvent['action'] as int);
|
||||||
|
final int originalActionMasked = getActionMasked(originalEvent['action'] as int);
|
||||||
|
final String synthesizedActionName =
|
||||||
|
getActionName(synthesizedActionMasked, synthesizedEvent['action'] as int);
|
||||||
|
final String originalActionName =
|
||||||
|
getActionName(originalActionMasked, originalEvent['action'] as int);
|
||||||
|
|
||||||
|
if (synthesizedActionMasked != originalActionMasked)
|
||||||
|
diffBuffer.write(
|
||||||
|
'action (expected: $originalActionName actual: $synthesizedActionName) ');
|
||||||
|
|
||||||
|
if (kPointerActions.contains(originalActionMasked) &&
|
||||||
|
originalActionMasked == synthesizedActionMasked) {
|
||||||
|
final int originalPointer = getPointerIdx(originalEvent['action'] as int);
|
||||||
|
final int synthesizedPointer = getPointerIdx(synthesizedEvent['action'] as int);
|
||||||
|
if (originalPointer != synthesizedPointer)
|
||||||
|
diffBuffer.write(
|
||||||
|
'pointerIdx (expected: $originalPointer actual: $synthesizedPointer action: $originalActionName ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void diffPointerProperties(StringBuffer diffBuffer,
|
||||||
|
Map<String, dynamic> originalEvent, Map<String, dynamic> synthesizedEvent) {
|
||||||
|
final List<Map<dynamic, dynamic>> expectedList =
|
||||||
|
(originalEvent['pointerProperties'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
|
||||||
|
final List<Map<dynamic, dynamic>> actualList =
|
||||||
|
(synthesizedEvent['pointerProperties'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
|
||||||
|
|
||||||
|
if (expectedList.length != actualList.length) {
|
||||||
|
diffBuffer.write(
|
||||||
|
'pointerProperties (actual length: ${actualList.length}, expected length: ${expectedList.length} ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < expectedList.length; i++) {
|
||||||
|
final Map<String, dynamic> expected =
|
||||||
|
expectedList[i].cast<String, dynamic>();
|
||||||
|
final Map<String, dynamic> actual = actualList[i].cast<String, dynamic>();
|
||||||
|
diffMaps(expected, actual, diffBuffer,
|
||||||
|
messagePrefix: '[pointerProperty $i] ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void diffPointerCoordsList(StringBuffer diffBuffer,
|
||||||
|
Map<String, dynamic> originalEvent, Map<String, dynamic> synthesizedEvent) {
|
||||||
|
final List<Map<dynamic, dynamic>> expectedList =
|
||||||
|
(originalEvent['pointerCoords'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
|
||||||
|
final List<Map<dynamic, dynamic>> actualList =
|
||||||
|
(synthesizedEvent['pointerCoords'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
|
||||||
|
|
||||||
|
if (expectedList.length != actualList.length) {
|
||||||
|
diffBuffer.write(
|
||||||
|
'pointerCoords (actual length: ${actualList.length}, expected length: ${expectedList.length} ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < expectedList.length; i++) {
|
||||||
|
final Map<String, dynamic> expected =
|
||||||
|
expectedList[i].cast<String, dynamic>();
|
||||||
|
final Map<String, dynamic> actual = actualList[i].cast<String, dynamic>();
|
||||||
|
diffPointerCoords(expected, actual, i, diffBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void diffPointerCoords(Map<String, dynamic> expected,
|
||||||
|
Map<String, dynamic> actual, int pointerIdx, StringBuffer diffBuffer) {
|
||||||
|
diffMaps(expected, actual, diffBuffer, messagePrefix: '[pointerCoord $pointerIdx] ');
|
||||||
|
}
|
||||||
|
|
||||||
|
void diffMaps(
|
||||||
|
Map<String, dynamic> expected,
|
||||||
|
Map<String, dynamic> actual,
|
||||||
|
StringBuffer diffBuffer, {
|
||||||
|
List<String> excludeKeys = const <String>[],
|
||||||
|
String messagePrefix = '',
|
||||||
|
}) {
|
||||||
|
const IterableEquality<String> eq = IterableEquality<String>();
|
||||||
|
if (!eq.equals(expected.keys, actual.keys)) {
|
||||||
|
diffBuffer.write(
|
||||||
|
'${messagePrefix}keys (expected: ${expected.keys} actual: ${actual.keys} ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final String key in expected.keys) {
|
||||||
|
if (excludeKeys.contains(key))
|
||||||
|
continue;
|
||||||
|
if (doublesApproximatelyMatch(expected[key], actual[key]))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (expected[key] != actual[key]) {
|
||||||
|
diffBuffer.write(
|
||||||
|
'$messagePrefix$key (expected: ${expected[key]} actual: ${actual[key]}) ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int getActionMasked(int action) => action & 0xff;
|
||||||
|
|
||||||
|
int getPointerIdx(int action) => (action >> 8) & 0xff;
|
||||||
|
|
||||||
|
String getActionName(int actionMasked, int action) {
|
||||||
|
const List<String> actionNames = <String>[
|
||||||
|
'DOWN',
|
||||||
|
'UP',
|
||||||
|
'MOVE',
|
||||||
|
'CANCEL',
|
||||||
|
'OUTSIDE',
|
||||||
|
'POINTER_DOWN',
|
||||||
|
'POINTER_UP',
|
||||||
|
'HOVER_MOVE',
|
||||||
|
'SCROLL',
|
||||||
|
'HOVER_ENTER',
|
||||||
|
'HOVER_EXIT',
|
||||||
|
'BUTTON_PRESS',
|
||||||
|
'BUTTON_RELEASE',
|
||||||
|
];
|
||||||
|
if (actionMasked < actionNames.length)
|
||||||
|
return '${actionNames[actionMasked]}($action)';
|
||||||
|
else
|
||||||
|
return 'ACTION_$actionMasked';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool doublesApproximatelyMatch(dynamic a, dynamic b) =>
|
||||||
|
a is double && b is double && (a - b).abs() < kDoubleErrorMargin;
|
@ -0,0 +1,305 @@
|
|||||||
|
// 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 'dart:io';
|
||||||
|
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 'motion_event_diff.dart';
|
||||||
|
import 'page.dart';
|
||||||
|
|
||||||
|
MethodChannel channel = const MethodChannel('android_views_integration');
|
||||||
|
|
||||||
|
const String kEventsFileName = 'touchEvents';
|
||||||
|
|
||||||
|
class MotionEventsPage extends PageWidget {
|
||||||
|
const MotionEventsPage()
|
||||||
|
: super('Motion Event Tests', const ValueKey<String>('MotionEventsListTile'));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MotionEventsBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
class MotionEventsBodyState extends State<MotionEventsBody> {
|
||||||
|
static const int kEventsBufferSize = 1000;
|
||||||
|
|
||||||
|
MethodChannel viewChannel;
|
||||||
|
|
||||||
|
/// The list of motion events that were passed to the FlutterView.
|
||||||
|
List<Map<String, dynamic>> flutterViewEvents = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
/// The list of motion events that were passed to the embedded view.
|
||||||
|
List<Map<String, dynamic>> embeddedViewEvents = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
height: 300.0,
|
||||||
|
child: AndroidPlatformView(
|
||||||
|
key: const ValueKey<String>('PlatformView'),
|
||||||
|
viewType: 'simple_view',
|
||||||
|
onPlatformViewCreated: onPlatformViewCreated,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemBuilder: buildEventTile,
|
||||||
|
itemCount: flutterViewEvents.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: RaisedButton(
|
||||||
|
child: const Text('RECORD'),
|
||||||
|
onPressed: listenToFlutterViewEvents,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RaisedButton(
|
||||||
|
child: const Text('CLEAR'),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
flutterViewEvents.clear();
|
||||||
|
embeddedViewEvents.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RaisedButton(
|
||||||
|
child: const Text('SAVE'),
|
||||||
|
onPressed: () {
|
||||||
|
const StandardMessageCodec codec = StandardMessageCodec();
|
||||||
|
saveRecordedEvents(
|
||||||
|
codec.encodeMessage(flutterViewEvents), context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RaisedButton(
|
||||||
|
key: const ValueKey<String>('play'),
|
||||||
|
child: const Text('PLAY FILE'),
|
||||||
|
onPressed: () { playEventsFile(); },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RaisedButton(
|
||||||
|
key: const ValueKey<String>('back'),
|
||||||
|
child: const Text('BACK'),
|
||||||
|
onPressed: () { Navigator.pop(context); },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> playEventsFile() async {
|
||||||
|
const StandardMessageCodec codec = StandardMessageCodec();
|
||||||
|
try {
|
||||||
|
final ByteData data = await rootBundle.load('packages/assets_for_android_views/assets/touchEvents');
|
||||||
|
final List<dynamic> unTypedRecordedEvents = codec.decodeMessage(data) as List<dynamic>;
|
||||||
|
final List<Map<String, dynamic>> recordedEvents = unTypedRecordedEvents
|
||||||
|
.cast<Map<dynamic, dynamic>>()
|
||||||
|
.map<Map<String, dynamic>>((Map<dynamic, dynamic> e) =>e.cast<String, dynamic>())
|
||||||
|
.toList();
|
||||||
|
await viewChannel.invokeMethod<void>('pipeTouchEvents');
|
||||||
|
print('replaying ${recordedEvents.length} motion events');
|
||||||
|
for (final Map<String, dynamic> event in recordedEvents.reversed) {
|
||||||
|
await channel.invokeMethod<void>('synthesizeEvent', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
await viewChannel.invokeMethod<void>('stopTouchEvents');
|
||||||
|
|
||||||
|
if (flutterViewEvents.length != embeddedViewEvents.length)
|
||||||
|
return 'Synthesized ${flutterViewEvents.length} events but the embedded view received ${embeddedViewEvents.length} events';
|
||||||
|
|
||||||
|
final StringBuffer diff = StringBuffer();
|
||||||
|
for (int i = 0; i < flutterViewEvents.length; ++i) {
|
||||||
|
final String currentDiff = diffMotionEvents(flutterViewEvents[i], embeddedViewEvents[i]);
|
||||||
|
if (currentDiff.isEmpty)
|
||||||
|
continue;
|
||||||
|
if (diff.isNotEmpty)
|
||||||
|
diff.write(', ');
|
||||||
|
diff.write(currentDiff);
|
||||||
|
}
|
||||||
|
return diff.toString();
|
||||||
|
} catch(e) {
|
||||||
|
return e.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
channel.setMethodCallHandler(onMethodChannelCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveRecordedEvents(ByteData data, BuildContext context) async {
|
||||||
|
if (!await channel.invokeMethod<bool>('getStoragePermission')) {
|
||||||
|
showMessage(
|
||||||
|
context, 'External storage permissions are required to save events');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final Directory outDir = await getExternalStorageDirectory();
|
||||||
|
// This test only runs on Android so we can assume path separator is '/'.
|
||||||
|
final File file = File('${outDir.path}/$kEventsFileName');
|
||||||
|
await file.writeAsBytes(data.buffer.asUint8List(0, data.lengthInBytes), flush: true);
|
||||||
|
showMessage(context, 'Saved original events to ${file.path}');
|
||||||
|
} catch (e) {
|
||||||
|
showMessage(context, 'Failed saving ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMessage(BuildContext context, String message) {
|
||||||
|
Scaffold.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPlatformViewCreated(int id) {
|
||||||
|
viewChannel = MethodChannel('simple_view/$id');
|
||||||
|
viewChannel.setMethodCallHandler(onViewMethodChannelCall);
|
||||||
|
driverDataHandler.handlerCompleter.complete(handleDriverMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void listenToFlutterViewEvents() {
|
||||||
|
viewChannel.invokeMethod<void>('pipeTouchEvents');
|
||||||
|
Timer(const Duration(seconds: 3), () {
|
||||||
|
viewChannel.invokeMethod<void>('stopTouchEvents');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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':
|
||||||
|
final Map<dynamic, dynamic> map = call.arguments as Map<dynamic, dynamic>;
|
||||||
|
flutterViewEvents.insert(0, map.cast<String, dynamic>());
|
||||||
|
if (flutterViewEvents.length > kEventsBufferSize)
|
||||||
|
flutterViewEvents.removeLast();
|
||||||
|
setState(() {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Future<dynamic>.sync(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> onViewMethodChannelCall(MethodCall call) {
|
||||||
|
switch (call.method) {
|
||||||
|
case 'onTouch':
|
||||||
|
final Map<dynamic, dynamic> map = call.arguments as Map<dynamic, dynamic>;
|
||||||
|
embeddedViewEvents.insert(0, map.cast<String, dynamic>());
|
||||||
|
if (embeddedViewEvents.length > kEventsBufferSize)
|
||||||
|
embeddedViewEvents.removeLast();
|
||||||
|
setState(() {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Future<dynamic>.sync(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildEventTile(BuildContext context, int index) {
|
||||||
|
if (embeddedViewEvents.length > index)
|
||||||
|
return TouchEventDiff(
|
||||||
|
flutterViewEvents[index], embeddedViewEvents[index]);
|
||||||
|
return Text(
|
||||||
|
'Unmatched event, action: ${flutterViewEvents[index]['action']}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TouchEventDiff extends StatelessWidget {
|
||||||
|
const TouchEventDiff(this.originalEvent, this.synthesizedEvent);
|
||||||
|
|
||||||
|
final Map<String, dynamic> originalEvent;
|
||||||
|
final Map<String, dynamic> synthesizedEvent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
Color color;
|
||||||
|
final String diff = diffMotionEvents(originalEvent, synthesizedEvent);
|
||||||
|
String msg;
|
||||||
|
final int action = synthesizedEvent['action'] as int;
|
||||||
|
final String actionName = getActionName(getActionMasked(action), action);
|
||||||
|
if (diff.isEmpty) {
|
||||||
|
color = Colors.green;
|
||||||
|
msg = 'Matched event (action $actionName)';
|
||||||
|
} else {
|
||||||
|
color = Colors.red;
|
||||||
|
msg = '[$actionName] $diff';
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: () {
|
||||||
|
print('expected:');
|
||||||
|
prettyPrintEvent(originalEvent);
|
||||||
|
print('\nactual:');
|
||||||
|
prettyPrintEvent(synthesizedEvent);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color: color,
|
||||||
|
margin: const EdgeInsets.only(bottom: 2.0),
|
||||||
|
child: Text(msg),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prettyPrintEvent(Map<String, dynamic> event) {
|
||||||
|
final StringBuffer buffer = StringBuffer();
|
||||||
|
final int action = event['action'] as int;
|
||||||
|
final int maskedAction = getActionMasked(action);
|
||||||
|
final String actionName = getActionName(maskedAction, action);
|
||||||
|
|
||||||
|
buffer.write('$actionName ');
|
||||||
|
if (maskedAction == 5 || maskedAction == 6) {
|
||||||
|
buffer.write('pointer: ${getPointerIdx(action)} ');
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Map<dynamic, dynamic>> coords = (event['pointerCoords'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
|
||||||
|
for (int i = 0; i < coords.length; i++) {
|
||||||
|
buffer.write('p$i x: ${coords[i]['x']} y: ${coords[i]['y']}, pressure: ${coords[i]['pressure']} ');
|
||||||
|
}
|
||||||
|
print(buffer.toString());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
// 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:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'android_platform_view.dart';
|
||||||
|
import 'page.dart';
|
||||||
|
|
||||||
|
class NestedViewEventPage extends PageWidget {
|
||||||
|
const NestedViewEventPage()
|
||||||
|
: super('Nested View Event Tests', const ValueKey<String>('NestedViewEventTile'));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => NestedViewEventBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
class NestedViewEventBody extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<NestedViewEventBody> createState() => NestedViewEventBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _LastTestStatus {
|
||||||
|
pending,
|
||||||
|
success,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
class NestedViewEventBodyState extends State<NestedViewEventBody> {
|
||||||
|
|
||||||
|
MethodChannel viewChannel;
|
||||||
|
_LastTestStatus lastTestStatus = _LastTestStatus.pending;
|
||||||
|
String lastError;
|
||||||
|
int id;
|
||||||
|
int nestedViewClickCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Nested view event'),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: AndroidPlatformView(
|
||||||
|
viewType: 'simple_view',
|
||||||
|
onPlatformViewCreated: onPlatformViewCreated,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (lastTestStatus != _LastTestStatus.pending) _statusWidget(),
|
||||||
|
if (viewChannel != null) ... <Widget>[
|
||||||
|
RaisedButton(
|
||||||
|
key: const ValueKey<String>('ShowAlertDialog'),
|
||||||
|
child: const Text('SHOW ALERT DIALOG'),
|
||||||
|
onPressed: onShowAlertDialogPressed,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
RaisedButton(
|
||||||
|
key: const ValueKey<String>('AddChildView'),
|
||||||
|
child: const Text('ADD CHILD VIEW'),
|
||||||
|
onPressed: onChildViewPressed,
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
key: const ValueKey<String>('TapChildView'),
|
||||||
|
child: const Text('TAP CHILD VIEW'),
|
||||||
|
onPressed: onTapChildViewPressed,
|
||||||
|
),
|
||||||
|
if (nestedViewClickCount > 0)
|
||||||
|
Text(
|
||||||
|
'Click count: $nestedViewClickCount',
|
||||||
|
key: const ValueKey<String>('NestedViewClickCount'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _statusWidget() {
|
||||||
|
assert(lastTestStatus != _LastTestStatus.pending);
|
||||||
|
final String message = lastTestStatus == _LastTestStatus.success ? 'Success' : lastError;
|
||||||
|
return Container(
|
||||||
|
color: lastTestStatus == _LastTestStatus.success ? Colors.green : Colors.red,
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
key: const ValueKey<String>('Status'),
|
||||||
|
style: TextStyle(
|
||||||
|
color: lastTestStatus == _LastTestStatus.error ? Colors.yellow : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onShowAlertDialogPressed() async {
|
||||||
|
if (lastTestStatus != _LastTestStatus.pending) {
|
||||||
|
setState(() {
|
||||||
|
lastTestStatus = _LastTestStatus.pending;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await viewChannel.invokeMethod<void>('showAndHideAlertDialog');
|
||||||
|
setState(() {
|
||||||
|
lastTestStatus = _LastTestStatus.success;
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
setState(() {
|
||||||
|
lastTestStatus = _LastTestStatus.error;
|
||||||
|
lastError = '$e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onChildViewPressed() async {
|
||||||
|
try {
|
||||||
|
await viewChannel.invokeMethod<void>('addChildViewAndWaitForClick');
|
||||||
|
setState(() {
|
||||||
|
nestedViewClickCount++;
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
setState(() {
|
||||||
|
lastTestStatus = _LastTestStatus.error;
|
||||||
|
lastError = '$e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onTapChildViewPressed() async {
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
// Dispatch a tap event on the child view inside the platform view.
|
||||||
|
//
|
||||||
|
// Android mutates `MotionEvent` instances, so in this case *do not* dispatch
|
||||||
|
// new instances as it won't cover the `MotionEventTracker` class in the embedding
|
||||||
|
// which tracks events.
|
||||||
|
//
|
||||||
|
// See the issue this prevents: https://github.com/flutter/flutter/issues/61169
|
||||||
|
await Process.run('input', const <String>['tap', '250', '550']);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPlatformViewCreated(int id) {
|
||||||
|
this.id = id;
|
||||||
|
setState(() {
|
||||||
|
viewChannel = MethodChannel('simple_view/$id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
22
dev/integration_tests/hybrid_android_views/lib/page.dart
Normal file
22
dev/integration_tests/hybrid_android_views/lib/page.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 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:flutter/material.dart';
|
||||||
|
|
||||||
|
/// The base class of all the testing pages
|
||||||
|
//
|
||||||
|
/// A testing page has to override this in order to be put as one of the items in the main page.
|
||||||
|
abstract class PageWidget extends StatelessWidget {
|
||||||
|
const PageWidget(this.title, this.tileKey);
|
||||||
|
|
||||||
|
/// The title of the testing page
|
||||||
|
///
|
||||||
|
/// It will be shown on the main page as the text on the link which opens the page.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The key of the ListTile that navigates to the page.
|
||||||
|
///
|
||||||
|
/// Used by the integration test to navigate to the corresponding page.
|
||||||
|
final ValueKey<String> tileKey;
|
||||||
|
}
|
94
dev/integration_tests/hybrid_android_views/pubspec.yaml
Normal file
94
dev/integration_tests/hybrid_android_views/pubspec.yaml
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
name: hybrid_platform_views
|
||||||
|
description: An integration test for hybrid composition on Android
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_driver:
|
||||||
|
sdk: flutter
|
||||||
|
path_provider: 1.6.11
|
||||||
|
collection: 1.15.0-nullsafety
|
||||||
|
assets_for_android_views:
|
||||||
|
git:
|
||||||
|
url: https://github.com/flutter/goldens.git
|
||||||
|
ref: c47f1308188dca65b3899228cac37f252ea8b411
|
||||||
|
path: dev/integration_tests/assets_for_android_views
|
||||||
|
|
||||||
|
archive: 2.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
args: 1.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
async: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
characters: 1.1.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
charcode: 1.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
crypto: 2.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
file: 5.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
intl: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
json_rpc_2: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
matcher: 0.12.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
meta: 1.3.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
path: 1.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
path_provider_linux: 0.0.1+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
path_provider_macos: 0.0.4+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
path_provider_platform_interface: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
platform: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
plugin_platform_interface: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
process: 3.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
pub_semver: 1.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
source_span: 1.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
stack_trace: 1.9.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
sync_http: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
term_glyph: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
typed_data: 1.3.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
vector_math: 2.1.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
web_socket_channel: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
webdriver: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
xdg_directories: 0.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
test: 1.15.2
|
||||||
|
|
||||||
|
_fe_analyzer_shared: 5.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
analyzer: 0.39.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
boolean_selector: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
clock: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
coverage: 0.14.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
fake_async: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
glob: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
http: 0.12.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
js: 0.6.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
mime: 0.9.6+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
node_interop: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
node_io: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
node_preamble: 1.4.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
package_config: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
pedantic: 1.9.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
pool: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
shelf: 0.7.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
shelf_packages_handler: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
shelf_static: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
shelf_web_socket: 0.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
source_map_stack_trace: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
source_maps: 0.10.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
string_scanner: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
test_api: 0.2.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
test_core: 0.3.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
vm_service: 4.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
watcher: 0.9.7+15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
webkit_inspection_protocol: 0.7.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
yaml: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# PUBSPEC CHECKSUM: bd09
|
@ -0,0 +1,65 @@
|
|||||||
|
// 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/flutter_driver.dart';
|
||||||
|
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
FlutterDriver driver;
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
driver = await FlutterDriver.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() {
|
||||||
|
driver.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Each test below must return back to the home page after finishing.
|
||||||
|
|
||||||
|
test('MotionEvent recomposition', () async {
|
||||||
|
final SerializableFinder motionEventsListTile =
|
||||||
|
find.byValueKey('MotionEventsListTile');
|
||||||
|
await driver.tap(motionEventsListTile);
|
||||||
|
await driver.waitFor(find.byValueKey('PlatformView'));
|
||||||
|
final String errorMessage = await driver.requestData('run test');
|
||||||
|
expect(errorMessage, '');
|
||||||
|
final SerializableFinder backButton = find.byValueKey('back');
|
||||||
|
await driver.tap(backButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Nested View Event', ()
|
||||||
|
{
|
||||||
|
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('AlertDialog from platform view context', () async {
|
||||||
|
final SerializableFinder showAlertDialog = find.byValueKey(
|
||||||
|
'ShowAlertDialog');
|
||||||
|
await driver.waitFor(showAlertDialog);
|
||||||
|
await driver.tap(showAlertDialog);
|
||||||
|
final String status = await driver.getText(find.byValueKey('Status'));
|
||||||
|
expect(status, 'Success');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Child view can handle touches', () async {
|
||||||
|
final SerializableFinder addChildView = find.byValueKey('AddChildView');
|
||||||
|
await driver.waitFor(addChildView);
|
||||||
|
await driver.tap(addChildView);
|
||||||
|
final SerializableFinder tapChildView = find.byValueKey('TapChildView');
|
||||||
|
await driver.tap(tapChildView);
|
||||||
|
final String nestedViewClickCount = await driver.getText(find.byValueKey('NestedViewClickCount'));
|
||||||
|
expect(nestedViewClickCount, 'Click count: 1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user