I’m new to Flutter and trying to replicate a design that has curved shapes with categories displayed along the curve
.
I’ve tried using ClipPath, but I haven’t been able to get the bottom arc to match the top one. Also, the category icons don’t naturally follow the curve of the container — they’re still aligned horizontally. I experimented with Stack, but the positioning is still off.
Below is everything I’ve tried so far.
import 'package:flutter/material.dart';
import 'package:starbucks_project/components/category_tile.dart';
import 'package:starbucks_project/data/category.dart';
import 'package:starbucks_project/themes/colors.dart';
import 'package:google_fonts/google_fonts.dart';
class DoubleCurveClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
// TODO: implement getClip
var path = Path();
// top curve
path.moveTo(0, 50);
path.quadraticBezierTo(
// control point (upward)
size.width / 2,
-50,
// endpoint
size.width,
50,
);
// right side
path.lineTo(size.width, size.height - 50);
// bottom curve
path.quadraticBezierTo(
// control point (upward)
size.width / 2,
-10,
// endpoint
0,
size.height - 50,
);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
class Intro extends StatefulWidget {
const Intro({super.key});
@override
State<Intro> createState() => _IntroState();
}
class _IntroState extends State<Intro> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
// appbar
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: Padding(
padding: const EdgeInsets.all(25),
child: Image.asset('lib/images/icons/logo.png'),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 25),
child: IconButton(
onPressed: () {},
icon: Icon(
Icons.shopping_bag_outlined,
color: iconColor,
size: 32,
),
),
),
],
),
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('lib/images/icons/background.png'),
fit: BoxFit.contain,
alignment: Alignment(0, -0.8),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// text
SizedBox(height: 32),
Container(
width: 280,
child: Padding(
padding: const EdgeInsets.only(left: 48.0),
child: Text(
'Enjoy coffee any time',
style: GoogleFonts.sen(
fontWeight: FontWeight.bold,
fontSize: 36,
),
),
),
),
SizedBox(height: 38),
ClipPath(
clipper: DoubleCurveClipper(),
child: Container(
decoration: BoxDecoration(color: primaryColor),
height: 280,
// child: ListView.builder(
// scrollDirection: Axis.horizontal,
// itemCount: allCategories.length,
// itemBuilder: (context, index) {
// final category = allCategories[index];
// return CategoryTile(category: category, onTap: () {});
// },
// ),
child: Stack(
children: List.generate(allCategories.length, (index) {
double width = MediaQuery.of(context).size.width;
double spacing =
width / allCategories.length; // space each tile evenly
double x = spacing * index;
// This is the "y along the curve" (simplified)
double y = 50 - 50 * (4 * (x / width) * (1 - x / width));
return Positioned(
left: x,
top: y,
child: CategoryTile(
category: allCategories[index],
onTap: () {},
),
);
}),
),
),
),
],
),
),
);
}
}