Flutter. Примеры использования виджета InkWell, которых вы не видели
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

InkWell — один из самых полезных виджетов Flutter. Он используется для добавления эффектов риппла и обработки взаимодействий с иначе неинтерактивными…
ВИДЖЕТ МЕСЯЦА
InkWell — один из самых полезных виджетов Flutter. Он используется для добавления эффектов риппла и обработки взаимодействий с иначе неинтерактивными виджетами.
Содержание
· Ключевые особенности InkWell
· Кастомизированные виджеты с InkWell
∘∘∘ Кастомизированные кнопки
∘∘∘ Интерактивные карточки
∘∘∘ Интерактивные элементы списка
∘∘∘ Кнопки с изображениями
∘∘∘ IconButton с обработчиком onDoubleTap
· InkWell vs InkResponse
· InkWell vs Ink
Если вы являетесь участником, пожалуйста, продолжайте;иначе, прочитайте полную статью здесь.
Ключевые особенности InkWell
- Создает эффекты всплеска в стиле Material Design при нажатии
- Обрабатывает различные жесты (нажатие, двойное нажатие, долгий тап)
- Работает внутри виджетов Material для отображения правильной визуальной обратной связи
- Может быть настроен с разными цветами и формами всплеска
Кастомизированные виджеты с InkWell
Кастомизированные кнопки
Кастомизированные кнопки с уникальным дизайном, которые поддерживают правильную тактильную обратную связь.

class EngageButton extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
final VoidCallback onTap;
const EngageButton({
super.key,
required this.icon,
required this.label,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
splashColor: color.withValues(alpha: 0.3),
highlightColor: color.withValues(alpha: 0.1),
child: Container(
padding: const EdgeInsets.all(12),
child: Icon(icon, color: color, size: 28),
),
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(color: color, fontWeight: FontWeight.bold),
),
],
);
}
}
Используйте их из представления:
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
EngageButton(
icon: Icons.favorite,
label: 'Like',
color: Colors.red,
onTap: () =>(){},
),
EngageButton(
icon: Icons.share,
label: 'Share',
color: Colors.blue,
onTap: () => (){},
),
EngageButton(
icon: Icons.comment,
label: 'Comment',
color: Colors.green,
onTap: () =>(){},
),
],
),
Интерактивные карточки
Нажимаемые виджеты карточек с визуальной обратной связью

class InteractiveCard extends StatelessWidget {
final String title;
final String description;
final IconData icon;
final VoidCallback onTap;
const InteractiveCard({
super.key,
required this.title,
required this.description,
required this.icon,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
elevation: 2,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: Colors.blue, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
),
);
}
}
Используйте из представления:
InteractiveCard(
title: 'Утренний ритуал',
description: 'Простые шаги, чтобы правильно начать день',
icon: Icons.wb_sunny,
onTap: () =>(){},
),
const SizedBox(height: 16),
InteractiveCard(
title: 'Вечернее размышление',
description: 'Оцените свой день и планируйте завтрашний',
icon: Icons.nightlight_round,
onTap: () => (){},
),
Интерактивные элементы списка
Список с пользовательскими макетами, но стандартными шаблонами взаимодействия.

class InteractiveListItem extends StatelessWidget {
final Widget leading;
final String title;
final String subtitle;
final Widget trailing;
final VoidCallback onTap;
const InteractiveListItem({
super.key,
required this.leading,
required this.title,
required this.subtitle,
required this.trailing,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
child: Row(
children: [
leading,
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
trailing,
],
),
),
),
);
}
}
Используйте из представления:
InteractiveListItem(
leading: const CircleAvatar(
backgroundColor: Colors.purple,
child: Icon(Icons.person, color: Colors.white),
),
title: 'Артем Новиков',
subtitle: 'Дизайнер UX',
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () =>(){},
),
const Divider(),
InteractiveListItem(
leading: const CircleAvatar(
backgroundColor: Colors.orange,
child: Icon(Icons.person, color: Colors.white),
),
title: 'Юрий Новиков',
subtitle: 'Разработчик ПО',
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: (){},
),
Кнопки с изображениями
Изображения с эффектом рябления и обработчиком нажатия.

class ImageButton extends StatelessWidget {
final ImageProvider imageProvider;
final String label;
final VoidCallback onTap;
const ImageButton({
super.key,
required this.imageProvider,
required this.label,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
elevation: 4,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Ink.image(
image: imageProvider,
width: 120,
height: 120,
fit: BoxFit.cover,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
color: Colors.black.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
);
}
}
Используйте их из представления:
ImageButton(
imageProvider: const NetworkImage(
'https://images.unsplash.com/photo-1472396961693-142e6e269027?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8bmF0dXJlfGVufDB8fDB8fHwy'
),
label: 'Природа',
onTap: (){},
),
ImageButton(
imageProvider: const NetworkImage(
'https://images.unsplash.com/photo-1575550959106-5a7defe28b56?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
),
label: 'Дикая природа',
onTap: (){},
),
Кнопка с иконкой и обработчиком двойного нажатия
Кнопка для воспроизведения аудио, которая воспроизводит с нормальной скоростью onTap, быстрой onDoubleTapи медленной onLongPress.

class AudioIconButton extends StatelessWidget {
final bool isPlaying;
final VoidCallback onTap;
final VoidCallback onDoubleTap;
final VoidCallback onLongPress;
final double size;
final Color color;
const AudioIconButton({
super.key,
required this.isPlaying,
required this.onTap,
required this.onDoubleTap,
required this.onLongPress,
this.size = 60.0,
this.color = Colors.purple,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: Material(
borderRadius: BorderRadius.circular(16),
color: color.withValues(alpha: 0.1),
child: InkWell(
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(16),
splashColor: color.withValues(alpha: 0.3),
highlightColor: color.withValues(alpha: 0.1),
child: Container(
padding: const EdgeInsets.all(12),
child: Center(
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: color,
size: 28,
),
),
),
),
),
);
}
}
Представление:
GetBuilder<AudioController>(
id: 'play',
builder: (controller) {
return AudioIconButton(
color: Colors.blue,
isPlaying: controller.isPlaying,
onTap: () {
controller.handleTap();
},
onDoubleTap: () {
controller.handleDoubleTap();
},
onLongPress: () {
controller.handleLongPress();
});
}
),
Контроллер:
bool isPlaying = false;
void handleTap() {
isPlaying = !isPlaying;
var message = 'Playing is stopped';
if (isPlaying) {
message = 'Playing with normal speed';
}
Get.showSnackbar(GetSnackBar(
message: message,
duration: Duration(seconds: 1),
));
update(['play']);
}
void handleDoubleTap() {
isPlaying = !isPlaying;
var message = 'Playing is stopped';
if (isPlaying) {
message = 'Playing fast';
}
Get.showSnackbar(GetSnackBar(
message: message,
duration: Duration(seconds: 1),
));
update(['play']);
}
void handleLongPress() {
isPlaying = !isPlaying;
var message = 'Playing is stopped';
if (isPlaying) {
message = 'Playing slowly';
}
Get.showSnackbar(GetSnackBar(
message: message,
duration: Duration(seconds: 1),
));
update(['play']);
}
InkWell против InkResponse
InkWell и InkResponse очень похожи. Оба реализуют InteractiveInkFeature и находятся в библиотеке ink_well.dart.
Разница в том, что InkResponse позволяет больше настроить форму выделения, в то время как InkWell всегда создает прямоугольную.
Все приведенные выше примеры можно переписать с использованием InkResponse.
InkWell против Ink
Эти два класса служат совершенно разным целям, но часто используются вместе.
В то время как InkWell позволяет взаимодействовать и предоставляет визуальную обратную связь в виде всплеска и выделения, Ink позволяет рисовать виджеты на подлежащем Material, на котором всплеск и выделение, предоставленные InkWell были бы видны.
Мы уже использовали Ink для наших ImageButtons.
Material(
elevation: 4,
borderRadius: BorderRadius.circular(12),
child: Ink.image(
image: imageProvider,
width: 120,
height: 120,
fit: BoxFit.cover,
child: InkWell(
Спасибо за чтение!