Skip to content

[Android] PlatformView tap coordinates mapping incorrectly when in a scroll view #146570

@frankkulak

Description

@frankkulak

Steps to reproduce

  1. Create an app with at least two tabs, using IndexedStack to switch between them.
  2. In those tabs, place a PlatformView (such as WebViewWidget from webview_flutter) in each.
  3. In one of the tabs, place the PlatformView in a fixed-height SizedBox, following a sibling fixed-height SizedBox, in a Column, in a SingleChildScrollView.
  4. Observe that taps on the PlatformView in the scroll view are mapped incorrectly:
    1. They are offset on the y axis by the height of the sibling above them in the column.
    2. When scrolling down, they are still mapping to the same place as if you hadn't scrolled at all.

Additional notes:

  • Our app had no issue with this structure on Flutter 3.13. We upgraded to 3.19, and started observing this issue on Android only.
  • We believe it is an issue with Flutter itself rather than a specific package (such as webview_flutter) because we also observe this behavior on other PlatformViews like Google Maps.

Expected results

Tapping on a PlatformView should always register the tap at the correct coordinates for where the PlatformView is currently being rendered.

Actual results

It is taking the coordinates of where the user tapped on the viewport, and using those exact coordinates in the PlatformView.

Code sample

The provided code sample is the absolute minimum we believe is required for the issue to occur: there needs to be an IndexedStack, at least two PlatformViews, and any PlatformView that doesn't fill the entire screen's height will have the issue.

Note: Forcing the PlatformView to refresh at least once will temporarily "solve" the issue. We did this by setting the key of the webview, waiting for it to load, and then changing the key one time. However, upon switching tabs and going back to the broken page, the issue comes back. To show this, we added a button to this sample that will change the key of the webview.

main.dart

Code sample
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_test/html_content.dart';

void main() {
  runApp(const ScrollIssueTestApp());
}

class ScrollIssueTestApp extends StatefulWidget {
  const ScrollIssueTestApp({super.key});

  @override
  State<ScrollIssueTestApp> createState() => _ScrollIssueTestAppState();
}

class _ScrollIssueTestAppState extends State<ScrollIssueTestApp> {
  int _selectedTab = 0;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: IndexedStack(
            index: _selectedTab,
            children: [
              const _ProblemWebViewPage(),
              _PlaceholderWebViewPage(),
            ],
          ),
        ),
        bottomNavigationBar: BottomNavigationBar(
          showUnselectedLabels: true,
          type: BottomNavigationBarType.fixed,
          currentIndex: _selectedTab,
          onTap: ((index) {
            setState(() {
              _selectedTab = index;
            });
          }),
          items: const [
            BottomNavigationBarItem(
              icon: Icon(Icons.local_fire_department_outlined),
              label: 'Problem',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.abc),
              label: 'Other',
            ),
          ],
        ),
      ),
    );
  }
}

class _ProblemWebViewPage extends StatefulWidget {
  const _ProblemWebViewPage();

  @override
  State<_ProblemWebViewPage> createState() => _ProblemWebViewPageState();
}

class _ProblemWebViewPageState extends State<_ProblemWebViewPage> {
  late final WebViewController _controller;
  static const double _webviewHeight = 850;
  String _webviewKey = 'default_key';

  @override
  void initState() {
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted).then((_) {
        _controller.loadHtmlString(problemPageContent);
      });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          SizedBox(
            height: 100,
            width: double.infinity,
            child: Center(
              child: ElevatedButton(
                onPressed: () {
                  setState(() {
                    _webviewKey = 'new_key';
                  });
                },
                child: const Text('tap to "fix" webview'),
              ),
            ),
          ),
          SizedBox(
            height: _webviewHeight,
            width: double.infinity,
            child: WebViewWidget(
              key: Key(_webviewKey),
              controller: _controller,
            ),
          ),
        ],
      ),
    );
  }
}

class _PlaceholderWebViewPage extends StatelessWidget {
  _PlaceholderWebViewPage();

  final WebViewController _controller = WebViewController()
    ..loadHtmlString(placeholderPageContent);

  @override
  Widget build(BuildContext context) {
    return WebViewWidget(controller: _controller);
  }
}

html_content.dart (Sample webview content that clearly shows the issue.)

Code sample
const problemPageContent = r'''
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Flutter Sample Problem</title>
</head>

<body>
  <div class="end-marker">
    <p>top of webview</p>
  </div>
  <div id="color-list"></div>
  <div class="end-marker">
    <p>bottom of webview</p>
  </div>
  <script>
    (() => {
      const colorList = document.getElementById("color-list");
      for (let i = 0; i < 16; ++i) {
        const colorCell = document.createElement("button");
        colorList.appendChild(colorCell);

        colorCell.innerText = `Button #${i} (Tapped: None)`;
        colorCell.classList.add("color-cell");
        colorCell.style.backgroundColor = "#" + (15 - i).toString(16).repeat(6);
        colorCell.style.color = i > 7 ? "white" : "black";
        colorCell.onclick = () => {
          const buttons = document.querySelectorAll("button");
          buttons.forEach((button, j) => {
            button.innerText = `Button #${j} (Tapped: #${i})`
          });
        };
      }
    })();
  </script>
</body>
<style>
  body,
  p {
    margin: 0;
  }

  .end-marker {
    width: 100%;
    height: 25px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #a9f1fb;
  }

  .color-cell {
    width: 100%;
    height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
  }
</style>

</html>
''';

const placeholderPageContent = r'''
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Placeholder</title>
</head>

<body>
  <h1>Placeholder Webview</h1>
</body>

</html>
''';

Screenshots or Video

Screenshots / Video demonstration FlutterIssue
FlutterWebviewIssue.mov

Logs

For logs, see file:

FlutterLogs.txt

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):

[✓] Flutter (Channel stable, 3.19.5, on macOS 13.6.4 22G513 darwin-x64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.2)
[✓] VS Code (version 1.81.1)
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

Metadata

Metadata

Labels

P2Important issues not at the top of the work lista: platform-viewsEmbedding Android/iOS views in Flutter appsc: regressionIt was better in the past than it is nowf: scrollingViewports, list views, slivers, etc.found in release: 3.19Found to occur in 3.19found in release: 3.22Found to occur in 3.22frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionteam-androidOwned by Android platform teamtriaged-androidTriaged by Android platform team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions