Skip to content

[pointer_interceptor] Add iOS implementation and platform interface #5233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 57 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e10c9d8
initial working commit
LouiseHsu Oct 25, 2023
6d3f6ca
fixed compiler error, move testing factory to runner, remove default …
LouiseHsu Oct 25, 2023
f16a10b
clean up, remove dependency on ios version
LouiseHsu Oct 30, 2023
09fa93f
remove extraneous files in platform interface
LouiseHsu Oct 30, 2023
f04c392
unused exports
LouiseHsu Oct 30, 2023
ee38fa7
Merge branch 'main' into pointer-interceptor-ios
LouiseHsu Oct 30, 2023
6c610b9
update versioning + changelog
LouiseHsu Oct 30, 2023
45a02df
Merge branch 'pointer-interceptor-ios' of github.com:LouiseHsu/packag…
LouiseHsu Oct 30, 2023
736b4b1
fix yaml issue
LouiseHsu Oct 31, 2023
badba67
fix example
LouiseHsu Oct 31, 2023
8c979d2
fix imports
LouiseHsu Oct 31, 2023
7af12bb
some more clean up, formatting
LouiseHsu Oct 31, 2023
276461c
add publish_to none
LouiseHsu Oct 31, 2023
bda6870
remove extra analysis_options
LouiseHsu Oct 31, 2023
29a3cc1
update platform interface pubspec.yaml
LouiseHsu Oct 31, 2023
78aa1e3
add public member comments, renaming
LouiseHsu Oct 31, 2023
b9bc419
remove extraneous analysis files
LouiseHsu Oct 31, 2023
f968902
remove extra test files
LouiseHsu Nov 1, 2023
f9294ab
more linter stuff
LouiseHsu Nov 1, 2023
3854eb9
lower versioning
LouiseHsu Nov 1, 2023
ef7f76f
lower versioning for platform interface
LouiseHsu Nov 1, 2023
819d7d4
version lowering
LouiseHsu Nov 1, 2023
6067505
fixing ios plugin overriding default ios behaviour
LouiseHsu Nov 2, 2023
994a8ae
remove old ios implementation
LouiseHsu Nov 3, 2023
68a4d8c
fix tests
LouiseHsu Nov 7, 2023
2bcdde2
update ios test
LouiseHsu Nov 7, 2023
fbf0e04
formatting
LouiseHsu Nov 7, 2023
85f0899
Merge branch 'main' into pointer-interceptor-ios
LouiseHsu Nov 7, 2023
6c18fa9
pr comments, update changelog/readme
LouiseHsu Nov 8, 2023
e6a8a09
remove empty test dir in example
LouiseHsu Nov 8, 2023
a67da96
rename web.dart to pointer_interceptor_web.dart, remove "intercepting…
LouiseHsu Nov 8, 2023
f38a019
update podspec
LouiseHsu Nov 9, 2023
800fba2
update READMEs, CHANGELOGs, remove extraneous files, various clean up…
LouiseHsu Nov 10, 2023
b73c75c
move pointer_interceptor_ios into part of pointer_interceptor
LouiseHsu Nov 13, 2023
b309e31
remove accidental prints
LouiseHsu Nov 13, 2023
4edc671
add publish to none
LouiseHsu Nov 13, 2023
60ec17b
add ios configuration to example package
LouiseHsu Nov 13, 2023
7092edf
fix licensing and podfile
LouiseHsu Nov 13, 2023
196a9dd
add licensing to swift files
LouiseHsu Nov 13, 2023
d5bf4c9
podfile
LouiseHsu Nov 13, 2023
75a19a8
refactor web into own package, move integration tests, rewrite exampl…
LouiseHsu Nov 14, 2023
b53e4f8
licensing, formatting
LouiseHsu Nov 14, 2023
afb90bd
fix integration test
LouiseHsu Nov 14, 2023
7b4d1be
formatting
LouiseHsu Nov 15, 2023
fa36d02
fix pbxproj
LouiseHsu Nov 15, 2023
d40abd2
fix pbxproj?
LouiseHsu Nov 15, 2023
340bd5c
pbxproj
LouiseHsu Nov 15, 2023
a766eb8
fix ios platform tests, add unit tests
LouiseHsu Nov 15, 2023
1e5c612
pbxproj
LouiseHsu Nov 15, 2023
d8a7fd2
add placeholder integration tests
LouiseHsu Nov 16, 2023
39c0439
formatting
LouiseHsu Nov 16, 2023
0d099f4
update README
LouiseHsu Nov 16, 2023
99bfefa
Merge branch 'main' into pointer-interceptor-ios
LouiseHsu Nov 16, 2023
e8f3445
PR comments
LouiseHsu Nov 16, 2023
836cdb9
Merge branch 'pointer-interceptor-ios' of github.com:LouiseHsu/packag…
LouiseHsu Nov 16, 2023
ba3fd60
fix test
LouiseHsu Nov 16, 2023
a4df634
some pr comments
LouiseHsu Nov 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.10.0

* Adds iOS implementation.

## 0.9.3+7

* Updates metadata to point to new source folder
Expand Down
13 changes: 7 additions & 6 deletions packages/pointer_interceptor/pointer_interceptor/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
# pointer_interceptor

`PointerInterceptor` is a widget that prevents mouse events (in web) from being captured by an underlying [`HtmlElementView`](https://api.flutter.dev/flutter/widgets/HtmlElementView-class.html).
| | iOS | Web |
|-------------|---------|-----|
| **Support** | iOS 11+ | Any |

You can use this widget in a cross-platform app freely. In mobile, where the issue that this plugin fixes does not exist, the widget acts as a pass-through of its `children`, without adding anything to the render tree.
`PointerInterceptor` is a widget that prevents mouse events from being captured by an underlying [`HtmlElementView`](https://api.flutter.dev/flutter/widgets/HtmlElementView-class.html) in web, or an underlying [`PlatformView`](https://api.flutter.dev/flutter/widgets/PlatformViewLink-class.html) on iOS.

## What is the problem?

When overlaying Flutter widgets on top of `HtmlElementView` widgets that respond to mouse gestures (handle clicks, for example), the clicks will be consumed by the `HtmlElementView`, and not relayed to Flutter.
When overlaying Flutter widgets on top of `HtmlElementView`/`PlatformView` widgets that respond to mouse gestures (handle clicks, for example), the clicks will be consumed by the `HtmlElementView`/`PlatformView`, and not relayed to Flutter.

The result is that Flutter widget's `onTap` (and other) handlers won't fire as expected, but they'll affect the underlying webview.
The result is that Flutter widget's `onTap` (and other) handlers won't fire as expected, but they'll affect the underlying native platform view.

|The problem...|
|:-:|
|![Depiction of problematic areas](https://github.com/raw/flutter/packages/main/packages/pointer_interceptor/doc/img/affected-areas.png)|
|_In the dashed areas, mouse events won't work as expected. The `HtmlElementView` will consume them before Flutter sees them._|


## How does this work?

`PointerInterceptor` creates a platform view consisting of an empty HTML element. The element has the size of its `child` widget, and is inserted in the layer tree _behind_ its child in paint order.
In web, `PointerInterceptor` creates a platform view consisting of an empty HTML element, while on iOS it creates an empty `UIView` instead. The element has the size of its `child` widget, and is inserted in the layer tree _behind_ its child in paint order.

This empty platform view doesn't do anything with mouse events, other than preventing them from reaching other platform views underneath it.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
/build/

# Web related
lib/generated_plugin_registrant.dart

# Symbolication related
app.*.symbols
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,27 @@
# This file should be version controlled and should not be manually edited.

version:
revision: e6bd95bc5caa5e34c5b0285a559673984374b7ea
channel: master
revision: "969911d1d09d6c4f145e9ce27c08093e8c285561"
channel: "main"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 969911d1d09d6c4f145e9ce27c08093e8c285561
base_revision: 969911d1d09d6c4f145e9ce27c08093e8c285561
- platform: ios
create_revision: 969911d1d09d6c4f145e9ce27c08093e8c285561
base_revision: 969911d1d09d6c4f145e9ce27c08093e8c285561

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
Original file line number Diff line number Diff line change
Expand Up @@ -4,176 +4,13 @@

// ignore_for_file: avoid_print

import 'dart:html' as html;

// Imports the Flutter Driver API.
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'package:pointer_interceptor_example/main.dart' as app;

final Finder nonClickableButtonFinder =
find.byKey(const Key('transparent-button'));
final Finder clickableWrappedButtonFinder =
find.byKey(const Key('wrapped-transparent-button'));
final Finder clickableButtonFinder = find.byKey(const Key('clickable-button'));
final Finder backgroundFinder =
find.byKey(const ValueKey<String>('background-widget'));

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('Without semantics', () {
testWidgets(
'on wrapped elements, the browser does not hit the background-html-view',
(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

final html.Element element =
_getHtmlElementAtCenter(clickableButtonFinder, tester);

expect(element.id, isNot('background-html-view'));
}, semanticsEnabled: false);

testWidgets(
'on wrapped elements with intercepting set to false, the browser hits the background-html-view',
(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

final html.Element element =
_getHtmlElementAtCenter(clickableWrappedButtonFinder, tester);

expect(element.id, 'background-html-view');
}, semanticsEnabled: false);

testWidgets(
'on unwrapped elements, the browser hits the background-html-view',
(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

final html.Element element =
_getHtmlElementAtCenter(nonClickableButtonFinder, tester);

expect(element.id, 'background-html-view');
}, semanticsEnabled: false);

testWidgets('on background directly', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

final html.Element element =
_getHtmlElementAt(tester.getTopLeft(backgroundFinder));

expect(element.id, 'background-html-view');
}, semanticsEnabled: false);
});

group('With semantics', () {
testWidgets('finds semantics of wrapped widgets',
(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

if (!_newSemanticsAvailable()) {
print('Skipping test: Needs flutter > 2.10');
return;
}

final html.Element element =
_getHtmlElementAtCenter(clickableButtonFinder, tester);

expect(element.tagName.toLowerCase(), 'flt-semantics');
expect(element.getAttribute('aria-label'), 'Works As Expected');
});

testWidgets(
'finds semantics of wrapped widgets with intercepting set to false',
(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

if (!_newSemanticsAvailable()) {
print('Skipping test: Needs flutter > 2.10');
return;
}

final html.Element element =
_getHtmlElementAtCenter(clickableWrappedButtonFinder, tester);

expect(element.tagName.toLowerCase(), 'flt-semantics');
expect(element.getAttribute('aria-label'),
'Never calls onPressed transparent');
});

testWidgets('finds semantics of unwrapped elements',
(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

if (!_newSemanticsAvailable()) {
print('Skipping test: Needs flutter > 2.10');
return;
}

final html.Element element =
_getHtmlElementAtCenter(nonClickableButtonFinder, tester);

expect(element.tagName.toLowerCase(), 'flt-semantics');
expect(element.getAttribute('aria-label'), 'Never calls onPressed');
});

// Notice that, when hit-testing the background platform view, instead of
// finding a semantics node, the platform view itself is found. This is
// because the platform view does not add interactive semantics nodes into
// the framework's semantics tree. Instead, its semantics is determined by
// the HTML content of the platform view itself. Flutter's semantics tree
// simply allows the hit test to land on the platform view by making itself
// hit test transparent.
testWidgets('on background directly', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

final html.Element element =
_getHtmlElementAt(tester.getTopLeft(backgroundFinder));

expect(element.id, 'background-html-view');
});
});
}

// Calls [_getHtmlElementAt] passing it the center of the widget identified by
// the `finder`.
html.Element _getHtmlElementAtCenter(Finder finder, WidgetTester tester) {
final Offset point = tester.getCenter(finder);
return _getHtmlElementAt(point);
}

// Locates the DOM element at the given `point` using `elementFromPoint`.
//
// `elementFromPoint` is an approximate proxy for a hit test, although it's
// sensitive to the presence of shadow roots and browser quirks (not all
// browsers agree on what it should return in all situations). Since this test
// runs only in Chromium, it relies on Chromium's behavior.
html.Element _getHtmlElementAt(Offset point) {
// Probe at the shadow so the browser reports semantics nodes in addition to
// platform view elements. If probed from `html.document` the browser hides
// the contents of <flt-glass-name> as an implementation detail.
final html.ShadowRoot glassPaneShadow =
html.document.querySelector('flt-glass-pane')!.shadowRoot!;
return glassPaneShadow.elementFromPoint(point.dx.toInt(), point.dy.toInt())!;
}

// TODO(dit): Remove this after flutter master (2.13) lands into stable.
// This detects that we can do new semantics assertions by looking at the 'id'
// attribute on flt-semantics elements (it is now set in 2.13 and up).
bool _newSemanticsAvailable() {
final html.ShadowRoot glassPaneShadow =
html.document.querySelector('flt-glass-pane')!.shadowRoot!;
final List<html.Element> elements =
glassPaneShadow.querySelectorAll('flt-semantics[id]');
return elements.isNotEmpty;
// TODO(louisehsu): given the difficulty of making the same integration tests
// work for both web and ios implementations, please find tests in their respective
// platform implementation packages.
testWidgets('placeholder test', (WidgetTester tester) async {});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*

# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}

def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end

File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
Loading