Skip to content

Request: Add a way to round inside shape border. #104201

@bernaferrari

Description

@bernaferrari

By default, Flutter squares the inner border of RoundedRectangleBorder (and BoxDecoration) on larger values. I don't think we can change that because it would be a bad breaking change. But we could add a property that improves it.

ORRR, we could have a breaking change. What if I made a PR and we checked how many goldens it breaks at Google? 🌚 Maybe there are not many people impacted because larger box is not that common. We could still have the old behavior in BoxDecoration that does some things differently.

image

Figma doesn't do this, and with the new StrokeAlign, I think things would be more consistent with this feel:
image

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData.dark(),
      home: const Scaffold(body: MyHomePage()),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

List<Widget> spaceRow(double gap, Iterable<Widget> children) => children
    .expand((item) sync* {
      yield SizedBox(width: gap);
      yield item;
    })
    .skip(1)
    .toList();

List<Widget> spaceColumn(double gap, Iterable<Widget> children) => children
    .expand((item) sync* {
      yield SizedBox(height: gap);
      yield item;
    })
    .skip(1)
    .toList();

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    const double width = 40;

    return ListView(
      padding: const EdgeInsets.all(50),
      children: [
        const Text(
          "Container",
          textAlign: TextAlign.center,
        ),
        const SizedBox(height: 40),
        Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: spaceRow(
            width * 2,
            [
              for (StrokeAlign align in StrokeAlign.values)
                Container(
                  width: 100,
                  height: 100,
                  decoration: ShapeDecoration(
                    color: Colors.green,
                    shape: RoundedRectangleBorder(
                      isStrokeInsideRound: false,
                      side: BorderSide(
                        color: Colors.red.withOpacity(0.5),
                        width: 20,
                        strokeAlign: align,
                      ),
                      borderRadius: BorderRadius.circular(20),
                    ),
                  ),
                ),
            ],
          ),
        ),
        const SizedBox(height: 40),
        Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: spaceRow(
            width * 2,
            [
              for (StrokeAlign align in StrokeAlign.values)
                Container(
                  width: 100,
                  height: 100,
                  decoration: BoxDecoration(
                    color: Colors.red,
                    borderRadius: BorderRadius.circular(0),
                    border: Border.all(
                      color: Colors.black.withOpacity(0.75),
                      width: width,
                      strokeAlign: align,
                    ),
                  ),
                  // child: Center(
                  //     child: Container(
                  //         width: 100, height: 100, color: Colors.blue)),
                ),
            ],
          ),
        ),
        const SizedBox(height: width * 2),
        Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: spaceRow(
            width * 2,
            [
              for (StrokeAlign align in StrokeAlign.values)
                Container(
                  width: 100,
                  height: 100,
                  decoration: BoxDecoration(
                      color: Colors.green,
                      border: Border.all(
                        color: Colors.black.withOpacity(0.75),
                        width: 20,
                        strokeAlign: align,
                      ),
                      borderRadius: BorderRadius.circular(8)),
                ),
            ],
          ),
        ),
        const SizedBox(height: width * 2),
        Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: spaceRow(
            width * 2,
            [
              for (StrokeAlign align in StrokeAlign.values)
                Container(
                  width: 100,
                  height: 100,
                  decoration: BoxDecoration(
                    color: Colors.blue,
                    shape: BoxShape.circle,
                    border: Border.all(
                      color: Colors.black.withOpacity(0.75),
                      width: width,
                      strokeAlign: align,
                    ),
                  ),
                ),
            ],
          ),
        ),
      ],
    );
  }
}

Proposed fix:

Add isStrokeInsideRound to RoundedRectangleBorder and update paint to the following:

@override
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
        final double width = side.width;
        if (width == 0.0) {
          canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), side.toPaint());
        } else {
          final Paint paint = Paint()
            ..color = side.color;
          if (side.strokeAlign == StrokeAlign.inside && !isStrokeInsideRound) {
            final RRect outer = borderRadius.resolve(textDirection).toRRect(rect);
            final RRect inner = outer.deflate(width);
            canvas.drawDRRect(outer, inner, paint);
          } else {
            final Rect inner;
            final Rect outer;
            switch (side.strokeAlign) {
              case StrokeAlign.inside:
                inner = rect.deflate(width);
                outer = rect;
                break;
              case StrokeAlign.center:
                inner = rect.deflate(width / 2);
                outer = rect.inflate(width / 2);
                break;
              case StrokeAlign.outside:
                inner = rect;
                outer = rect.inflate(width);
                break;
            }
            final BorderRadius borderRadiusResolved = borderRadius.resolve(textDirection);
            canvas.drawDRRect(borderRadiusResolved.toRRect(outer), borderRadiusResolved.toRRect(inner), paint);
        }
      }
    }
  }

Challenge: lerp is kind of impossible between two booleans.

Metadata

Metadata

Assignees

Labels

c: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterf: material designflutter/packages/flutter/material repository.frameworkflutter/packages/flutter repository. See also f: labels.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions