gesturesshare 0.0.1
gesturesshare: ^0.0.1 copied to clipboard
Flutter 隔空传送分享插件,支持 HarmonyOS/OpenHarmony。通过系统 ShareKit 注册隔空传送监听,用户触发手势时自动分享指定图片与 App Linking。
example/lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:gesturesshare/gesturesshare.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: Colors.red,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
/// 首页 - 选择测试场景
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('隔空传送分享测试')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_ScenarioCard(
icon: Icons.card_giftcard,
color: Colors.red,
title: '分享福卡',
subtitle: '模拟隔空传送赠送福卡场景(图片 + App Linking)',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ShareCardPage()),
),
),
const SizedBox(height: 12),
_ScenarioCard(
icon: Icons.image,
color: Colors.blue,
title: '分享图片',
subtitle: '仅分享一张本地图片(无链接)',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ShareImagePage()),
),
),
const SizedBox(height: 12),
_ScenarioCard(
icon: Icons.tune,
color: Colors.teal,
title: '自定义参数',
subtitle: '手动填写所有参数进行测试',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CustomTestPage()),
),
),
],
),
);
}
}
class _ScenarioCard extends StatelessWidget {
final IconData icon;
final Color color;
final String title;
final String subtitle;
final VoidCallback onTap;
const _ScenarioCard({
required this.icon,
required this.color,
required this.title,
required this.subtitle,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(subtitle,
style:
TextStyle(fontSize: 13, color: Colors.grey[600])),
],
),
),
Icon(Icons.chevron_right, color: Colors.grey[400]),
],
),
),
),
);
}
}
// ============================================================
// 场景一:分享福卡(模拟 uni-app 示例)
// ============================================================
class ShareCardPage extends StatefulWidget {
const ShareCardPage({super.key});
@override
State<ShareCardPage> createState() => _ShareCardPageState();
}
class _ShareCardPageState extends State<ShareCardPage> {
final _plugin = Gesturesshare();
StreamSubscription<GesturesShareResult>? _sub;
bool _isSharing = false;
bool _isLoading = false;
String _statusText = '点击按钮注册隔空传送分享';
@override
void initState() {
super.initState();
_sub = _plugin.onShareTriggered.listen((result) {
if (!mounted) return;
if (result.success) {
_showSnackBar('分享成功!对方即将收到福卡', Colors.green);
} else {
_showSnackBar('分享失败: ${result.errMsg}', Colors.red);
}
});
}
@override
void dispose() {
_sub?.cancel();
// 页面销毁时注销(与 uni-app onHide 行为一致)
if (_isSharing) {
_plugin.unregisterGesturesShare();
}
super.dispose();
}
void _showSnackBar(String msg, Color color) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
backgroundColor: color,
behavior: SnackBarBehavior.floating,
),
);
}
/// 分享福卡 - 对应 uni-app 中的 shareCard()
Future<void> _shareCard() async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
await _plugin.registerGesturesShare(
RegisterGesturesShareOptions(
imagePath: '/data/storage/el2/base/haps/entry/files/card.jpg',
appLink: 'https://babyone.ljcljc.cn/api/festival/receive?uid=fc_xxx',
title: '赠送福卡',
description: '快来接收吧!',
),
);
setState(() {
_isSharing = true;
_statusText = '已注册,请使用隔空传送手势分享';
});
_showSnackBar('请使用隔空传送手势分享', Colors.blue);
} on PlatformException catch (e) {
setState(() => _statusText = '注册失败: ${e.message}');
_showSnackBar('隔空传送功能不可用', Colors.red);
} catch (e) {
setState(() => _statusText = '注册失败: $e');
_showSnackBar('隔空传送功能不可用', Colors.red);
} finally {
setState(() => _isLoading = false);
}
}
/// 取消分享 - 对应 uni-app 中 onHide 的注销
Future<void> _cancelShare() async {
if (!_isSharing) return;
setState(() => _isLoading = true);
try {
await _plugin.unregisterGesturesShare();
setState(() {
_isSharing = false;
_statusText = '已取消分享';
});
} catch (e) {
_showSnackBar('取消失败: $e', Colors.red);
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('分享福卡')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// 福卡预览
Card(
color: Colors.red[50],
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
child: Column(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.red[100],
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.red[300]!, width: 2),
),
child: Icon(Icons.card_giftcard,
size: 64, color: Colors.red[400]),
),
const SizedBox(height: 16),
Text(
'赠送福卡',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red[800],
),
),
const SizedBox(height: 8),
Text(
'快来接收吧!',
style: TextStyle(fontSize: 14, color: Colors.red[600]),
),
const SizedBox(height: 4),
Text(
'App Linking: babyone.ljcljc.cn/...',
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
),
],
),
),
),
const SizedBox(height: 24),
// 状态
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: _isSharing ? Colors.green[50] : Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
_isSharing ? Icons.check_circle : Icons.info_outline,
size: 18,
color: _isSharing ? Colors.green : Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Text(
_statusText,
style: TextStyle(
fontSize: 13,
color: _isSharing ? Colors.green[800] : Colors.grey[700],
),
),
),
],
),
),
const SizedBox(height: 24),
// 按钮
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton.icon(
onPressed: _isSharing || _isLoading ? null : _shareCard,
icon: _isLoading && !_isSharing
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.card_giftcard),
label: const Text('隔空传送分享福卡'),
style: FilledButton.styleFrom(
backgroundColor: Colors.red,
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton.icon(
onPressed: !_isSharing || _isLoading ? null : _cancelShare,
icon: const Icon(Icons.close),
label: const Text('取消分享'),
),
),
],
),
),
);
}
}
// ============================================================
// 场景二:仅分享图片
// ============================================================
class ShareImagePage extends StatefulWidget {
const ShareImagePage({super.key});
@override
State<ShareImagePage> createState() => _ShareImagePageState();
}
class _ShareImagePageState extends State<ShareImagePage> {
final _plugin = Gesturesshare();
StreamSubscription<GesturesShareResult>? _sub;
bool _isSharing = false;
bool _isLoading = false;
String _statusText = '点击按钮注册隔空传送分享图片';
@override
void initState() {
super.initState();
_sub = _plugin.onShareTriggered.listen((result) {
if (!mounted) return;
if (result.success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('图片分享成功!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('图片分享失败: ${result.errMsg}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
});
}
@override
void dispose() {
_sub?.cancel();
if (_isSharing) {
_plugin.unregisterGesturesShare();
}
super.dispose();
}
Future<void> _shareImage() async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
await _plugin.registerGesturesShare(
RegisterGesturesShareOptions(
imagePath: '/data/storage/el2/base/haps/entry/files/share_image.jpg',
),
);
setState(() {
_isSharing = true;
_statusText = '已注册,请使用隔空传送手势分享图片';
});
} on PlatformException catch (e) {
setState(() => _statusText = '注册失败: ${e.message}');
} catch (e) {
setState(() => _statusText = '注册失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _cancelShare() async {
if (!_isSharing) return;
setState(() => _isLoading = true);
try {
await _plugin.unregisterGesturesShare();
setState(() {
_isSharing = false;
_statusText = '已取消分享';
});
} catch (e) {
debugPrint('取消失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('分享图片')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// 图片预览
Card(
color: Colors.blue[50],
child: Container(
width: double.infinity,
height: 200,
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image, size: 64, color: Colors.blue[300]),
const SizedBox(height: 12),
Text(
'share_image.jpg',
style: TextStyle(fontSize: 14, color: Colors.blue[700]),
),
const SizedBox(height: 4),
Text(
'仅分享图片,不含链接',
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
],
),
),
),
const SizedBox(height: 24),
// 状态
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: _isSharing ? Colors.green[50] : Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
_isSharing ? Icons.check_circle : Icons.info_outline,
size: 18,
color: _isSharing ? Colors.green : Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Text(
_statusText,
style: TextStyle(
fontSize: 13,
color: _isSharing ? Colors.green[800] : Colors.grey[700],
),
),
),
],
),
),
const SizedBox(height: 24),
// 按钮
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton.icon(
onPressed: _isSharing || _isLoading ? null : _shareImage,
icon: _isLoading && !_isSharing
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.image),
label: const Text('隔空传送分享图片'),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton.icon(
onPressed: !_isSharing || _isLoading ? null : _cancelShare,
icon: const Icon(Icons.close),
label: const Text('取消分享'),
),
),
],
),
),
);
}
}
// ============================================================
// 场景三:自定义参数测试
// ============================================================
class CustomTestPage extends StatefulWidget {
const CustomTestPage({super.key});
@override
State<CustomTestPage> createState() => _CustomTestPageState();
}
class _CustomTestPageState extends State<CustomTestPage> {
String _platformVersion = 'Unknown';
bool _isRegistered = false;
bool _isLoading = false;
final List<String> _logs = [];
final _plugin = Gesturesshare();
StreamSubscription<GesturesShareResult>? _sub;
final _imagePathController = TextEditingController(
text: '/data/storage/el2/base/haps/entry/files/share_image.jpg',
);
final _appLinkController = TextEditingController();
final _titleController = TextEditingController(text: '隔空传送分享');
final _descriptionController =
TextEditingController(text: '来自 Flutter 的分享');
@override
void initState() {
super.initState();
_initPlatformState();
_sub = _plugin.onShareTriggered.listen((result) {
if (result.success) {
_addLog('[分享触发] 成功: ${result.errMsg}');
} else {
_addLog('[分享触发] 失败: ${result.errCode} - ${result.errMsg}');
}
});
}
@override
void dispose() {
_sub?.cancel();
if (_isRegistered) {
_plugin.unregisterGesturesShare();
}
_imagePathController.dispose();
_appLinkController.dispose();
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
Future<void> _initPlatformState() async {
String version;
try {
version = await _plugin.getPlatformVersion() ?? 'Unknown';
} on PlatformException {
version = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() => _platformVersion = version);
_addLog('平台版本: $_platformVersion');
}
Future<void> _register() async {
if (_isLoading) return;
final imagePath = _imagePathController.text.trim();
if (imagePath.isEmpty) {
_addLog('[错误] 图片路径不能为空');
return;
}
setState(() => _isLoading = true);
_addLog('正在注册隔空传送分享...');
try {
await _plugin.registerGesturesShare(
RegisterGesturesShareOptions(
imagePath: imagePath,
appLink: _appLinkController.text.trim().isNotEmpty
? _appLinkController.text.trim()
: null,
title: _titleController.text.trim().isNotEmpty
? _titleController.text.trim()
: null,
description: _descriptionController.text.trim().isNotEmpty
? _descriptionController.text.trim()
: null,
),
);
_addLog('[注册成功] 隔空传送分享已注册');
setState(() => _isRegistered = true);
} on PlatformException catch (e) {
_addLog('[注册失败] ${e.code}: ${e.message}');
} catch (e) {
_addLog('[注册失败] $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _unregister() async {
if (_isLoading) return;
setState(() => _isLoading = true);
_addLog('正在注销隔空传送分享...');
try {
await _plugin.unregisterGesturesShare();
_addLog('[注销成功] 隔空传送分享已注销');
setState(() => _isRegistered = false);
} on PlatformException catch (e) {
_addLog('[注销失败] ${e.code}: ${e.message}');
} catch (e) {
_addLog('[注销失败] $e');
} finally {
setState(() => _isLoading = false);
}
}
void _addLog(String message) {
final time = TimeOfDay.now();
final timeStr =
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
if (!mounted) return;
setState(() {
_logs.insert(0, '[$timeStr] $message');
if (_logs.length > 50) _logs.removeLast();
});
}
void _clearLogs() => setState(() => _logs.clear());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('自定义参数'),
actions: [
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: '清空日志',
onPressed: _clearLogs,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 状态卡片
_buildStatusCard(),
const SizedBox(height: 16),
// 参数设置
_buildSettingsCard(),
const SizedBox(height: 16),
// 操作按钮
_buildActionButtons(),
const SizedBox(height: 16),
// 日志区域
_buildLogCard(),
],
),
),
);
}
Widget _buildStatusCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
_isRegistered ? Icons.check_circle : Icons.cancel,
color: _isRegistered ? Colors.green : Colors.grey,
size: 32,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isRegistered ? '已注册' : '未注册',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
'平台: $_platformVersion',
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
),
],
),
),
],
),
),
);
}
Widget _buildSettingsCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('分享参数',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
TextField(
controller: _imagePathController,
decoration: const InputDecoration(
labelText: '图片路径 *',
hintText: '本地图片文件路径',
border: OutlineInputBorder(),
isDense: true,
),
enabled: !_isRegistered,
),
const SizedBox(height: 12),
TextField(
controller: _appLinkController,
decoration: const InputDecoration(
labelText: 'App Linking(可选)',
hintText: 'https://example.com/share',
border: OutlineInputBorder(),
isDense: true,
),
enabled: !_isRegistered,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题(可选)',
border: OutlineInputBorder(),
isDense: true,
),
enabled: !_isRegistered,
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: '描述(可选)',
border: OutlineInputBorder(),
isDense: true,
),
enabled: !_isRegistered,
),
),
],
),
],
),
),
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: _isRegistered || _isLoading ? null : _register,
icon: _isLoading && !_isRegistered
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.share),
label: const Text('注册分享'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: !_isRegistered || _isLoading ? null : _unregister,
icon: _isLoading && _isRegistered
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.close),
label: const Text('注销分享'),
),
),
],
);
}
Widget _buildLogCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.article_outlined, size: 20),
const SizedBox(width: 8),
const Text('运行日志',
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const Spacer(),
Text('${_logs.length} 条',
style: TextStyle(fontSize: 12, color: Colors.grey[500])),
],
),
const Divider(),
if (_logs.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Center(
child: Text('暂无日志',
style: TextStyle(color: Colors.grey)),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _logs.length,
itemBuilder: (context, index) {
final log = _logs[index];
Color textColor = Colors.black87;
if (log.contains('[成功]') ||
log.contains('[注册成功]') ||
log.contains('[注销成功]')) {
textColor = Colors.green[700]!;
} else if (log.contains('[失败]') ||
log.contains('[错误]') ||
log.contains('[注册失败]') ||
log.contains('[注销失败]')) {
textColor = Colors.red[700]!;
} else if (log.contains('[分享触发]')) {
textColor = Colors.blue[700]!;
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
log,
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: textColor,
),
),
);
},
),
],
),
),
);
}
}