I am trying to implement a whatsapp like chat box with voice message feature exactly like in whatsapp, the ui design for voice message is working fine, but the delete,send,pause button inside the Positioned widgets are not working(these buttons used when recording audio using the lock concept exactly in whatsapp) when I click these buttons or inside the Positioned widgets the keyboard pops up for the underlying message box, How can I avoid this and make the buttons working when click.
below is my code ;
main.dart :
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Audio Chat"),
),
body: Padding(
padding: const EdgeInsets.all(Globals.defaultPadding),
child: Column(
children: [
const Expanded(child: AudioList()),
Row(
mainAxisSize: MainAxisSize.max,
children: [
ChatBox(controller: controller),
const SizedBox(width: 4),
RecordButton(controller: controller),
],
),
],
),
),
);
}
}
record_button.dart :
class _RecordButtonState extends State<RecordButton> {
static const double size = 55;
final double lockerHeight = 200;
double timerWidth = 0;
late Animation<double> buttonScaleAnimation;
late Animation<double> timerAnimation;
late Animation<double> lockerAnimation;
DateTime? startTime;
Timer? timer;
String recordDuration = "00:00";
late Record record;
bool isLocked = false;
bool showLottie = false;
@override
void initState() {
super.initState();
buttonScaleAnimation = Tween<double>(begin: 1, end: 2).animate(
CurvedAnimation(
parent: widget.controller,
curve: const Interval(0.0, 0.6, curve: Curves.elasticInOut),
),
);
widget.controller.addListener(() {
setState(() {});
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
timerWidth = MediaQuery.of(context).size.width - 2 * Globals.defaultPadding - 4 + 5;
timerAnimation =
Tween<double>(begin: timerWidth + Globals.defaultPadding, end: 0)
.animate(
CurvedAnimation(
parent: widget.controller,
curve: const Interval(0.2, 1, curve: Curves.easeIn),
),
);
lockerAnimation =
Tween<double>(begin: lockerHeight + Globals.defaultPadding, end: 0)
.animate(
CurvedAnimation(
parent: widget.controller,
curve: const Interval(0.2, 1, curve: Curves.easeIn),
),
);
}
@override
void dispose() {
record.dispose();
timer?.cancel();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
lockSlider(),
cancelSlider(),
audioButton(),
if (isLocked) timerLocked(),
],
);
}
Widget lockSlider() {
return Positioned(
bottom: -lockerAnimation.value,
child: Container(
height: lockerHeight,
width: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(Globals.borderRadius),
color: Colors.black,
),
padding: const EdgeInsets.symmetric(vertical: 15),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(FontAwesomeIcons.lock, size: 20),
const SizedBox(height: 8),
FlowShader(
direction: Axis.vertical,
child: Column(
children: const [
Icon(Icons.keyboard_arrow_up),
Icon(Icons.keyboard_arrow_up),
Icon(Icons.keyboard_arrow_up),
],
),
),
],
),
),
);
}
Widget cancelSlider() {
return Positioned(
right: -timerAnimation.value,
child: Container(
height: size,
width: timerWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(Globals.borderRadius),
color: Colors.black,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
showLottie ? const LottieAnimation() : Text(recordDuration),
const SizedBox(width: size),
FlowShader(
child: Row(
children: const [
Icon(Icons.keyboard_arrow_left),
Text("Slide to cancel")
],
),
duration: const Duration(seconds: 3),
flowColors: const [Colors.white, Colors.grey],
),
const SizedBox(width: size),
],
),
),
),
);
}
Widget timerLocked() {
return Positioned(
right: 0,
bottom: 0,
child: Container(
height: 80,
width: timerWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(Globals.borderRadius - 10),
color: Colors.lightBlue,
),
child: Padding(
padding: const EdgeInsets.only(left: 15, right: 25),
child: Column(
children: [
Flexible(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Text(recordDuration),
FlowShader(
child: const Text("Tap lock to stop"),
duration: const Duration(seconds: 3),
flowColors: const [Colors.white, Colors.grey],
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
print('Recording finished');
},
child: const Center(
child: Icon(
FontAwesomeIcons.check,
size: 18,
color: Colors.black,
),
),
),
],
),
),
Flexible(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: [
Center(
child: InkWell(
onTap: () {
print('Action Delete');
},
child: Icon(FontAwesomeIcons.trash,size: 18,color: Colors.black,),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
print('Action Pause');
},
child: const Center(
child: Icon(
FontAwesomeIcons.pause,
size: 18,
color: Colors.black,
),
),
),
],
),
),
],
),
),
),
);
}
Widget audioButton() {
return GestureDetector(
child: Transform.scale(
scale: buttonScaleAnimation.value,
child: Container(
child: const Icon(Icons.mic),
height: size,
width: size,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).primaryColor,
),
),
),
onLongPressDown: (_) {
widget.controller.forward();
},
onLongPressEnd: (details) async {
if (isCancelled(details.localPosition, context)) {
} else if (checkIsLocked(details.localPosition)) {
widget.controller.reverse();
Vibrate.feedback(FeedbackType.heavy);
setState(() {
isLocked = true;
});
} else {
print('Recording Finished');
}
},
onLongPressCancel: () {
widget.controller.reverse();
},
onLongPress: () async {
Vibrate.feedback(FeedbackType.success);
if (await Record().hasPermission()) {
record = Record();
await record.start(
path: Globals.documentPath +
"audio_${DateTime.now().millisecondsSinceEpoch}.m4a",
encoder: AudioEncoder.aacEld,
bitRate: 128000,
samplingRate: 44100,
);
startTime = DateTime.now();
timer = Timer.periodic(const Duration(seconds: 1), (_) {
final minDur = DateTime.now().difference(startTime!).inMinutes;
final secDur = DateTime.now().difference(startTime!).inSeconds % 60;
String min = minDur < 10 ? "0$minDur" : minDur.toString();
String sec = secDur < 10 ? "0$secDur" : secDur.toString();
setState(() {
recordDuration = "$min:$sec";
});
});
}
},
);
}
bool checkIsLocked(Offset offset) {
return (offset.dy < -35);
}
bool isCancelled(Offset offset, BuildContext context) {
return (offset.dx < -(MediaQuery.of(context).size.width * 0.2));
}
}
UPDATE :
The only work around i found till now is to enclose the timerLocked Positioned widget with Stack,SizedBox,Positioned
as below.
Widget timerLocked() {
return Positioned(
child: SizedBox(
height: 80,
width: timerWidth,
child: Stack(
children: [
Positioned(
right: 0,
bottom: 0,
height: 80,
width: timerWidth,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(Globals.borderRadius - 10),
color: Colors.lightBlue,
),
child: Padding(
padding: const EdgeInsets.only(left: 15, right: 25),
child: Column(
children: [
],
),
),),
),
],),),);
}
2
Answers
Finally I found a solution.I replaced
Positioned
widget withExpandTapWidget
(ExpandTapWidget) and that solved the problem.This was a bug/documentaion issue and is posted here which helped me to solve this.Below is the code,To fix this issue, we need to wrap the buttons with an IgnorePointer widget and make sure that the gestures are not passed to the underlying widgets.