Repository: flutter
Issue: Calling localToGlobal for a RenderBox inside ListView with padding yields wrong and inconsistent results
Labels: framework,f: scrolling,c: rendering,has reproducible steps,P2,found in release: 2.8,found in release: 2.9,team-framework,triaged-framework

## Steps to Reproduce

<!-- Please include full steps to reproduce so that we can reproduce the problem. -->

1. Execute `flutter run` on the code sample
2. Recognize that the vertical padding of the `ListView` is 56 and that the sticky header should also respect the padding and stick at a top offset of 56.
3. Scroll down then scroll up multiple times with different speeds and watch the sticky header glitching.
4. Remove padding from `ListView` and notice that the glitch disappears.
5. Uncomment code below TODO at line 71 to see that the sticky header implementation works flawlessly with a second header.

**Expected results:**
`localToGlobal` `dy` should always be 0.0 when scrolled to top.

**Actual results:** <!-- what did you see? -->
`localToGlobal` `dy` is never 0.0 when scrolled to top, and it differs from scroll to scroll how close to 0.0 it gets.
This causes the sticky header to glitch.

See the video and example console logs for clarification:

https://user-images.githubusercontent.com/26022902/148198873-b9e260ba-2daa-46eb-84e0-1686ffd6bcf9.mov

```
flutter: -252.0
flutter: -254.0
flutter: -255.0
flutter: -252.0
flutter: -228.0
flutter: -119.0
flutter: -56.0
```

```
flutter: -115.0
flutter: -114.0
flutter: -103.0
flutter: -78.0
flutter: -56.0
flutter: -44.0
flutter: -6.0
```

```
flutter: -137.0
flutter: -113.0
flutter: -79.0
flutter: -56.0
flutter: -43.0
flutter: -19.0
```

<details>
<summary>Code sample</summary>

```dart
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO: Set padding to zero and see that it does not glitch anymore.
    const padding = EdgeInsets.symmetric(vertical: 56);
    return MaterialApp(
      home: Scaffold(
        body: ListView(
          padding: padding,
          children: [
            StickyHeader(
              stickOffset: padding.top,
              header: Container(height: 56, color: Colors.orange.withOpacity(0.5)),
              content: Column(
                children: const [
                  SizedBox(height: 200, child: Placeholder()),
                  SizedBox(height: 200, child: Placeholder()),
                  SizedBox(height: 200, child: Placeholder()),
                  SizedBox(height: 200, child: Placeholder()),
                ],
              ),
            ),
            // TODO: Uncomment this to see that the sticky header effect works correctly.
            /*
            StickyHeader(
              stickOffset: padding.top,
              header: Container(height: 56, color: Colors.orange.withOpacity(0.5)),
              content: Column(
                children: const [
                  SizedBox(height: 200, child: Placeholder()),
                  SizedBox(height: 200, child: Placeholder()),
                  SizedBox(height: 200, child: Placeholder()),
                  SizedBox(height: 200, child: Placeholder()),
                ],
              ),
            )*/
          ],
        ),
      ),
    );
  }
}

class StickyHeader extends MultiChildRenderObjectWidget {
  final Widget header;
  final Widget content;
  final double stickOffset;

  StickyHeader({
    Key? key,
    required this.header,
    required this.content,
    this.stickOffset = 0,
  }) : super(
          key: key,
          children: [content, header],
        );

  @override
  RenderStickyHeader createRenderObject(BuildContext context) {
    final scrollPosition = Scrollable.of(context)!.position;
    return RenderStickyHeader(stickOffset: stickOffset, scrollPosition: scrollPosition);
  }

  @override
  void updateRenderObject(BuildContext context, RenderStickyHeader renderObject) {
    final scrollPosition = Scrollable.of(context)!.position;
    renderObject
      ..scrollPosition = scrollPosition
      ..stickOffset = stickOffset;
  }
}

class RenderStickyHeader extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
  double _stickOffset;
  ScrollPosition _scrollPosition;

  RenderStickyHeader({
    required ScrollPosition scrollPosition,
    double stickOffset = 0.0,
    RenderBox? header,
    RenderBox? content,
  })  : _stickOffset = stickOffset,
        _scrollPosition = scrollPosition {
    if (content != null) add(content);
    if (header != null) add(header);
  }

  set stickOffset(double value) {
    if (value == _stickOffset) {
      return;
    }
    _stickOffset = value;
    markNeedsLayout();
  }

  set scrollPosition(ScrollPosition newValue) {
    if (_scrollPosition == newValue) {
      return;
    }
    final ScrollPosition oldValue = _scrollPosition;
    _scrollPosition = newValue;
    markNeedsLayout();
    if (attached) {
      oldValue.removeListener(markNeedsLayout);
      newValue.addListener(markNeedsLayout);
    }
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _scrollPosition.addListener(markNeedsLayout);
  }

  @override
  void detach() {
    _scrollPosition.removeListener(markNeedsLayout);
    super.detach();
  }

  // short-hand to access the child RenderObjects
  RenderBox get _headerBox => lastChild!;

  RenderBox get _contentBox => firstChild!;

  @override
  void performLayout() {
    // layout both header and content widget
    final childConstraints = constraints.loosen();
    _headerBox.layout(childConstraints, parentUsesSize: true);
    _contentBox.layout(childConstraints, parentUsesSize: true);

    final headerHeight = _headerBox.size.height;
    final contentHeight = _contentBox.size.height;

    // determine size of ourselves based on content widget
    final width = max(constraints.minWidth, _contentBox.size.width);
    final height = max(constraints.minHeight, headerHeight + contentHeight);
    size = Size(width, height);

    // place content underneath header
    final contentParentData = _contentBox.parentData! as MultiChildLayoutParentData;
    contentParentData.offset = Offset(0.0, headerHeight);

    final double headerOffset = determineHeaderOffset(_stickOffset);
    final double maxOffset = height - headerHeight;

    // TODO: Scroll down then scroll up multiple times.
    // The value varies and it never reaches 0.0 as expected.
    print("$headerOffset");

    final headerParentData = _headerBox.parentData! as MultiChildLayoutParentData;
    headerParentData.offset = Offset(0.0, max(0, min(-headerOffset, maxOffset)));
  }

  double determineHeaderOffset(double padding) {
    final scrollBox = _scrollPosition.context.notificationContext!.findRenderObject();
    if (scrollBox?.attached ?? false) {
      try {
        return localToGlobal(Offset(0, -padding), ancestor: scrollBox).dy;
      } catch (e) {
        // ignore and fall-through and return 0.0
      }
    }
    return 0.0;
  }

  @override
  void setupParentData(RenderObject child) {
    super.setupParentData(child);
    if (child.parentData is! MultiChildLayoutParentData) {
      child.parentData = MultiChildLayoutParentData();
    }
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    return _contentBox.getMinIntrinsicWidth(height);
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    return _contentBox.getMaxIntrinsicWidth(height);
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    return _headerBox.getMinIntrinsicHeight(width) + _contentBox.getMinIntrinsicHeight(width);
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    return _headerBox.getMaxIntrinsicHeight(width) + _contentBox.getMaxIntrinsicHeight(width);
  }

  @override
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

  @override
  bool hitTestChildren(HitTestResult result, {required Offset position}) {
    return defaultHitTestChildren(result as BoxHitTestResult, position: position);
  }

  @override
  bool get isRepaintBoundary => true;

  @override
  void paint(PaintingContext context, Offset offset) {
    context.pushClipRect(false, offset, Offset.zero & size, defaultPaint);
  }
}
```

</details>

<details>
  <summary>Logs</summary>

<!-- Finally, paste the output of running `flutter doctor -v` here. -->

```
[✓] Flutter (Channel stable, 2.8.1, on macOS 11.6.1 20G224 darwin-x64, locale en-DE)
    • Flutter version 2.8.1 at /Users/tobias/Documents/Dev/Tools/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 77d935af4d (3 weeks ago), 2021-12-16 08:37:33 -0800
    • Engine revision 890a5fca2e
    • Dart version 2.15.1

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/tobias/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • Java binary at: /Users/tobias/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/203.7784292/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 3.6)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)

[✓] Android Studio (version 4.2)
    • Android Studio at /Users/tobias/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/202.7486908/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)

[✓] Android Studio (version 2020.3)
    • Android Studio at /Users/tobias/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/203.7784292/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] IntelliJ IDEA Ultimate Edition (version 2021.2.3)
    • IntelliJ at /Users/tobias/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-1/212.5457.46/IntelliJ IDEA.app
    • Flutter plugin version 62.0.3
    • Dart plugin version 212.5632

[✓] VS Code (version 1.63.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension can be installed from:
      🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-x64     • macOS 11.6.1 20G224 darwin-x64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 96.0.4664.110

• No issues found!
```

</details>