Skip to content

[Android] Platform view multi-touch: pointer ids and coordinates are mis-paired when a released Android pointer id is reused #191105

Closed

Description

@wizardlyc

Steps to reproduce

Minimal project attached (a bare PlatformViewLink + AndroidViewSurface over a plain Android View that only logs and draws the MotionEvents it receives — no WebView, no plugins).

On a physical Android device:

  1. Press and hold on the left half, moving in circles.
  2. With that finger still down, press and hold on the right half.
  3. Release the left finger only.
  4. Press the left half again.

Expected results

Each pointer keeps reporting its own finger's position. Pointer ids and coordinates stay paired.

Actual results

From step 4 on, the MotionEvents delivered to the platform view are corrupted:

  • one pointer reports coordinates pixel-identical to the other finger (two real fingers cannot occupy the same pixel);
  • the other pointer reports 2 * P_other - P_real — a point reflection through the other pointer — which frequently lands outside the view bounds;
  • the framework also emits a pointer-up for a finger that is still touching the screen.

Single touch is unaffected.

Code sample

Two files. Nothing else is needed — no plugins, no dependencies beyond a default
flutter create project.

The platform view is a plain Android View that only logs the MotionEvents it
receives and flags two anomalies computed from the raw event data:

  • two pointers reporting identical integer coordinates — two real fingers cannot
    occupy the same pixel;
  • a pointer whose coordinates fall outside the view bounds.

Steps

  1. flutter run on a physical Android device (multi-touch required).
  2. Press and hold on the left half, moving in circles.
  3. With that finger still down, press and hold on the right half.
  4. Release the left finger only.
  5. Press the left half again.
  6. adb logcat -s MTRepro

Expected: pointer ids stay paired with their own finger's coordinates.
Actual: ★ lines appear from step 5 on.


android/app/src/main/kotlin/<...>/MainActivity.kt

package com.example.multitouch_repro   // <-- match your package

import android.content.Context
import android.util.Log
import android.view.MotionEvent
import android.view.View
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        flutterEngine.platformViewsController.registry
            .registerViewFactory("touch_logger", TouchLoggerFactory())
    }
}

class TouchLoggerFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, viewId: Int, args: Any?) =
        object : PlatformView {
            private val view = TouchLoggerView(context)
            override fun getView(): View = view
            override fun dispose() {}
        }
}

class TouchLoggerView(context: Context) : View(context) {
    override fun onTouchEvent(event: MotionEvent): Boolean {
        val sb = StringBuilder()
        val seen = HashMap<String, Int>()
        var problem: String? = null

        for (i in 0 until event.pointerCount) {
            val id = event.getPointerId(i)
            val x = event.getX(i)
            val y = event.getY(i)
            sb.append(" [i=$i id=$id ${x.toInt()},${y.toInt()}]")

            val key = "${x.toInt()},${y.toInt()}"
            val prev = seen.put(key, id)
            if (prev != null && prev != id) {
                problem = "id=$prev and id=$id report identical coordinates ($key)"
            }
            if (x < -2 || y < -2 || x > width + 2 || y > height + 2) {
                problem = "id=$id at ${x.toInt()},${y.toInt()} is outside the view (${width}x$height)"
            }
        }

        val name = when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> "DOWN"
            MotionEvent.ACTION_UP -> "UP"
            MotionEvent.ACTION_POINTER_DOWN -> "POINTER_DOWN(idx=${event.actionIndex})"
            MotionEvent.ACTION_POINTER_UP -> "POINTER_UP(idx=${event.actionIndex})"
            MotionEvent.ACTION_MOVE -> "MOVE"
            MotionEvent.ACTION_CANCEL -> "CANCEL"
            else -> "OTHER"
        }
        Log.i("MTRepro", "$name count=${event.pointerCount}$sb")
        problem?.let { Log.e("MTRepro", "  ★ $it") }
        return true
    }
}

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MaterialApp(home: Scaffold(body: TouchLogger())));

class TouchLogger extends StatelessWidget {
  const TouchLogger({super.key});

  @override
  Widget build(BuildContext context) {
    return PlatformViewLink(
      viewType: 'touch_logger',
      surfaceFactory: (context, controller) => AndroidViewSurface(
        controller: controller as AndroidViewController,
        hitTestBehavior: PlatformViewHitTestBehavior.opaque,
        gestureRecognizers: const {},
      ),
      onCreatePlatformView: (params) =>
          PlatformViewsService.initExpensiveAndroidView(
            id: params.id,
            viewType: 'touch_logger',
            layoutDirection: TextDirection.ltr,
          )
            ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
            ..create(),
    );
  }
}

initExpensiveAndroidView (Hybrid Composition) is shown because that is what
flutter_inappwebview uses by default. initSurfaceAndroidView (TLHC/VD) and a
plain AndroidView reproduce the same way; so does
initHybridAndroidView (HCPP) with
io.flutter.embedding.android.EnableSurfaceControl enabled on Android 13+.
Adding an EagerGestureRecognizer makes no difference.

Evidence

Instrumented at three points — the framework's raw PointerEvents, a 1:1 replica of _AndroidMotionEventConverter showing what the framework should be sending, and the MotionEvent actually received by the Android view.

At the moment of corruption (device density 2.625; logical → physical):

[SYNTH] POINTER_DOWN(idx=1) count=2 [id=1 347,403] [id=0 188,360]            <- framework side, correct
MTRepro POINTER_DOWN(idx=0 id=0) count=2 [i=0 id=0 912,1057] [i=1 id=1 1331,1169]   <- as received, corrupted
  • i=0 id=0 reports 912,1057, which is 347,403 × 2.625 — the coordinates belonging to id=1.
  • i=1 id=1 reports 1331,1169, which is 2 × P(id=1) - P(id=0):
    2×912 − 188×2.625 = 1330.5 (observed 1331), 2×1057 − 360×2.625 = 1169 (observed 1169).

The reflection holds across 30+ samples to within 1px, e.g. P(id=0) = 60,323 → predicted 2×912 − 157.5 = 1666.5, observed 1666.

A control comparison from the same session, before the id is reused, is correct:

[SYNTH] POINTER_DOWN(idx=1) count=2 [id=0 66,378] [id=1 347,403]
MTRepro POINTER_DOWN(idx=1 id=1) count=2 [i=0 id=0 172,992] [i=1 id=1 912,1057]

(66,378 × 2.625 = 173,992 ≈ 172,992; 347,403 × 2.625 = 911,1058 ≈ 912,1057.)

Analysis

The framework builds pointerProperties and pointerCoords from pointerPositions.keys
(packages/flutter/lib/src/services/platform_views.dart, _AndroidMotionEventConverter.toAndroidMotionEvent), which is a LinkedHashMap in insertion order.

That order matches ascending Android pointer id order until a released id is reused: _remove() returns the id to usedAndroidPointerIds, and the next handlePointerDownEvent() assigns the lowest free id to a newly pressed pointer. After the sequence above, insertion order is [B, A] — ids [1, 0].

Engine side, PlatformViewsController.toMotionEvent() pops the original event from motionEventTracker and translates its coordinates rather than assigning them per pointer. The translation delta is taken positionally from the framework arrays, so a mismatched order produces a delta of P_other − P_real applied to every pointer — yielding exactly the two symptoms above.

So the two sides disagree about what the array order means: the framework emits insertion order, the engine reads it as ascending-id order.

Validation

Three-way A/B in a single build, switchable at runtime, dispatching to the same platform view:

Dispatch Result
Framework (dispatchPointerEvent) reproduces
Manual via AndroidViewController.sendMotionEvent, same ordering as the framework reproduces
Manual, arrays sorted by Android pointer id does not reproduce; dragging feels normal

The middle row is the control: it establishes that the manual channel is equivalent to the framework's, so the third row's result is attributable to the ordering change alone.

Proposed fix

Sort the pointer list by Android pointer id so both parallel arrays and the derived action index match what the engine assumes (patch attached, flutter-fix.patch):

final List<int> pointers = pointerPositions.keys.toList()
  ..sort((int a, int b) => pointerProperties[a]!.id.compareTo(pointerProperties[b]!.id));

This makes the caller satisfy the engine's expectation. It does not remove the engine's positional assumption; a complementary engine-side fix would pair coordinates by pointer id instead of by array position. Happy to send either as a PR.

One thing worth a maintainer's eye: pointerIdx is also used by the batching guard
isSinglePointerAction(event) && pointerIdx < numPointers - 1, so sorting changes which pointer counts as "last". No dropped moves were observed in testing, but that testing was two-finger and manual — 3+ finger behaviour has not been stress-tested, and this converter has had multi-touch trouble there before (#69431).

Related

Previously reported as #157463 (closed while still labelled "in triage" / "waiting for response", no linked fix). That reporter traced it to translateMotionEvent and reported that Flutter 3.22.0 behaves correctly, i.e. this is a regression — we have not independently verified the 3.22.0 claim.

Real-world impact: an app embedding third-party HTML5/Godot games in a WebView. Every game using an on-screen joystick together with an action button is broken on Android; the same page in mobile Chrome behaves correctly.

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

[!] Flutter (Channel [user-branch], 3.47.0, on macOS 26.5.2 25F84 darwin-arm64, locale zh-Hans-CN)
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 4cf2416426 (3 days ago), 2026-08-11 11:53:49 -0700
    • Engine revision 5f77625673
    • Dart version 3.13.0
    • DevTools version 2.60.0

[!] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
    • Android SDK at ~/Library/Android/sdk
    • Platform android-36, build-tools 35.0.0
    • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72)

[✓] Xcode - develop for iOS and macOS (Xcode 26.5)
[✓] Connected device (6 available)
    • 23113RKC6C (mobile) • android-arm64 • Android 16 (API 36)

Reproduced on a physical Android device (MIUI), minSdkVersion 24 / targetSdk 35 / compileSdk 36,
in both initExpensiveAndroidView (HC) and initSurfaceAndroidView (TLHC/VD) modes, and in
initHybridAndroidView (HCPP) with io.flutter.embedding.android.EnableSurfaceControl enabled.
Adding an EagerGestureRecognizer makes no difference.

Activity

  1. saurabh-mirajkar commented on Aug 14, 2026

    @saurabh-mirajkar
    Contributor

    @wizardlyc Thanks for the detailed report. I see no code sample in the report, and the provided flutter doctor output is from Flutter 3.35.7 which is quite old. Could you please verify the attached reproduction on the latest stable Flutter 3.47.0 and, if it still reproduces, provide the minimal reproduction/code sample used for the test?

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    needs repro infoAutomated crash report whose cause isn't yet known
    and removed
    in triagePresently being triaged by the triage team
    on Aug 14, 2026
  3. wizardlyc commented on Aug 14, 2026

    @wizardlyc
    Author

    @wizardlyc Thanks for the detailed report. I see no code sample in the report, and the provided flutter doctor output is from Flutter 3.35.7 which is quite old. Could you please verify the attached reproduction on the latest stable Flutter 3.47.0 and, if it still reproduces, provide the minimal reproduction/code sample used for the test?

    Code added,I test on 3.47.0, the bug is still there

  4. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Aug 14, 2026
  5. saurabh-mirajkar commented on Aug 14, 2026

    @saurabh-mirajkar
    Contributor

    @wizardlyc Thanks for adding the code sample and confirming that this is still reproducible on latest stable Flutter 3.47.0.

    I was able to reproduce the issue on the latest stable as well, using a Pixel 6 (Android 17) with the provided code sample.

    Additionally, I found a related older report #96640, which similarly involves incorrect multi-touch gestures being delivered to Android Platform Views.

    Keeping this issue open for the team to evaluate further.

  6. added
    platform-androidAndroid applications specifically
    frameworkflutter/packages/flutter repository. See also f: labels.
    f: gesturesflutter/packages/flutter/gestures repository.
    a: platform-viewsEmbedding Android/iOS views in Flutter apps
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-androidOwned by Android platform team
    and removed
    needs repro infoAutomated crash report whose cause isn't yet known
    on Aug 14, 2026
  7. added
    P2Important issues not at the top of the work list
    triaged-androidTriaged by Android platform team
    on Aug 18, 2026
  8. added a commit that references this issue on Sep 30, 2026
    b9db4f9
  9. github-actions commented on Oct 14, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  10. locked as resolved and limited conversation to collaborators on Oct 14, 2026
  11. added a commit that references this issue on Oct 29, 2026
    38d3fec
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: platform-viewsEmbedding Android/iOS views in Flutter appsf: gesturesflutter/packages/flutter/gestures repository.found in release: 3.47Found to occur in 3.47frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyteam-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