Making Flutter apps look more native. Part 1: tap effects

Roman Ismagilov
3 min readApr 29, 2024

--

By default, if you are using InkWells to handle tap events in your UI, a ripple effect will be applied. The issue here is that while the ripple effect is an essential part of Android design, it seems somewhat out of place in the iOS world.

Instead, it’s more common for buttons to simply fade out the content. Let’s see how we can imlpement in Flutter:

First, let’s create a widget, that will be used instead of InkWell in the project.

class TapArea extends StatefulWidget {
final Widget child;
final GestureTapCallback? onTap;
final GestureTapCallback? onLongTap;
final double borderRadius;
final EdgeInsets? padding;

const TapArea({
super.key,
required this.child,
required this.onTap,
this.onLongTap,
this.padding,
this.borderRadius = 0,
});

@override
State<TapArea> createState() => defaultTargetPlatform == TargetPlatform.iOS
? _TapAreaIosState()
: _TapAreaAndroidState();
}

You can pass as many GestureTapCallbacks as needed, in my experience having only onTap and onLongTap is usually enough.

In case you want to support different radii, pass a BorderRadiusGeometry object instead of a double.

It is a very common mistake that some developers make: wrap a clickable area in a padding instead of doing vice verca. While it may work well when clicking UI elements with a mouse during development, it can be tricky when used on a real device. To avoid that type of mistake it might be helpful to pass a padding directly to our widget.

Let’s implement Android part. It is pretty straight-forward:

class _TapAreaAndroidState extends State<TapArea> {
@override
Widget build(BuildContext context) {
final content = Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: widget.child,
);

if (widget.onTap == null && widget.onLongTap == null) return content;

return Material(
color: Colors.transparent,
clipBehavior: Clip.none,
borderRadius: BorderRadius.circular(widget.borderRadius),
child: InkWell(
borderRadius: BorderRadius.circular(widget.borderRadius),
onTap: widget.onTap,
onLongPress: widget.onLongTap,
child: content,
),
);
}
}

When it comes to iOS, we should use GestureDetector isntead of InkWell and add a desired tap effect. In my case I’m changing the opacity to 0.7.

class _TapAreaIosState extends State<TapArea> {
bool _isDown = false;

@override
Widget build(BuildContext context) {
final content = Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: widget.child,
);

if (widget.onTap == null && widget.onLongTap == null) return content;

return GestureDetector(
onTapDown: (_) {
setState(() {
_isDown = true;
});
},
onTapCancel: () {
setState(() {
_isDown = false;
});
},
onTap: () {
setState(() {
_isDown = false;
});
widget.onTap!();
},
onLongPress: widget.onLongTap,
child: Focus(
child: Opacity(
opacity: _isDown ? 0.7 : 1.0,
child: content,
),
),
);
}
}

Why not use CupertinoButton you might ask. There are 2 problems:

Here’s the demostration how it looks like:

Code example could be found here: https://github.com/Pomis/flutter_native_ui_examples/tree/main/lib/1_tap_effects

Hope you found this article useful. I’m planning to cover as many platform differences as possible in this blog, follow me to stay updated.

--

--

Roman Ismagilov

Covering some non-obvious nuances of Flutter development in my articles