For my quiz app in Flutter, I am trying to create a widget for invitations to a new quiz. To accept / decline it, a Dismissible is used to swipe the invitation to the left (decline, red background) or to the right (accept, green background). The calculated background color is also applied to the invitation itself.
Here is a screenshot of what a dragged invitation looks so far:
The problem is that the "foreground" green color is inside a container with rounded borders. The corners of that container remain white resulting in this ugly transition from background to foreground. I want it to look like it is one huge rounded container when it is dragged to one side without the white corners.
Here is the code for the widget:
class InvitationCard extends StatefulWidget {
final GameInvitation invitation;
final void Function(GameInvitation) onAccept;
final void Function(GameInvitation) onDecline;
const InvitationCard({super.key, required this.invitation, required this.onAccept, required this.onDecline});
@override
State<InvitationCard> createState() => _InvitationCardState();
}
class _InvitationCardState extends State<InvitationCard> {
final GlobalKey _cardKey = GlobalKey();
double? cardHeight;
double swipeOffset = 0;
Color overlayedColor = Colors.transparent;
EdgeInsets padding = const EdgeInsets.all(8.0);
double elevation = 1.5;
BorderRadius borderRadius = BorderRadius.circular(30);
Alignment iconAlignment = Alignment.centerLeft;
@override
void initState() {
super.initState();
// wait for the first frame to be rendered
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = _cardKey.currentContext;
if (context != null) {
final size = context.size;
if (size != null && mounted) {
setState(() {
cardHeight = size.height;
});
}
}
});
}
void _updateSwipeState(double offset) {
setState(() {
swipeOffset = offset;
if (swipeOffset == 0) {
// no action
padding = const EdgeInsets.all(8.0);
elevation = 1.5;
borderRadius = BorderRadius.circular(30);
overlayedColor = Colors.transparent;
iconAlignment = Alignment.centerLeft;
} else if (swipeOffset > 0) {
// accept
padding = const EdgeInsets.only(top: 8.0, bottom: 8.0);
elevation = 0;
borderRadius = const BorderRadius.only(topRight: Radius.circular(30), bottomRight: Radius.circular(30));
overlayedColor = Colors.green.withValues(alpha: (swipeOffset / MediaQuery.of(context).size.width).clamp(0.3, 1.0));
iconAlignment = Alignment.centerLeft;
} else {
// decline
padding = const EdgeInsets.only(top: 8.0, bottom: 8.0);
elevation = 0;
borderRadius = const BorderRadius.only(topLeft: Radius.circular(30), bottomLeft: Radius.circular(30));
overlayedColor = Colors.red.withValues(alpha: (-swipeOffset / MediaQuery.of(context).size.width).clamp(0.3, 1.0));
iconAlignment = Alignment.centerRight;
}
if (swipeOffset.abs() < 30) {
borderRadius = BorderRadius.circular(30);
}
});
}
Widget swipeRightBackground() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: Container(
width: 100,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: (swipeOffset / MediaQuery.of(context).size.width).clamp(0.3, 1.0)),
borderRadius: BorderRadius.circular(30),
),
child: Icon(Icons.check, color: Colors.white, size: 30),
),
),
);
}
Widget swipeLeftBackground() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: (-swipeOffset / MediaQuery.of(context).size.width).clamp(0.3, 1.0)),
borderRadius: BorderRadius.circular(30),
),
child: Icon(Icons.delete, color: Colors.white, size: 30),
),
),
);
}
Widget _roundTypeBox(IconData icon, String typeName) {
return Container(
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary, borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey),
const SizedBox(width: 8),
Text(typeName, style: TextStyle(color: Colors.grey, fontSize: 16, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _invitationInformation(GameInvitation invitation) {
List<String> gameTypes = [];
List<IconData> icons = [];
if (invitation.isMultipleChoiceSelected) {
gameTypes.add("Multiple Choice");
icons.add(Symbols.tile_small);
}
if (invitation.isEstimatingSelected) {
gameTypes.add("Schätzfragen");
icons.add(Symbols.sliders);
}
// String gameTypesString = gameTypes.join(", ");
return Padding(
padding: padding,
child: Material(
elevation: elevation,
shadowColor: Theme.of(context).shadowColor,
borderRadius: borderRadius,
child: Container(
decoration: BoxDecoration(color: Theme.of(context).colorScheme.primary, borderRadius: borderRadius),
child: ListTile(
title: Stack(
children: [
Row(
children: [
roundProfileImage(context, null, 50, 50),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary, borderRadius: BorderRadius.circular(8)),
child: Text(invitation.host, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
SizedBox(width: 10),
Text(invitation.timestamp.timeAgo, style: TextStyle(fontSize: 16, color: Colors.grey, fontWeight: FontWeight.bold)),
],
),
Text("${invitation.roundsCount} Runden", style: TextStyle(fontSize: 16, color: Colors.grey, fontWeight: FontWeight.bold)),
Row(
children: [
...List.generate(gameTypes.length, (index) => _roundTypeBox(icons[index], gameTypes[index])),
const SizedBox(width: 10),
],
),
],
),
],
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Dismissible(
key: ValueKey(widget.invitation.id),
direction: DismissDirection.horizontal,
background: swipeRightBackground(), // accept
secondaryBackground: swipeLeftBackground(), // decline
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
widget.onAccept(widget.invitation);
} else {
widget.onDecline(widget.invitation);
}
return false;
},
child: Listener(
onPointerMove: (event) => setState(() => _updateSwipeState(swipeOffset + event.delta.dx)),
onPointerUp: (_) => setState(() => _updateSwipeState(0)),
child: Stack(
children: [
SizedBox(key: _cardKey, child: _invitationInformation(widget.invitation)),
if (cardHeight != null)
Padding(
padding: padding,
child: ClipRRect(
borderRadius: borderRadius,
child: Container(
height: cardHeight! - 16, // subtract padding
alignment: iconAlignment,
decoration: BoxDecoration(color: overlayedColor, borderRadius: borderRadius),
padding: EdgeInsets.symmetric(horizontal: 20),
),
),
),
],
),
),
);
}
}
This is what the not-dragged widget looks like:
Can someone fix these ugly corners to let them not appear when dragging?
If needed, I can provide you with more screenshots and code.
I suggest that you completely revise your code structure and use flutter_slidable instead.
If you want to achieve the same behavior as the Dismissible
class, then by using flutter_slidable, just use its properties dismissible: DismissiblePane(onDismissed: () {}),
This will simplify your code logic and also lessen its verbosity to achieve a similar outcome. Relax, it is part of the Flutter Favorite program
To achieve these use cases:
- swipe the invitation to the left (decline, red background)
- or to the right (accept, green background)
You need to use startActionPane
and endActionPane
and their dismissible:
properties.
P.S., I am also using it because of the ease of use and integration.