mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-28 14:36:39 +10:00
Swipe to reply (#160)
* Add swipe to reply * format * Cleaned up code * format * remove my gitignore change - ignore this * fix * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor onHorizontalDragStart for readability fixed formating. * Fix swipe end handling in channel chat screen * Refactor swipe gesture handling in chat screen * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor swipe handling for reply functionality * Adjust swipe thresholds and logic in chat screen * Conditionally render reply bubble or padding --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Winston Lowe <wel97459@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
@@ -271,193 +272,243 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
? message.pathVariants.first
|
? message.pathVariants.first
|
||||||
: Uint8List(0));
|
: Uint8List(0));
|
||||||
|
|
||||||
return Padding(
|
const maxSwipeOffset = 64.0;
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
const replySwipeThreshold = 64.0;
|
||||||
child: Column(
|
final messageBody = Column(
|
||||||
crossAxisAlignment: isOutgoing
|
crossAxisAlignment: isOutgoing
|
||||||
? CrossAxisAlignment.end
|
? CrossAxisAlignment.end
|
||||||
: CrossAxisAlignment.start,
|
: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: isOutgoing
|
mainAxisAlignment: isOutgoing
|
||||||
? MainAxisAlignment.end
|
? MainAxisAlignment.end
|
||||||
: MainAxisAlignment.start,
|
: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!isOutgoing) ...[
|
if (!isOutgoing) ...[
|
||||||
_buildAvatar(message.senderName),
|
_buildAvatar(message.senderName),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
Flexible(
|
Flexible(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _showMessagePathInfo(message),
|
onTap: () => _showMessagePathInfo(message),
|
||||||
onLongPress: () => _showMessageActions(message),
|
onLongPress: () => _showMessageActions(message),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: gifId != null
|
padding: gifId != null
|
||||||
? const EdgeInsets.all(4)
|
? const EdgeInsets.all(4)
|
||||||
: const EdgeInsets.symmetric(
|
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
horizontal: 12,
|
constraints: BoxConstraints(
|
||||||
vertical: 8,
|
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints(
|
decoration: BoxDecoration(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
color: isOutgoing
|
||||||
),
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
decoration: BoxDecoration(
|
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
color: isOutgoing
|
borderRadius: BorderRadius.circular(12),
|
||||||
? Theme.of(context).colorScheme.primaryContainer
|
),
|
||||||
: Theme.of(
|
child: Column(
|
||||||
context,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
).colorScheme.surfaceContainerHighest,
|
children: [
|
||||||
borderRadius: BorderRadius.circular(12),
|
if (!isOutgoing) ...[
|
||||||
),
|
Padding(
|
||||||
child: Column(
|
padding: gifId != null
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
? const EdgeInsets.only(
|
||||||
children: [
|
left: 8,
|
||||||
if (!isOutgoing) ...[
|
top: 4,
|
||||||
Padding(
|
bottom: 4,
|
||||||
padding: gifId != null
|
)
|
||||||
? const EdgeInsets.only(
|
: EdgeInsets.zero,
|
||||||
left: 8,
|
child: Text(
|
||||||
top: 4,
|
message.senderName,
|
||||||
bottom: 4,
|
style: TextStyle(
|
||||||
)
|
fontSize: 12,
|
||||||
: EdgeInsets.zero,
|
fontWeight: FontWeight.bold,
|
||||||
child: Text(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
message.senderName,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (gifId == null) const SizedBox(height: 4),
|
),
|
||||||
],
|
if (gifId == null) const SizedBox(height: 4),
|
||||||
if (message.replyToMessageId != null) ...[
|
],
|
||||||
_buildReplyPreview(message),
|
if (message.replyToMessageId != null) ...[
|
||||||
const SizedBox(height: 8),
|
_buildReplyPreview(message),
|
||||||
],
|
const SizedBox(height: 8),
|
||||||
if (poi != null)
|
],
|
||||||
_buildPoiMessage(context, poi, isOutgoing)
|
if (poi != null)
|
||||||
else if (gifId != null)
|
_buildPoiMessage(context, poi, isOutgoing)
|
||||||
ClipRRect(
|
else if (gifId != null)
|
||||||
borderRadius: BorderRadius.circular(8),
|
ClipRRect(
|
||||||
child: GifMessage(
|
borderRadius: BorderRadius.circular(8),
|
||||||
url:
|
child: GifMessage(
|
||||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
url:
|
||||||
backgroundColor: Colors.transparent,
|
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||||
fallbackTextColor: isOutgoing
|
backgroundColor: Colors.transparent,
|
||||||
? Theme.of(context)
|
fallbackTextColor: isOutgoing
|
||||||
.colorScheme
|
? Theme.of(context)
|
||||||
.onPrimaryContainer
|
.colorScheme
|
||||||
.withValues(alpha: 0.7)
|
.onPrimaryContainer
|
||||||
: Theme.of(context).colorScheme.onSurface
|
.withValues(alpha: 0.7)
|
||||||
.withValues(alpha: 0.6),
|
: Theme.of(context).colorScheme.onSurface
|
||||||
),
|
.withValues(alpha: 0.6),
|
||||||
)
|
|
||||||
else
|
|
||||||
Linkify(
|
|
||||||
text: message.text,
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
linkStyle: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.green,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
options: const LinkifyOptions(
|
|
||||||
humanize: false,
|
|
||||||
defaultToHttps: false,
|
|
||||||
),
|
|
||||||
linkifiers: const [UrlLinkifier()],
|
|
||||||
onOpen: (link) =>
|
|
||||||
LinkHandler.handleLinkTap(context, link.url),
|
|
||||||
),
|
),
|
||||||
if (displayPath.isNotEmpty) ...[
|
)
|
||||||
const SizedBox(height: 4),
|
else
|
||||||
Padding(
|
Linkify(
|
||||||
padding: gifId != null
|
text: message.text,
|
||||||
? const EdgeInsets.symmetric(horizontal: 8)
|
style: const TextStyle(fontSize: 14),
|
||||||
: EdgeInsets.zero,
|
linkStyle: const TextStyle(
|
||||||
child: Text(
|
fontSize: 14,
|
||||||
'via ${_formatPathPrefixes(displayPath)}',
|
color: Colors.green,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
options: const LinkifyOptions(
|
||||||
|
humanize: false,
|
||||||
|
defaultToHttps: false,
|
||||||
|
),
|
||||||
|
linkifiers: const [UrlLinkifier()],
|
||||||
|
onOpen: (link) =>
|
||||||
|
LinkHandler.handleLinkTap(context, link.url),
|
||||||
|
),
|
||||||
|
if (displayPath.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: gifId != null
|
||||||
|
? const EdgeInsets.symmetric(horizontal: 8)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: Text(
|
||||||
|
'via ${_formatPathPrefixes(displayPath)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: gifId != null
|
||||||
|
? const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 4,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatTime(message.timestamp),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (message.repeatCount > 0) ...[
|
||||||
],
|
const SizedBox(width: 6),
|
||||||
const SizedBox(height: 4),
|
Icon(
|
||||||
Padding(
|
Icons.repeat,
|
||||||
padding: gifId != null
|
size: 12,
|
||||||
? const EdgeInsets.only(
|
color: Colors.grey[600],
|
||||||
left: 8,
|
),
|
||||||
right: 8,
|
const SizedBox(width: 2),
|
||||||
bottom: 4,
|
|
||||||
)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
Text(
|
||||||
_formatTime(message.timestamp),
|
'${message.repeatCount}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message.repeatCount > 0) ...[
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Icon(
|
|
||||||
Icons.repeat,
|
|
||||||
size: 12,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
'${message.repeatCount}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (isOutgoing) ...[
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Icon(
|
|
||||||
message.status == ChannelMessageStatus.sent
|
|
||||||
? Icons.check
|
|
||||||
: message.status ==
|
|
||||||
ChannelMessageStatus.pending
|
|
||||||
? Icons.schedule
|
|
||||||
: Icons.error_outline,
|
|
||||||
size: 14,
|
|
||||||
color:
|
|
||||||
message.status ==
|
|
||||||
ChannelMessageStatus.failed
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey[600],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
if (isOutgoing) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Icon(
|
||||||
|
message.status == ChannelMessageStatus.sent
|
||||||
|
? Icons.check
|
||||||
|
: message.status ==
|
||||||
|
ChannelMessageStatus.pending
|
||||||
|
? Icons.schedule
|
||||||
|
: Icons.error_outline,
|
||||||
|
size: 14,
|
||||||
|
color:
|
||||||
|
message.status ==
|
||||||
|
ChannelMessageStatus.failed
|
||||||
|
? Colors.red
|
||||||
|
: Colors.grey[600],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
if (message.reactions.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
|
||||||
child: _buildReactionsDisplay(message),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
if (message.reactions.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
||||||
|
child: _buildReactionsDisplay(message),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOutgoing) {
|
||||||
|
return _SwipeReplyBubble(
|
||||||
|
maxSwipeOffset: maxSwipeOffset,
|
||||||
|
replySwipeThreshold: replySwipeThreshold,
|
||||||
|
onReplyTriggered: () => _setReplyingTo(message),
|
||||||
|
hintBuilder: ({required isStart}) =>
|
||||||
|
_buildReplySwipeHint(isStart: isStart),
|
||||||
|
child: messageBody,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: messageBody,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReplySwipeHint({required bool isStart}) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final content = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.reply, color: colorScheme.primary),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
context.l10n.chat_reply,
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
alignment: isStart ? Alignment.centerLeft : Alignment.centerRight,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
color: colorScheme.primary.withValues(alpha: 0.08),
|
||||||
|
child: isStart
|
||||||
|
? content
|
||||||
|
: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.l10n.chat_reply,
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Icon(Icons.reply, color: colorScheme.primary),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1007,6 +1058,157 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SwipeReplyBubble extends StatefulWidget {
|
||||||
|
final double maxSwipeOffset;
|
||||||
|
final double replySwipeThreshold;
|
||||||
|
final VoidCallback onReplyTriggered;
|
||||||
|
final Widget Function({required bool isStart}) hintBuilder;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _SwipeReplyBubble({
|
||||||
|
required this.maxSwipeOffset,
|
||||||
|
required this.replySwipeThreshold,
|
||||||
|
required this.onReplyTriggered,
|
||||||
|
required this.hintBuilder,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SwipeReplyBubble> createState() => _SwipeReplyBubbleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||||
|
Offset? _swipeStartPosition;
|
||||||
|
double _swipeOffset = 0;
|
||||||
|
double _maxSwipeDistance = 0;
|
||||||
|
int? _swipePointerId;
|
||||||
|
bool _swipeLockedToHorizontal = false;
|
||||||
|
|
||||||
|
void _handleSwipeStart(Offset position) {
|
||||||
|
_swipeStartPosition = position;
|
||||||
|
_maxSwipeDistance = 0;
|
||||||
|
if (_swipeOffset != 0) {
|
||||||
|
setState(() => _swipeOffset = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipePointerDown(PointerDownEvent event) {
|
||||||
|
_swipePointerId = event.pointer;
|
||||||
|
_swipeLockedToHorizontal = false;
|
||||||
|
_handleSwipeStart(event.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipePointerMove(PointerMoveEvent event) {
|
||||||
|
if (_swipePointerId != event.pointer || _swipeStartPosition == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dx = event.position.dx - _swipeStartPosition!.dx;
|
||||||
|
|
||||||
|
const axisLockThreshold = 12.0;
|
||||||
|
if (!_swipeLockedToHorizontal) {
|
||||||
|
if (-dx < axisLockThreshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_swipeLockedToHorizontal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSwipeUpdate(event.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipeUpdate(Offset position) {
|
||||||
|
if (_swipeStartPosition == null) return;
|
||||||
|
|
||||||
|
final dx = position.dx - _swipeStartPosition!.dx;
|
||||||
|
if (dx >= 0) return;
|
||||||
|
|
||||||
|
if (-dx < 6) return;
|
||||||
|
|
||||||
|
if (-dx > _maxSwipeDistance) {
|
||||||
|
_maxSwipeDistance = -dx;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble();
|
||||||
|
final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
|
||||||
|
if (adjusted != _swipeOffset) {
|
||||||
|
setState(() => _swipeOffset = adjusted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipePointerUp(Offset position) {
|
||||||
|
if (_swipeLockedToHorizontal && _swipeStartPosition != null) {
|
||||||
|
final dx = position.dx - _swipeStartPosition!.dx;
|
||||||
|
final peak = math.max(
|
||||||
|
_maxSwipeDistance,
|
||||||
|
(-dx).clamp(0.0, double.infinity),
|
||||||
|
);
|
||||||
|
if (peak >= widget.replySwipeThreshold) {
|
||||||
|
widget.onReplyTriggered();
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_resetSwipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetSwipe() {
|
||||||
|
if (_swipeOffset != 0) {
|
||||||
|
setState(() => _swipeOffset = 0);
|
||||||
|
}
|
||||||
|
_swipeStartPosition = null;
|
||||||
|
_maxSwipeDistance = 0;
|
||||||
|
_swipePointerId = null;
|
||||||
|
_swipeLockedToHorizontal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _applySwipeResistance(double rawOffset, double maxOffset) {
|
||||||
|
final abs = rawOffset.abs();
|
||||||
|
if (abs <= 0) return 0;
|
||||||
|
final norm = (abs / maxOffset).clamp(0.0, 1.0);
|
||||||
|
const deadZone = 0.18;
|
||||||
|
if (norm <= deadZone) {
|
||||||
|
return rawOffset.sign * maxOffset * (norm * 0.08);
|
||||||
|
}
|
||||||
|
final t = ((norm - deadZone) / (1 - deadZone)).clamp(0.0, 1.0);
|
||||||
|
final curved = t < 0.5
|
||||||
|
? 16 * math.pow(t, 5)
|
||||||
|
: 1 - math.pow(-2 * t + 2, 5) / 2;
|
||||||
|
const deadZoneEnd = 0.0144;
|
||||||
|
return rawOffset.sign *
|
||||||
|
maxOffset *
|
||||||
|
(deadZoneEnd + curved * (1 - deadZoneEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: _handleSwipePointerDown,
|
||||||
|
onPointerMove: _handleSwipePointerMove,
|
||||||
|
onPointerUp: (event) => _handleSwipePointerUp(event.position),
|
||||||
|
onPointerCancel: (_) => _resetSwipe(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
|
||||||
|
child: widget.hintBuilder(isStart: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
transform: Matrix4.translationValues(_swipeOffset, 0, 0),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _PoiInfo {
|
class _PoiInfo {
|
||||||
final double lat;
|
final double lat;
|
||||||
final double lon;
|
final double lon;
|
||||||
|
|||||||
Reference in New Issue
Block a user