Repository: flutter
Issue: Scaling jumping issues, widgets size calculation
Labels: c: regression,engine,platform-windows,has reproducible steps,P2,found in release: 3.3,found in release: 3.7,team-engine,triaged-engine

When you try to adjust your app for different screen resolutions on Windows by using (size * scaleFactor) way some widgets start jumping while wrapping them with AnimatedOpacity(Or Fading...), AnimatedSwitcher, Scrollable(with BouncingScrollPhysics) widgets.

This issue is in flutter somewhere from 2.2 version(at least that's the first time i noticed it). It's not related to https://github.com/flutter/flutter/issues/110957 because it's an old issue and did not appear in Flutter 3.3.

**Since this is not a raster cache problem i thought that it needs to be recreated in a separate issue so that somebody looks at it and fixes it.**

I think it happens because of rare double sizes which you get, for example, by multiplying 15 to your scale value which is 0.9. The result of such scaling often happens to be something like 13.275, 14.2, 21.8, etc.

![test_project_3_YqQNJcbRXv](https://user-images.githubusercontent.com/63228361/209406053-8d38dd01-614f-4356-8665-fe8e29e1e282.gif)
<details>

<summary>1 Example (AnimatedOpacity)</summary>

```dart
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

const double scaleFactor = 0.734341;

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

/// Default scroll configuration for the app with touch, mouse and stylus
/// support.
class EasyScrollBehavior extends ScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
        PointerDeviceKind.stylus,
      };
}

class _MyAppState extends State<MyApp> {
  bool _isHovered = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scrollBehavior: EasyScrollBehavior().copyWith(scrollbars: false),
      title: 'Material App',
      home: Scaffold(
        appBar: AppBar(
          title: const CustomTextComponent(
            text: 'Material AppBar',
            fontSize: 18.0,
            color: Colors.red,
          ),
        ),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              ExLDropdown(
                items: ['Test', '123213123', 'adpwd[apLWD[aWD'],
                initialValue: 'Test',
                onChanged: (_) {},
              ),
              // Expanded(
              //   child: ListView.builder(
              //     shrinkWrap: true,
              //     itemCount: 24,
              //     physics: const BouncingScrollPhysics(),
              //     itemBuilder: (_, index) => Container(
              //       width: 78.0 * scaleFactor,
              //       margin: const EdgeInsets.only(top: 11.0 * scaleFactor),
              //       color: Colors.red,
              //       child: Padding(
              //         padding: const EdgeInsets.symmetric(
              //           vertical: 13.0 * scaleFactor,
              //           horizontal: 13.0 * scaleFactor,
              //         ),
              //         child: Column(
              //           children: const [
              //             CustomTextComponent(
              //               text: 'text',
              //               fontSize: 18.0,
              //               color: Colors.white,
              //             ),
              //             Padding(
              //               padding: EdgeInsets.only(top: 2.0 * scaleFactor),
              //               child: CustomTextComponent(
              //                 text: 'text',
              //                 fontSize: 19.0,
              //                 color: Colors.white,
              //               ),
              //             ),
              //           ],
              //         ),
              //       ),
              //     ),
              //   ),
              // ),
            ],
          ),
        ),
      ),
    );
  }
}

extension FigmaDimention on double {
  double toFigmaHeight(double fontSize) {
    return this / fontSize;
  }
}

class CustomTextComponent extends StatelessWidget {
  const CustomTextComponent({
    super.key,
    required this.text,
    required this.fontSize,
    this.color,
  });

  final String text;
  final double fontSize;
  final Color? color;

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: TextStyle(
        fontSize: fontSize * scaleFactor,
        height: (16.0.toFigmaHeight(fontSize * scaleFactor) * scaleFactor),
        color: color,
      ),
    );
  }
}

class ExLDropdown extends StatefulWidget {
  final String? hintText;
  final List<String> items;

  /// If you use items with identical names and want to provide an icon than
  /// icon has to contain :;Key after it's path.
  final List<String>? iconPaths;
  final Function onChanged;
  final String? initialValue;
  final Function? onTapCallbackInsteadOfMenu;
  final double? maxDropdownMenuHeight;
  final bool includeBorder;
  final String? emptyValueIcon;
  final bool useSmallerInsets;
  final String? backgroundImage;
  final double? widthOverride;
  final List<BoxShadow>? shadow;

  const ExLDropdown({
    Key? key,
    this.initialValue,
    this.hintText,
    required this.items,
    this.iconPaths,
    required this.onChanged,
    this.onTapCallbackInsteadOfMenu,
    this.maxDropdownMenuHeight,
    this.includeBorder = false,
    this.emptyValueIcon,
    this.useSmallerInsets = false,
    this.backgroundImage,
    this.widthOverride,
    this.shadow,
  }) : super(key: key);

  @override
  _ExLDropdownState createState() => _ExLDropdownState();
}

class _ExLDropdownState extends State<ExLDropdown> {
  // A notifier to control arrow icon opacity
  final ValueNotifier<bool> _isHovered = ValueNotifier(false);
  late String? selectedValue = widget.initialValue;

  @override
  void initState() {
    // Ensure that icon will be rendered instead of default NoneSort value if
    // nothing is selected and emptyValueIcon is not null
    if (selectedValue == 'NoneSort' && widget.emptyValueIcon != null) {
      selectedValue = null;
    }
    super.initState();
  }

  @override
  void dispose() {
    _isHovered.dispose();
    super.dispose();
  }

  Future<void> _showMenuOrExecuteCallback(TapDownDetails details) async {
    if (widget.onTapCallbackInsteadOfMenu != null) {
      widget.onTapCallbackInsteadOfMenu?.call();
    } else {
      int? item;

      // Wait before menu disappear to call the callback
      await Future.delayed(const Duration(milliseconds: 300));
      if (item != null && selectedValue != widget.items[item] && mounted) {
        setState(() {
          // Prefer icon instead of NoneSort value
          if (widget.emptyValueIcon != null &&
              widget.items[item] == 'NoneSort') {
            selectedValue = null;
          } else {
            selectedValue = widget.items[item];
          }
        });
        widget.onChanged(widget.items[item]);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => _isHovered.value = true,
      onExit: (_) => _isHovered.value = false,
      cursor: SystemMouseCursors.click,
      child: ValueListenableBuilder(
        valueListenable: _isHovered,
        builder: (_, bool value, __) {
          // Determines whether emptyValueIcon should be rendered instead of
          // text and arrow.
          final bool renderIcon =
              widget.emptyValueIcon != null && selectedValue == null;

          // Color that is used for text, icon and arrow filling.
          final Color colorOverride = widget.backgroundImage != null
              ? Colors.white
              : Colors.white.withOpacity(0.75);

          // Vertical padding for main Container
          final double padd = (renderIcon
                  ? 12.0
                  : widget.useSmallerInsets
                      ? 10.0
                      : 14.0) *
              scaleFactor;

          // Horizontal padding for main Container
          final double horizontalPadd =
              (widget.useSmallerInsets ? 8.0 : 12.0) * scaleFactor;
          return GestureDetector(
            onTapDown: _showMenuOrExecuteCallback,
            child: AnimatedOpacity(
              curve: Curves.easeIn,
              duration: const Duration(milliseconds: 100),
              opacity: value ? 0.75 : 1.0,
              child: AnimatedSize(
                duration: const Duration(milliseconds: 350),
                curve: Curves.easeInOutCubic,
                child: AnimatedSwitcher(
                  duration: const Duration(milliseconds: 350),
                  switchInCurve: Curves.easeInOutCubic,
                  switchOutCurve: Curves.easeInOutCubic,
                  child: Stack(
                    alignment: Alignment.center,
                    clipBehavior: Clip.antiAliasWithSaveLayer,
                    key: ValueKey(renderIcon),
                    children: [
                      Container(
                        padding: EdgeInsets.only(
                          bottom: padd,
                          top: widget.includeBorder
                              ? (12.0 * scaleFactor)
                              : padd,
                        ),
                        width: widget.widthOverride != null
                            ? widget.widthOverride! * scaleFactor
                            : null,
                        decoration: BoxDecoration(
                          color: widget.backgroundImage == null
                              ? Theme.of(context).colorScheme.onSurface
                              : Colors.transparent,
                          borderRadius: const BorderRadius.all(
                            Radius.circular(4.0 * scaleFactor),
                          ),
                          border: widget.includeBorder
                              ? Border.all(
                                  width: 2.0 * scaleFactor,
                                  color: Theme.of(context)
                                      .colorScheme
                                      .onBackground
                                      .withOpacity(0.10),
                                )
                              : null,
                          boxShadow: widget.shadow,
                        ),
                        child: Padding(
                          padding: EdgeInsets.only(
                            left: horizontalPadd,
                            right: horizontalPadd,
                          ),
                          child: Row(
                            mainAxisSize: MainAxisSize.min,
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: <Widget>[
                              if (!renderIcon)
                                Padding(
                                  padding: const EdgeInsets.only(
                                    right: 16.0 * scaleFactor,
                                    left: 4.0 * scaleFactor,
                                  ),
                                  child: AnimatedSize(
                                    duration: widget.widthOverride != null
                                        ? const Duration(milliseconds: 235)
                                        : const Duration(milliseconds: 350),
                                    curve: Curves.easeInOutCubic,
                                    child: AnimatedSwitcher(
                                      duration: const Duration(
                                        milliseconds: 200,
                                      ),
                                      switchOutCurve: Curves.easeIn,
                                      switchInCurve: Curves.easeIn,
                                      child: CustomTextComponent(
                                        text: selectedValue != null
                                            ? selectedValue!.split(':;').first
                                            : widget.hintText ?? '',
                                        key: ValueKey(selectedValue),
                                        color: colorOverride,
                                        fontSize: 15.0,
                                      ),
                                    ),
                                  ),
                                ),
                              // if (!renderIcon)
                              //   ExLSvgIcon(
                              //     'resources/icons/arrow-down.svg',
                              //     color: colorOverride,
                              //     height: 24.0,
                              //     width: 24.0,
                              //   ),
                              // if (renderIcon)
                              //   ExLSvgIcon(
                              //     'resources/icons/${widget.emptyValueIcon}',
                              //     color: colorOverride,
                              //     height: 28.0,
                              //     width: 28.0,
                              //   ),
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}
```

</details>

![studio64_aVA7sP58Qp](https://user-images.githubusercontent.com/63228361/209406223-0a78ab82-7476-4616-91fb-01cb90d45f6e.gif)
<details>

<summary>2 Example (Slightly noticeable, but in real app with more complex widget tree it's a nightmare) (BouncingScrollPhysics)</summary>

```dart
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

const double scaleFactor = 0.734341;

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

/// Default scroll configuration for the app with touch, mouse and stylus
/// support.
class EasyScrollBehavior extends ScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
        PointerDeviceKind.stylus,
      };
}

class _MyAppState extends State<MyApp> {
  bool _isHovered = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scrollBehavior: EasyScrollBehavior().copyWith(scrollbars: false),
      title: 'Material App',
      home: Scaffold(
        appBar: AppBar(
          title: const CustomTextComponent(
            text: 'Material AppBar',
            fontSize: 18.0,
            color: Colors.red,
          ),
        ),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              // ExLDropdown(
              //   items: ['Test', '123213123', 'adpwd[apLWD[aWD'],
              //   initialValue: 'Test',
              //   onChanged: (_) {},
              // ),
              Expanded(
                child: ListView.builder(
                  shrinkWrap: true,
                  itemCount: 24,
                  physics: const BouncingScrollPhysics(),
                  itemBuilder: (_, index) => Container(
                    width: 78.0 * scaleFactor,
                    margin: const EdgeInsets.only(top: 11.0 * scaleFactor),
                    color: Colors.transparent,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(
                        vertical: 13.0 * scaleFactor,
                        horizontal: 13.0 * scaleFactor,
                      ),
                      child: Column(
                        children: const [
                          CustomTextComponent(
                            text: 'text',
                            fontSize: 18.0,
                            color: Colors.black,
                          ),
                          Padding(
                            padding: EdgeInsets.only(top: 2.0 * scaleFactor),
                            child: CustomTextComponent(
                              text: 'text',
                              fontSize: 19.0,
                              color: Colors.black,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

extension FigmaDimention on double {
  double toFigmaHeight(double fontSize) {
    return this / fontSize;
  }
}

class CustomTextComponent extends StatelessWidget {
  const CustomTextComponent({
    super.key,
    required this.text,
    required this.fontSize,
    this.color,
  });

  final String text;
  final double fontSize;
  final Color? color;

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: TextStyle(
        fontSize: fontSize * scaleFactor,
        height: (16.0.toFigmaHeight(fontSize * scaleFactor) * scaleFactor),
        color: color,
      ),
    );
  }
}

class ExLDropdown extends StatefulWidget {
  final String? hintText;
  final List<String> items;

  /// If you use items with identical names and want to provide an icon than
  /// icon has to contain :;Key after it's path.
  final List<String>? iconPaths;
  final Function onChanged;
  final String? initialValue;
  final Function? onTapCallbackInsteadOfMenu;
  final double? maxDropdownMenuHeight;
  final bool includeBorder;
  final String? emptyValueIcon;
  final bool useSmallerInsets;
  final String? backgroundImage;
  final double? widthOverride;
  final List<BoxShadow>? shadow;

  const ExLDropdown({
    Key? key,
    this.initialValue,
    this.hintText,
    required this.items,
    this.iconPaths,
    required this.onChanged,
    this.onTapCallbackInsteadOfMenu,
    this.maxDropdownMenuHeight,
    this.includeBorder = false,
    this.emptyValueIcon,
    this.useSmallerInsets = false,
    this.backgroundImage,
    this.widthOverride,
    this.shadow,
  }) : super(key: key);

  @override
  _ExLDropdownState createState() => _ExLDropdownState();
}

class _ExLDropdownState extends State<ExLDropdown> {
  // A notifier to control arrow icon opacity
  final ValueNotifier<bool> _isHovered = ValueNotifier(false);
  late String? selectedValue = widget.initialValue;

  @override
  void initState() {
    // Ensure that icon will be rendered instead of default NoneSort value if
    // nothing is selected and emptyValueIcon is not null
    if (selectedValue == 'NoneSort' && widget.emptyValueIcon != null) {
      selectedValue = null;
    }
    super.initState();
  }

  @override
  void dispose() {
    _isHovered.dispose();
    super.dispose();
  }

  Future<void> _showMenuOrExecuteCallback(TapDownDetails details) async {
    if (widget.onTapCallbackInsteadOfMenu != null) {
      widget.onTapCallbackInsteadOfMenu?.call();
    } else {
      int? item;

      // Wait before menu disappear to call the callback
      await Future.delayed(const Duration(milliseconds: 300));
      if (item != null && selectedValue != widget.items[item] && mounted) {
        setState(() {
          // Prefer icon instead of NoneSort value
          if (widget.emptyValueIcon != null &&
              widget.items[item] == 'NoneSort') {
            selectedValue = null;
          } else {
            selectedValue = widget.items[item];
          }
        });
        widget.onChanged(widget.items[item]);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => _isHovered.value = true,
      onExit: (_) => _isHovered.value = false,
      cursor: SystemMouseCursors.click,
      child: ValueListenableBuilder(
        valueListenable: _isHovered,
        builder: (_, bool value, __) {
          // Determines whether emptyValueIcon should be rendered instead of
          // text and arrow.
          final bool renderIcon =
              widget.emptyValueIcon != null && selectedValue == null;

          // Color that is used for text, icon and arrow filling.
          final Color colorOverride = widget.backgroundImage != null
              ? Colors.white
              : Colors.white.withOpacity(0.75);

          // Vertical padding for main Container
          final double padd = (renderIcon
                  ? 12.0
                  : widget.useSmallerInsets
                      ? 10.0
                      : 14.0) *
              scaleFactor;

          // Horizontal padding for main Container
          final double horizontalPadd =
              (widget.useSmallerInsets ? 8.0 : 12.0) * scaleFactor;
          return GestureDetector(
            onTapDown: _showMenuOrExecuteCallback,
            child: AnimatedOpacity(
              curve: Curves.easeIn,
              duration: const Duration(milliseconds: 100),
              opacity: value ? 0.75 : 1.0,
              child: AnimatedSize(
                duration: const Duration(milliseconds: 350),
                curve: Curves.easeInOutCubic,
                child: AnimatedSwitcher(
                  duration: const Duration(milliseconds: 350),
                  switchInCurve: Curves.easeInOutCubic,
                  switchOutCurve: Curves.easeInOutCubic,
                  child: Stack(
                    alignment: Alignment.center,
                    clipBehavior: Clip.antiAliasWithSaveLayer,
                    key: ValueKey(renderIcon),
                    children: [
                      Container(
                        padding: EdgeInsets.only(
                          bottom: padd,
                          top: widget.includeBorder
                              ? (12.0 * scaleFactor)
                              : padd,
                        ),
                        width: widget.widthOverride != null
                            ? widget.widthOverride! * scaleFactor
                            : null,
                        decoration: BoxDecoration(
                          color: widget.backgroundImage == null
                              ? Theme.of(context).colorScheme.onSurface
                              : Colors.transparent,
                          borderRadius: const BorderRadius.all(
                            Radius.circular(4.0 * scaleFactor),
                          ),
                          border: widget.includeBorder
                              ? Border.all(
                                  width: 2.0 * scaleFactor,
                                  color: Theme.of(context)
                                      .colorScheme
                                      .onBackground
                                      .withOpacity(0.10),
                                )
                              : null,
                          boxShadow: widget.shadow,
                        ),
                        child: Padding(
                          padding: EdgeInsets.only(
                            left: horizontalPadd,
                            right: horizontalPadd,
                          ),
                          child: Row(
                            mainAxisSize: MainAxisSize.min,
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: <Widget>[
                              if (!renderIcon)
                                Padding(
                                  padding: const EdgeInsets.only(
                                    right: 16.0 * scaleFactor,
                                    left: 4.0 * scaleFactor,
                                  ),
                                  child: AnimatedSize(
                                    duration: widget.widthOverride != null
                                        ? const Duration(milliseconds: 235)
                                        : const Duration(milliseconds: 350),
                                    curve: Curves.easeInOutCubic,
                                    child: AnimatedSwitcher(
                                      duration: const Duration(
                                        milliseconds: 200,
                                      ),
                                      switchOutCurve: Curves.easeIn,
                                      switchInCurve: Curves.easeIn,
                                      child: CustomTextComponent(
                                        text: selectedValue != null
                                            ? selectedValue!.split(':;').first
                                            : widget.hintText ?? '',
                                        key: ValueKey(selectedValue),
                                        color: colorOverride,
                                        fontSize: 15.0,
                                      ),
                                    ),
                                  ),
                                ),
                              // if (!renderIcon)
                              //   ExLSvgIcon(
                              //     'resources/icons/arrow-down.svg',
                              //     color: colorOverride,
                              //     height: 24.0,
                              //     width: 24.0,
                              //   ),
                              // if (renderIcon)
                              //   ExLSvgIcon(
                              //     'resources/icons/${widget.emptyValueIcon}',
                              //     color: colorOverride,
                              //     height: 28.0,
                              //     width: 28.0,
                              //   ),
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}
```

</details>


Example 3 - https://github.com/flutter/flutter/issues/110957#issuecomment-1247834470
Example 4 - https://github.com/flutter/flutter/issues/110957#issuecomment-1250040296