Introduction
The circular_clip_route
package provides an easy way to create circular clipping effects for route transitions in Flutter. This can add a visually appealing and unique touch to your app’s navigation.
Content
1.Add the circular_clip_route
dependency:
Open your pubspec.yaml
file and add the circular_clip_route
dependency.
dependencies:
circular_clip_route: ^latest_version
2.Import the package:
Import the circular_clip_route
package in your Dart file.
import 'package:circular_clip_route/circular_clip_route.dart';
3.Create a Circular Clip Route:
Use the CircularClipRoute
class to create a circular clip route.
Navigator.of(context).push(
CircularClipRoute(
builder: (context) => YourNextScreen(),
animationDuration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut,
clipColor: Colors.white,
),
);
The builder
parameter should return the widget for the next screen.
Adjust the animationDuration
, curve
, reverseCurve
, and clipColor
parameters to customize the route transition animation
4.Customize the Circular Clip Transition:
You can further customize the circular clip transition by providing a custom clipper.
Navigator.of(context).push(
CircularClipRoute(
builder: (context) => YourNextScreen(),
clipper: (animation, child) => MyCustomClipper(animation, child),
// Other parameters...
),
);
5.Handle Backward Navigation:
When navigating back, the circular clip effect should also be applied. You can achieve this by using the CircularClipRoute
for popping the route.
Navigator.of(context).pop(
CircularClipRoute(
builder: (context) => YourPreviousScreen(),
// Other parameters...
),
);
6.Run the app:
Run your Flutter app to see the circular clip route transitions in action.
Sample Code
import 'package:flutter/material.dart';
import 'package:circular_clip_route/circular_clip_route.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: ContactListPage(),
);
}
}
class ContactInfo {
const ContactInfo({
required this.avatarAsset,
required this.name,
required this.email,
required this.phone,
});
final String avatarAsset;
final String name;
final String email;
final String phone;
}
const contactInfos = [
ContactInfo(
avatarAsset: 'https://images.pexels.com/photos/771742/pexels-photo-771742.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
name: 'Tom Arbo',
email: 'tom.arbo@example.com',
phone: '(711) 265-9193',
),
ContactInfo(
avatarAsset: 'https://images.pexels.com/photos/771742/pexels-photo-771742.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
name: 'Elly Foster',
email: 'elly@example.com',
phone: '(675) 844-7400',
),
ContactInfo(
avatarAsset: 'https://images.pexels.com/photos/771742/pexels-photo-771742.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
name: 'Carolyn Durnham',
email: 'carolyn.durnham@example.com',
phone: '(995) 565-4039',
),
ContactInfo(
avatarAsset: 'https://images.pexels.com/photos/771742/pexels-photo-771742.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
name: 'Corrina Nicholls',
email: 'c.nicholls@example.com',
phone: '(966) 291-5045',
),
ContactInfo(
avatarAsset: 'https://images.pexels.com/photos/771742/pexels-photo-771742.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
name: 'Omer Anderson',
email: 'omer@example.net',
phone: '(519) 978-4733',
),
];
class ContactListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Contacts'),
),
body: ListView.builder(
itemBuilder: (context, i) => ContactListItem(contactInfo: contactInfos[i]),
itemCount: contactInfos.length,
),
);
}
}
class ContactListItem extends StatefulWidget {
const ContactListItem({
required this.contactInfo,
});
final ContactInfo contactInfo;
@override
_ContactListItemState createState() => _ContactListItemState();
}
class _ContactListItemState extends State<ContactListItem> {
final _avatarKey = GlobalKey();
@override
Widget build(BuildContext context) {
return ListTile(
leading: SizedBox(
key: _avatarKey,
width: 50,
height: 50,
child: AvatarHero(contactInfo: widget.contactInfo),
),
title: Text(widget.contactInfo.name),
subtitle: Text(widget.contactInfo.email),
onTap: () {
Navigator.push(
context,
CircularClipRoute<void>(
builder: (context) => ContactDetailPage(contactInfo: widget.contactInfo),
expandFrom: _avatarKey.currentContext!,
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.fastOutSlowIn.flipped,
opacity: ConstantTween(1),
transitionDuration: const Duration(seconds: 1),
),
);
},
);
}
}
class AvatarHero extends StatelessWidget {
final ContactInfo contactInfo;
const AvatarHero({
required this.contactInfo,
});
@override
Widget build(BuildContext context) {
final child = Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 3),
),
],
),
child: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
clipBehavior: Clip.antiAlias,
child: Image.network(
contactInfo.avatarAsset,
fit: BoxFit.cover,
),
),
);
return Hero(
tag: 'image_${contactInfo.hashCode}',
createRectTween: (begin, end) {
return RectTween(
begin: Rect.fromCenter(
center: begin!.center,
width: begin.width,
height: begin.height,
),
end: Rect.fromCenter(
center: end!.center,
width: end.width,
height: end.height,
),
);
},
child: child,
);
}
}
class ContactDetailPage extends StatefulWidget {
const ContactDetailPage({
required this.contactInfo,
});
final ContactInfo contactInfo;
@override
_ContactDetailPageState createState() => _ContactDetailPageState();
}
class _ContactDetailPageState extends State<ContactDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.contactInfo.name),
elevation: 0,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FractionallySizedBox(
widthFactor: 1,
child: Container(
alignment: Alignment.center,
child: Stack(
alignment: Alignment.topCenter,
children: [
Container(
height: 140,
color: Colors.blue,
),
Container(
height: 220,
width: 220,
padding: const EdgeInsets.all(20.0),
child: AvatarHero(contactInfo: widget.contactInfo),
),
],
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Table(
columnWidths: const {
0: IntrinsicColumnWidth(flex: 1),
1: FlexColumnWidth(99),
},
textBaseline: TextBaseline.alphabetic,
defaultVerticalAlignment: TableCellVerticalAlignment.baseline,
children: [
_buildTableRow(label: 'Email', value: widget.contactInfo.email),
_buildTableRow(label: 'Phone', value: widget.contactInfo.phone),
],
),
),
],
),
);
}
TableRow _buildTableRow({required String label, required String value}) {
return TableRow(
children: [
IntrinsicWidth(
child: Container(
padding: const EdgeInsets.all(10),
alignment: Alignment.centerRight,
child: Text(
'$label:',
style: Theme.of(context).textTheme.bodyText1,
),
),
),
Text(value),
],
);
}
}
Output
Conclusion
By following these steps, you can easily integrate the circular_clip_route
package into your Flutter app and create circular clipping effects for route transitions. This can enhance the visual appeal of your app’s navigation.