Skip to content

ListView under a Transform breaks for some perspective values #173104

Closed

Description

@FufferKS

Hi, I'm experiencing render issues when playing around with perspective in Flutter.

I have a "Road" like widget that is basically a ListView under perspective, but for certain matrix values it breaks.

I've raised similar issues in the past:
#155450
#154104
And got some help from @jason-simmons, so 🤞 its resolvable this time around as well.

I've provided minimal repro and a video, where it's visible for which values the ListView starts breaking (-0.003 on the device I was testing) (iPhone 16 Pro Simulator).

  • Using y scale makes the situation worse.
  • Changing the height of the Container does not seem to matter.
  • I am confident this is not device, platform or debug mode specific, as our actual app experiences this for both ios and android + release and debug.

Steps to reproduce

Create a ListView, wrap it in a Transform and setEntry(3,1, highEnoughValueLike0point005), see it disappear.

Code sample

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Perspective Issues',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _yScale = 1.0;
  double _perspective = 0.000;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Perspective Issues')),
      body: Stack(
        alignment: Alignment.bottomCenter,
        children: [
          Transform(
            transform: Matrix4.identity()
              ..setEntry(3, 1, _perspective)
              ..scale(1.0, _yScale, 1.0),
            alignment: Alignment.center,

            child: ListView.builder(
              reverse: true,
              itemBuilder: (context, index) {
                final hue = (index * 137.5) % 360;
                final color = HSLColor.fromAHSL(1.0, hue, 0.6, 0.5).toColor();
                return Container(
                  height: 100.0,
                  color: color,
                  child: Center(
                    child: Text(
                      'Index: $index',
                      style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
                    ),
                  ),
                );
              },
            ),
          ),
          _buildDebugControls(),
        ],
      ),
    );
  }

  Widget _buildDebugControls() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 12.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            _buildSlider(
              label: 'Perspective',
              value: _perspective,
              max: 0,
              min: -0.005,
              defaultValue: 0.000,
              onChanged: (value) => setState(() => _perspective = value),
            ),
            _buildSlider(
              label: 'Y Scale',
              value: _yScale,
              min: 1.0,
              max: 5.0,
              defaultValue: 1.0,
              onChanged: (value) => setState(() => _yScale = value),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSlider({
    required String label,
    required double value,
    required double min,
    required double max,
    required double defaultValue,
    required ValueChanged<double> onChanged,
  }) {
    return Column(
      children: [
        Text('$label: ${value.toStringAsFixed(4)}'),
        Row(
          children: [
            Expanded(
              child: Slider(
                value: value,
                min: min,
                max: max,
                label: value.toStringAsFixed(4),
                onChanged: onChanged,
              ),
            ),
            const SizedBox(width: 8.0),
            IconButton(icon: const Icon(Icons.refresh, size: 20), onPressed: () => onChanged(defaultValue)),
          ],
        ),
      ],
    );
  }
}

Screenshots or Video

Details
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-08-01.at.14.32.53.mp4

Logs

Logs

Nothing in the logs

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.32.6, on macOS 14.5 23F79 darwin-arm64, locale en-PL)
[!] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.3)
[✓] VS Code (version 1.102.3)
[✓] Connected device (3 available)
...
[✓] Network resources

Activity

  1. tirth-patel-nc commented on Aug 1, 2025

    @tirth-patel-nc
    Member

    Thanks for the report. Seeing the same behaviour with latest SDK versions. Reproducible on Android, iOS & macOS. Can't reproduce on Web (Chrome).

    stable : 3.32.8
    master : 3.33.0-1.0.pre-1226
    

    Android, iOS, macOS

    Screen.Recording.2025-08-01.at.6.56.02.PM.mov

    also noticed a rendering glitch (white)

    Image

    Chrome

    Screen.Recording.2025-08-01.at.6.57.34.PM.mov
  2. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    p: material_uimaterial_ui package in flutter/packages
    f: scrollingViewports, list views, slivers, etc.
    and removed
    in triagePresently being triaged by the triage team
    on Aug 1, 2025
  3. FufferKS commented on Aug 1, 2025

    @FufferKS
    ContributorAuthor

    Thanks for the quick triage 🙌

  4. added
    P2Important issues not at the top of the work list
    on Aug 4, 2025
  5. jason-simmons commented on Aug 7, 2025

    @jason-simmons
    Contributor

    This is related to Impeller's handling of perspective and clipping. It renders as expected with Skia.

    @bdero

  6. added
    e: impellerImpeller rendering backend issues and features requests
    on Aug 7, 2025
  7. FufferKS commented on Oct 16, 2025

    @FufferKS
    ContributorAuthor

    Any news on this being worked on perhaps 🙏 ?
    Quite crucial to the UI of our app.

  8. FufferKS commented on Oct 16, 2025

    @FufferKS
    ContributorAuthor

    one new finding: alignment: Alignment.center, setting this to bottomCenter seems to make it much more stable

  9. flar commented on Jan 16, 2026

    @flar
    Contributor

    Simplifying the child of the transform to just a container shows no anomalies, i.e.:

                child: Container(
                  color: Colors.blue,
                  width: 400,
                  height: 800,
                ),
    

    But adding a ClipRect summons the bug:

                child: ClipRect(
                  child: Container(
                    color: Colors.blue,
                    width: 400,
                    height: 800,
                  ),
                ),
    
  10. flar commented on Jan 16, 2026

    @flar
    Contributor

    This playground test (added to any of the impeller aiks_dl_..._unittests.cc files) also demonstrates the problem if the perspective percentage goes past 56 and the clip is set. Turning the clip off allows the full perspective range (the computed val for 0-100% ranges from 0.0 to -0.005 as per the test case submitted).

    Update: I added a checkbox for switching between intersect and diff clips and both types of clip fail at the same time. The intersect clip fails by clipping everything out suddenly, and the diff clip fails by just not carving out the center chunk as it is supposed to. But, both types of clip fail at around 56% perspective.

    Playground test with controls for perspective %, clip/diff clip/no clip
    TEST_P(AiksTest, PerspectiveRectangle) {
      int perspective = 40;
      bool use_clip = true;
      bool diff_clip = false;
    
      auto callback = [&]() -> sk_sp<DisplayList> {
        if (AiksTest::ImGuiBegin("Controls", nullptr,
                                 ImGuiWindowFlags_AlwaysAutoResize)) {
          ImGui::SliderInt("perspective%", &perspective, 0, 100);
          ImGui::Checkbox("use clip", &use_clip);
          if (use_clip) {
            ImGui::Checkbox("diff clip", &diff_clip);
          }
          ImGui::End();
        }
    
        DisplayListBuilder builder;
    
        Scalar val = perspective * -0.00005f;
        builder.TransformFullPerspective(
          1.0f,  0.0f, 0.0f, 400.0f,
          0.0f,  1.0f, 0.0f, 400.0f,
          0.0f,  0.0f, 1.0f, 0.0f,
          0.0f,  val,  0.0f, 2.2f
        );
    
        if (use_clip) {
          Rect clip = DlRect::MakeLTRB(0, 0, 400, 800);
          DlClipOp clip_op = DlClipOp::kIntersect;
          if (diff_clip) {
            clip = clip.Expand(-20);
            clip_op = DlClipOp::kDifference;
          }
          builder.ClipRect(clip, clip_op);
        }
    
        DlPaint paint;
        paint.setColor(DlColor::kBlue());
        builder.DrawRect(DlRect::MakeLTRB(0, 0, 400, 800), paint);
    
        return builder.Build();
      };
      ASSERT_TRUE(OpenPlaygroundHere(callback));
    }
    
  11. github-actions commented on Feb 9, 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.

  12. locked as resolved and limited conversation to collaborators on Feb 9, 2026
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 liste: impellerImpeller rendering backend issues and features requestsf: scrollingViewports, list views, slivers, etc.found in release: 3.32Found to occur in 3.32found in release: 3.33Found to occur in 3.33frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagesr: fixedIssue is closed as already fixed in a newer versionteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions