skip to Main Content

I’m trying to recreate this effect in Flutter, I initially created it in Angular using CSS.

Basically imagine a bottom NavBar sitting behind a container with bottom border radius.

I’m pretty new to Flutter and still learning things, but there doesn’t seem to be an obvious way to accomplish this. I do know about Stack but I’m not sure if I can combine it with BottomNavigationBar (or if it’s good practice seeing how a property on Scaffold is made specifically for it)

NavBar

Here’s what I tried using ClipPath.

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(
      home: Scaffold(
        extendBody: true,
        body: Column(
          children: [
            Expanded(
                child: Container(
              height: 100,
              decoration: const BoxDecoration(
                color: Colors.red,
              ),
            ))
          ],
        ),
        bottomNavigationBar: const NavBar(),
      ),
    );
  }
}

class NavBar extends StatefulWidget {
  const NavBar({super.key});
  @override
  State<NavBar> createState() => _NavBarState();
}

class _NavBarState extends State<NavBar> {
  static const List<BottomNavigationBarItem> _items = <BottomNavigationBarItem>[
    BottomNavigationBarItem(
      icon: Icon(Icons.home_outlined),
      label: 'Home',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.search),
      label: 'Search',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.messenger_outline),
      label: 'Message',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.person_outlined),
      label: 'Profile',
    ),
  ];
  final int _selectedIndex = 0;
  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: CustomShapeClipper(),
      child: SizedBox(
        height: 75,
        child: BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          items: _items,
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.amber[800],
          unselectedItemColor: Colors.black,
          showUnselectedLabels: true,
          showSelectedLabels: true,
        ),
      ),
    );
  }
}

class CustomShapeClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    double cornerHeight = size.height - 75;
    path.moveTo(0, size.height);
    path.lineTo(0, cornerHeight); // Raise the starting point of the curve to the top right corner
    path.quadraticBezierTo(size.width * 0.5, size.height - 50, size.width, cornerHeight); // Adjust the control points to flatten the curve
    path.lineTo(size.width, size.height);
    path.close();

    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return false;
  }
}

This is what it looks like:

Code Example Output

Is there a better way to accomplish this?
Any help would really be appreciated.

2

Answers


  1. You should use CustomPainter

    fixed code:

    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(
          home: Scaffold(
            extendBody: true,
            body: Column(
              children: [
                Expanded(
                  child: Container(
                    height: 100,
                    decoration: const BoxDecoration(
                      color: Colors.red,
                    ),
                  ),
                ),
              ],
            ),
            bottomNavigationBar: const NavBar(),
          ),
        );
      }
    }
    
    class NavBar extends StatefulWidget {
      const NavBar({super.key});
      @override
      State<NavBar> createState() => _NavBarState();
    }
    
    class _NavBarState extends State<NavBar> {
      static const List<BottomNavigationBarItem> _items = <BottomNavigationBarItem>[
        BottomNavigationBarItem(
          icon: Icon(Icons.home_outlined),
          label: 'Home',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.search),
          label: 'Search',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.messenger_outline),
          label: 'Message',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.person_outlined),
          label: 'Profile',
        ),
      ];
      final int _selectedIndex = 0;
      @override
      Widget build(BuildContext context) {
        return CustomPaint(
          painter: _Painter(radius: const Radius.elliptical(100, 75)),
          child: SizedBox(
            child: BottomNavigationBar(
              type: BottomNavigationBarType.fixed,
              items: _items,
              currentIndex: _selectedIndex,
              selectedItemColor: Colors.amber[800],
              unselectedItemColor: Colors.black,
              showUnselectedLabels: true,
              showSelectedLabels: true,
            ),
          ),
        );
      }
    }
    
    class _Painter extends CustomPainter {
      final Radius radius;
    
      _Painter({this.radius = Radius.zero});
    
      @override
      void paint(Canvas canvas, Size size) {
        Paint paint = Paint()
          ..color = Colors.white
          ..style = PaintingStyle.fill;
    
        final width = size.width;
        final height = size.height;
        final yRadius = radius.y;
        final xRadius = radius.x;
        final p1 = Offset(0, -yRadius);
        final p2 = Offset(xRadius, 0);
        final p3 = Offset(width - xRadius, 0);
        final p4 = Offset(width, -yRadius);
        final p5 = Offset(width, height);
        final p6 = Offset(0, height);
    
        final path = Path();
        path.moveTo(p1.dx, p1.dy);
        path.arcToPoint(p2, radius: radius, clockwise: false);
        path.lineTo(p3.dx, p3.dy);
        path.arcToPoint(p4, radius: radius, clockwise: false);
        path.lineTo(p5.dx, p5.dy);
        path.lineTo(p6.dx, p6.dy);
        path.close();
    
        canvas.drawPath(path, paint);
      }
    
      @override
      bool shouldRepaint(covariant _Painter oldDelegate) {
        return oldDelegate.radius != radius;
      }
    }
    

    result:
    enter image description here

    Login or Signup to reply.
  2. The simple approach is you can use ClipRRect to clip your body, and by using this method you can change the radius easily by changing borderRadius property.

    The code below is to demonstrate the clipping result with ClipRRect to ListView

    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(
          home: Scaffold(
            body: Column(
              children: [
                Expanded(
                  child: ClipRRect(
                    borderRadius: BorderRadius.vertical(bottom: Radius.circular(50)),
                    child: Container(
                        height: 100,
                        decoration: BoxDecoration(color: Colors.red),
                        child: ListView.builder(
                          itemCount: 100,
                          itemBuilder: (_, i) => Text(i.toString()),
                        )),
                  ),
                ),
              ],
            ),
            bottomNavigationBar: const NavBar(),
          ),
        );
      }
    }
    
    class NavBar extends StatefulWidget {
      const NavBar({super.key});
      @override
      State<NavBar> createState() => _NavBarState();
    }
    
    class _NavBarState extends State<NavBar> {
      static const List<BottomNavigationBarItem> _items = <BottomNavigationBarItem>[
        BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: 'Home'),
        BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
        BottomNavigationBarItem(icon: Icon(Icons.messenger_outline), label: 'Message'),
        BottomNavigationBarItem(icon: Icon(Icons.person_outlined), label: 'Profile'),
      ];
      final int _selectedIndex = 0;
      @override
      Widget build(BuildContext context) {
        return BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          elevation: 0,
          items: _items,
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.amber[800],
          unselectedItemColor: Colors.black,
          showUnselectedLabels: true,
          showSelectedLabels: true,
        );
      }
    }
    

    And this is the result:

    enter image description here

    Hopefully it can solve your problem, Thanks 😉

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search