Repository: flutter
Issue: Icon does not pay attention to emoji's real width
Labels: framework,has reproducible steps,P2,team-framework,triaged-framework,found in release: 3.27,found in release: 3.28

### Steps to reproduce

Drawing Icon with emoji in IconData type argument inside colored/bordered/etc. container leads to misaligned icon relatively horizontal center (tested on both Linux and Android).

I looked up into Icon implementation and tried fix it myself just for fun, I have achieved expected result when multiplied values both fontSize and height to 0.81 in TextStyle fontStyle inside Icon's build() method. Tested only on default material font used by Icon class by default.

<details><summary>Code sample to compare default Icon's behavior and modified one's (you also can compare code below with lib/src/widgets/icon.dart)</summary>

```dart
import 'package:flutter/material.dart';

int getCodepointFromString(String s) {
    assert([1, 2].contains(s.codeUnits.length));

    if (s.codeUnits.length == 2) {
        final (highCodeUnit, lowCodeUnit) = (s.codeUnitAt(0), s.codeUnitAt(1));
        assert((highCodeUnit >= 0xD800 && highCodeUnit <= 0xDBFF) && (lowCodeUnit >= 0xDC00 && lowCodeUnit <= 0xDFFF));
        return ((highCodeUnit - 0xD800) << 10) + (lowCodeUnit - 0xDC00) + 0x10000;
    }

    return s.codeUnitAt(0);
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  var index = 0;

  @override
  Widget build(BuildContext context) {
final emojiCode = getCodepointFromString('😀');
  final size = MediaQuery.of(context).size.shortestSide / 2;
  final color = Colors.indigoAccent;
  final widgets = <Widget>[ Icon(IconData(emojiCode), size: size), LabelIcon(IconData(emojiCode), size: size) ];

    return MaterialApp(
      home: Scaffold(
        body: GestureDetector(
          onTap: () => setState(() => index ^= 1),
          child: Center(
          child: ColoredBox(
            color: color,
            child: widgets[index],
          ),
          ),
        ),
      ),
    );
  }
}

class LabelIcon extends StatelessWidget {
  /// Creates an icon.
  const LabelIcon(
    this.icon, {
    super.key,
    this.size,
    this.fill,
    this.weight,
    this.grade,
    this.opticalSize,
    this.color,
    this.shadows,
    this.semanticLabel,
    this.textDirection,
    this.applyTextScaling,
    this.blendMode,
  })  : assert(fill == null || (0.0 <= fill && fill <= 1.0)),
        assert(weight == null || (0.0 < weight)),
        assert(opticalSize == null || (0.0 < opticalSize));

  final IconData? icon;
  final double? size;
  final double? fill;
  final double? weight;
  final double? grade;
  final double? opticalSize;
  final Color? color;
  final List<Shadow>? shadows;
  final String? semanticLabel;
  final TextDirection? textDirection;
  final bool? applyTextScaling;
  final BlendMode? blendMode;

  @override
  Widget build(BuildContext context) {
    assert(this.textDirection != null || debugCheckHasDirectionality(context));
    final TextDirection textDirection = this.textDirection ?? Directionality.of(context);

    final IconThemeData iconTheme = IconTheme.of(context);

    final bool applyTextScaling = this.applyTextScaling ?? iconTheme.applyTextScaling ?? false;

    final double tentativeIconSize = size ?? iconTheme.size ?? kDefaultFontSize;

    final double iconSize = applyTextScaling ? MediaQuery.textScalerOf(context).scale(tentativeIconSize) : tentativeIconSize;

    final double? iconFill = fill ?? iconTheme.fill;

    final double? iconWeight = weight ?? iconTheme.weight;

    final double? iconGrade = grade ?? iconTheme.grade;

    final double? iconOpticalSize = opticalSize ?? iconTheme.opticalSize;

    final List<Shadow>? iconShadows = shadows ?? iconTheme.shadows;

    final IconData? icon = this.icon;
    if (icon == null) {
      return Semantics(
        label: semanticLabel,
        child: SizedBox(width: iconSize, height: iconSize),
      );
    }

    final double iconOpacity = iconTheme.opacity ?? 1.0;
    Color? iconColor = color ?? iconTheme.color!;
    Paint? foreground;
    if (iconOpacity != 1.0) {
      iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity);
    }
    if (blendMode != null) {
      foreground = Paint()
        ..blendMode = blendMode!
        ..color = iconColor;
      // Cannot provide both a color and a foreground.
      iconColor = null;
    }

    const fix = 0.81;

    final TextStyle fontStyle = TextStyle(
      fontVariations: <FontVariation>[
        if (iconFill != null) FontVariation('FILL', iconFill),
        if (iconWeight != null) FontVariation('wght', iconWeight),
        if (iconGrade != null) FontVariation('GRAD', iconGrade),
        if (iconOpticalSize != null) FontVariation('opsz', iconOpticalSize),
      ],
      inherit: false,
      color: iconColor,
      fontSize: iconSize * fix,
      fontFamily: icon.fontFamily,
      package: icon.fontPackage,
      fontFamilyFallback: icon.fontFamilyFallback,
      shadows: iconShadows,
      height: 1.0 * fix, // Makes sure the font's body is vertically centered within the iconSize x iconSize square.
      leadingDistribution: TextLeadingDistribution.even,
      foreground: foreground,
    );

    Widget iconWidget = RichText(
      overflow: TextOverflow.visible, // Never clip.
      textDirection: textDirection, // Since we already fetched it for the assert...
      text: TextSpan(
        text: String.fromCharCode(icon.codePoint),
        style: fontStyle,
      ),
    );

    if (icon.matchTextDirection) {
      switch (textDirection) {
        case TextDirection.rtl:
          iconWidget = Transform(
            transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0),
            alignment: Alignment.center,
            transformHitTests: false,
            child: iconWidget,
          );
        case TextDirection.ltr:
          break;
      }
    }

    return Semantics(
      label: semanticLabel,
      child: ExcludeSemantics(
        child: SizedBox(
          width: iconSize,
          height: iconSize,
          child: Center(
            child: iconWidget,
          ),
        ),
      ),
    );
  }
}
```

</details>


### Expected results

Emoji IconData should be centered horizontally.

### Actual results

Emoji IconData is not centered horizontally and overflows.

### Code sample

Minimal reproducible example.

<details><summary>Code sample</summary>

```dart
import 'package:flutter/material.dart';

int getCodepointFromString(String s) {
    assert([1, 2].contains(s.codeUnits.length));

    if (s.codeUnits.length == 2) {
        final (highCodeUnit, lowCodeUnit) = (s.codeUnitAt(0), s.codeUnitAt(1));
        assert((highCodeUnit >= 0xD800 && highCodeUnit <= 0xDBFF) && (lowCodeUnit >= 0xDC00 && lowCodeUnit <= 0xDFFF));
        return ((highCodeUnit - 0xD800) << 10) + (lowCodeUnit - 0xDC00) + 0x10000;
    }

    return s.codeUnitAt(0);
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    final emojiCode = getCodepointFromString('😀');
    final size = MediaQuery.of(context).size.shortestSide / 2;
    final color = Colors.indigoAccent;

    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ColoredBox(
            color: color,
            child: Icon(IconData(emojiCode), size: size),
          ),
        ),
      ),
    );
  }
}
```

</details>


### Screenshots or Video

<details>
<summary>Screenshots / Video demonstration</summary>

Actual result:
![actual](https://github.com/user-attachments/assets/8d001360-2918-46a7-a3d8-83d31264bf80)

Expected result:
![expected](https://github.com/user-attachments/assets/24951ff4-6159-4f60-bc67-22dd596ba002)

</details>


### Logs

_No response_

### Flutter Doctor output

<details><summary>Doctor output</summary>

```console
[✓] Flutter (Channel stable, 3.27.1, on Manjaro Linux 6.12.4-1-MANJARO, locale en_US.UTF-8)
    • Flutter version 3.27.1 on channel stable at /opt/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 17025dd882 (3 weeks ago), 2024-12-17 03:23:09 +0900
    • Engine revision cb4b5fff73
    • Dart version 3.6.0
    • DevTools version 2.40.2

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /home/serge/AndroidSDK
    • Platform android-35, build-tools 34.0.0
    • ANDROID_HOME = /home/serge/AndroidSDK
    • Java binary at: /usr/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.13+11)
    • All Android licenses accepted.

[✗] Chrome - develop for the web (Cannot find Chrome executable at google-chrome)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.

[✓] Linux toolchain - develop for Linux desktop
    • clang version 18.1.8
    • cmake version 3.31.2
    • ninja version 1.12.1
    • pkg-config version 2.3.0

[!] Android Studio (not installed)
    • Android Studio not found; download from https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/to/linux-android-setup for detailed instructions).

[✓] Connected device (1 available)
    • Linux (desktop) • linux • linux-x64 • Manjaro Linux 6.12.4-1-MANJARO

[✓] Network resources
    • All expected network resources are available.

! Doctor found issues in 2 categories.
```

</details>