如何在Flutter中实现类似WhatsApp的聊天气泡?
求助:Flutter实现WhatsApp式聊天气泡(提取URL消息中的网页预览图)
我正在开发类似WhatsApp的聊天气泡组件,目前需要实现从含URL的消息中提取并展示网页预览图的功能。例如给定新闻URL,希望能像WhatsApp那样显示网页标题+预览图+链接的组合样式。
预期效果


实际效果


当前代码
Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: MediaQuery.of(context).size.width * 0.75, padding: const EdgeInsets.all(5.0), decoration: const BoxDecoration( color: Colors.amber, borderRadius: BorderRadius.only( topLeft: Radius.circular(15.0), topRight: Radius.circular(15.0), bottomLeft: Radius.circular(0.0), bottomRight: Radius.circular(15.0), ), ), child: Column( children: [ Align( alignment: Alignment.centerLeft, child: Text( name, style: const TextStyle( color: Color.fromARGB(255, 50, 150, 150), fontSize: 12.5, ), ), ), Align( alignment: Alignment.centerLeft, child: Linkify( onOpen: (link) async { if (await canLaunchUrl( Uri.parse(link.url), )) { await launchUrl( Uri.parse(link.url)); } else { throw "Could not launch $link"; } }, text: message, style: const TextStyle( color: Color.fromARGB(255, 50, 150, 150), fontSize: 20.0, ), linkStyle: const TextStyle( fontSize: 20.0, color: Colors.blue, decoration: TextDecoration.underline, ), ), ), Align( alignment: Alignment.centerRight, child: Text( timeago.format(sentAt.toDate()), style: const TextStyle( color: Color.fromARGB(255, 50, 150, 150), fontSize: 15.0, ), ), ), ], ), ), ], ),
实现方案建议
要实现网页链接预览,核心是解析URL对应的网页元数据(标题、预览图),再调整布局展示预览卡片。
方法一:使用第三方链接预览包(快速实现)
推荐使用link_preview_generator包,它会自动解析网页的Open Graph标签,生成类似WhatsApp的预览卡片。
- 在
pubspec.yaml添加依赖:
dependencies: link_preview_generator: ^any
- 修改核心布局代码:
Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: MediaQuery.of(context).size.width * 0.75, padding: const EdgeInsets.all(5.0), decoration: const BoxDecoration( color: Colors.amber, borderRadius: BorderRadius.only( topLeft: Radius.circular(15.0), topRight: Radius.circular(15.0), bottomLeft: Radius.circular(0.0), bottomRight: Radius.circular(15.0), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: const TextStyle( color: Color.fromARGB(255, 50, 150, 150), fontSize: 12.5, ), ), // 判断消息中是否包含URL,显示预览或原文本 if (message.contains(RegExp(r'https?://\S+'))) // 提取消息中的第一个URL LinkPreviewGenerator( link: RegExp(r'https?://\S+').stringMatch(message)!, onTap: (url) async { if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); } }, borderRadius: 8.0, boxShadow: const [BoxShadow(color: Colors.grey, blurRadius: 2)], previewHeight: 120, ) else Text( message, style: const TextStyle( color: Color.fromARGB(255, 50, 150, 150), fontSize: 20.0, ), ), Align( alignment: Alignment.centerRight, child: Text( timeago.format(sentAt.toDate()), style: const TextStyle( color: Color.fromARGB(255, 50, 150, 150), fontSize: 15.0, ), ), ), ], ), ), ], ),
方法二:自定义解析网页元数据(无第三方依赖)
如果不想用第三方包,可通过http+html包手动解析网页的og标签:
- 添加依赖:
dependencies: http: ^any html: ^any
- 实现解析函数:
Future<Map<String, String?>> fetchWebPreview(String url) async { try { final response = await http.get(Uri.parse(url)); final document = parse(response.body); final ogTitle = document.querySelector('meta[property="og:title"]')?.attributes['content']; final ogImage = document.querySelector('meta[property="og:image"]')?.attributes['content']; return {'title': ogTitle, 'image': ogImage}; } catch (e) { return {'title': url, 'image': null}; } }
- 在组件中使用
FutureBuilder展示预览:
// 假设已提前提取URL final String url = RegExp(r'https?://\S+').stringMatch(message)!; // 替换原Linkify部分为: FutureBuilder<Map<String, String?>>( future: fetchWebPreview(url), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } final previewData = snapshot.data!; return GestureDetector( onTap: () async { if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); } }, child: Container( margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (previewData['image'] != null) Expanded( flex: 1, child: Image.network( previewData['image']!, fit: BoxFit.cover, height: 100, ), ), Expanded( flex: 2, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( previewData['title'] ?? url, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Text( Uri.parse(url).host, style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), ], ), ), ), ], ), ), ); }, )
注意事项
- 处理网络请求异常:当网页无法访问或无og标签时,fallback显示原URL文本。
- 优化性能:缓存已解析的网页预览数据,避免重复请求。
- 权限配置:确保Android/iOS的网络权限已开启(Android的
INTERNET权限,iOS的NSAppTransportSecurity配置)。
内容的提问来源于stack exchange,提问作者My Car
相关产品推荐
相关产品推荐

