feat(contacts): окно обмена сверху + анимация получения контакта
This commit is contained in:
@@ -36,13 +36,26 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openNfcExchange() async {
|
Future<void> _openNfcExchange() async {
|
||||||
final cs = Theme.of(context).colorScheme;
|
await showGeneralDialog<void>(
|
||||||
await showModalBottomSheet<void>(
|
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
barrierDismissible: true,
|
||||||
backgroundColor: cs.surfaceContainerHigh,
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
shape: kSheetShape,
|
barrierColor: Colors.black54,
|
||||||
builder: (_) => const NfcExchangeSheet(),
|
transitionDuration: const Duration(milliseconds: 320),
|
||||||
|
pageBuilder: (_, _, _) => const Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: NfcExchangeSheet(),
|
||||||
|
),
|
||||||
|
transitionBuilder: (_, anim, _, child) {
|
||||||
|
final curved = CurvedAnimation(parent: anim, curve: Curves.easeOutCubic);
|
||||||
|
return SlideTransition(
|
||||||
|
position: Tween(
|
||||||
|
begin: const Offset(0, -1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(curved),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
import '../../../backend/modules/contacts.dart';
|
import '../../../backend/modules/contacts.dart';
|
||||||
@@ -32,9 +33,10 @@ class NfcExchangeSheet extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
||||||
with SingleTickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
final _nfc = NfcExchangeService.instance;
|
final _nfc = NfcExchangeService.instance;
|
||||||
late final AnimationController _pulse;
|
late final AnimationController _pulse;
|
||||||
|
late final AnimationController _reveal;
|
||||||
StreamSubscription<NfcEvent>? _sub;
|
StreamSubscription<NfcEvent>? _sub;
|
||||||
|
|
||||||
_Stage _stage = _Stage.checking;
|
_Stage _stage = _Stage.checking;
|
||||||
@@ -49,6 +51,10 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 1800),
|
duration: const Duration(milliseconds: 1800),
|
||||||
)..repeat();
|
)..repeat();
|
||||||
|
_reveal = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1100),
|
||||||
|
);
|
||||||
_begin();
|
_begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +63,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
_sub?.cancel();
|
_sub?.cancel();
|
||||||
_nfc.stop();
|
_nfc.stop();
|
||||||
_pulse.dispose();
|
_pulse.dispose();
|
||||||
|
_reveal.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +114,8 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
final id = event.id;
|
final id = event.id;
|
||||||
if (id == null || _peerId != null) return;
|
if (id == null || _peerId != null) return;
|
||||||
_peerId = id;
|
_peerId = id;
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
_reveal.forward(from: 0);
|
||||||
setState(() => _stage = _Stage.found);
|
setState(() => _stage = _Stage.found);
|
||||||
final info = await ContactInfoFetch.get(id);
|
final info = await ContactInfoFetch.get(id);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -181,13 +190,22 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
return SafeArea(
|
return SizedBox(
|
||||||
child: Padding(
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
child: Material(
|
||||||
child: Column(
|
color: cs.surfaceContainerHigh,
|
||||||
mainAxisSize: MainAxisSize.min,
|
shape: const RoundedRectangleBorder(
|
||||||
children: [
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(28)),
|
||||||
Row(
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 14, 20, 24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -219,9 +237,11 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
child: _buildContent(cs),
|
child: _buildContent(cs),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,11 +376,28 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
KometAvatar(
|
SizedBox(
|
||||||
name: _peerName(),
|
width: 160,
|
||||||
imageUrl: _peerInfo?['baseUrl'] as String?,
|
height: 160,
|
||||||
size: 88,
|
child: AnimatedBuilder(
|
||||||
fontSize: 34,
|
animation: _reveal,
|
||||||
|
builder: (context, child) {
|
||||||
|
final t = _reveal.value;
|
||||||
|
final pop = Curves.elasticOut.transform(t.clamp(0.0, 1.0));
|
||||||
|
return CustomPaint(
|
||||||
|
painter: _BurstPainter(t, cs.primary),
|
||||||
|
child: Center(
|
||||||
|
child: Transform.scale(scale: pop, child: child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: KometAvatar(
|
||||||
|
name: _peerName(),
|
||||||
|
imageUrl: _peerInfo?['baseUrl'] as String?,
|
||||||
|
size: 92,
|
||||||
|
fontSize: 34,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
Text(
|
Text(
|
||||||
@@ -435,3 +472,41 @@ class _RadarPainter extends CustomPainter {
|
|||||||
bool shouldRepaint(_RadarPainter oldDelegate) =>
|
bool shouldRepaint(_RadarPainter oldDelegate) =>
|
||||||
oldDelegate.progress != progress || oldDelegate.color != color;
|
oldDelegate.progress != progress || oldDelegate.color != color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BurstPainter extends CustomPainter {
|
||||||
|
final double progress;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
_BurstPainter(this.progress, this.color);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
if (progress <= 0) return;
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final maxRadius = size.width / 2;
|
||||||
|
final eased = Curves.easeOut.transform(progress.clamp(0.0, 1.0));
|
||||||
|
|
||||||
|
final glow = Paint()
|
||||||
|
..color = color.withValues(alpha: (1.0 - eased) * 0.18);
|
||||||
|
canvas.drawCircle(center, maxRadius * (0.45 + 0.55 * eased), glow);
|
||||||
|
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
final delay = i * 0.18;
|
||||||
|
final t = ((progress - delay) / (1.0 - delay)).clamp(0.0, 1.0);
|
||||||
|
if (t <= 0) continue;
|
||||||
|
final wave = Curves.easeOut.transform(t);
|
||||||
|
final radius = maxRadius * (0.3 + 0.7 * wave);
|
||||||
|
final opacity = (1.0 - wave) * 0.5;
|
||||||
|
if (opacity <= 0) continue;
|
||||||
|
final ring = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 2.5 * (1.0 - wave) + 0.5
|
||||||
|
..color = color.withValues(alpha: opacity);
|
||||||
|
canvas.drawCircle(center, radius, ring);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_BurstPainter oldDelegate) =>
|
||||||
|
oldDelegate.progress != progress || oldDelegate.color != color;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user