一、截图功能
使用RepaintBoundary实现
具体实现:
1、注册全局的key与RepaintBoundary匹配,来标明截图内容
//全局key-截图key
final GlobalKey boundaryKey = GlobalKey();
2、将需要截图的widget包裹在RepaintBoundary组建中,加入key属性
SingleChildScrollView(
child:RepaintBoundary(
key: boundaryKey,
child:Container(
color: MyColor.white,
child:Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(
MyDimens.margin, 10, MyDimens.margin, 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Visibility(
visible: result != null && result!.org.length > 0,
child: Image(
image: result != null &&
result!.org.length > 0 &&
result!.org[0].source!.jglx == "党政机关"
? AssetImage("assets/images/organ.png")
: AssetImage("assets/images/institution.png"),
width: 20,
height: 20,
)),
Visibility(
visible: result != null && result!.org.length > 0,
child: SizedBox(
width: 5,
)),
Expanded(
child: Text(
_content,
style: MyStyle.text_style_bold,
))
],
),
),
Visibility(
visible: result != null &&
result!.org.length > 0 &&
result!.org[0].source != null &&
result!.web[0].source!.zwym != "--",
child: InkWell(
onTap: () {
launch('' + result!.web[0].source!.zwym.split(',')[0]);
},
child: Padding(
padding:
EdgeInsets.fromLTRB(42, 0, MyDimens.margin, 10),
child: Text(
result != null &&
result!.org[0].source != null &&
result!.org.length > 0
? result!.web[0].source!.zwym.split(',')[0]
: "",
style: MyStyle.text_style_link,
)),
)),
Container(
color: MyColor.background,
height: 0.5,
),
Container(
color: MyColor.background,
height: 10,
),
Padding(
padding: EdgeInsets.all(MyDimens.margin),
child: Text(
"基本信息",
style: MyStyle.text_style_bold,
),
),
Container(
color: MyColor.background,
height: 0.5,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: MyDimens.margin),
child: Column(
children: [
KeyValueSingle(
mKey: '机构职能',
mValue: result != null && result!.org.length > 0
? result!.org[0].source!.jgzz
: ""),
],
),
),
Container(
color: MyColor.background,
height: 10,
),
Padding(
padding: EdgeInsets.all(MyDimens.margin),
child: Text(
"网站开办信息",
style: MyStyle.text_style_bold,
),
),
Container(
color: MyColor.background,
height: 0.5,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: MyDimens.margin),
child: Column(
children: [
KeyValueSingle(
mKey: '网站名称',
mValue: result != null && result!.org.length > 0
? result!.web[0].source!.wzmc
: ""),
Container(
color: MyColor.background,
height: 0.5,
),
],
),
),
],
),
),
),
)
注意点:
1、如果直接包裹listView则不能截取全部内容,最好用SingleChildScrollView + colum 结合实现列表 才能截取全部内容
2、如果截取的内容超出屏幕,则必须将RepaintBoundary直接包裹在滑动内容上(colum上),否则屏幕外的内容截取不到
满足以上两个条件,才能实现截取整个widget的内容
3、如果RepaintBoundary包裹的widget没有背景色,在安卓上截图会是默认黑色背景,所以最好添加相应的背景色。
二、图片保存
/*
* @Author: 王长春
* @Date: 2022-03-14 09:24:34
* @LastEditors: 王长春
* @LastEditTime: 2022-03-17 10:01:41
* @Description: 截图工具,生成截图,保存到相册或者保存本地cash文件夹返回文件路径(供分享使用)
*/
import 'dart:io';
import 'dart:typed_data';
import 'dart:async';
import 'dart:ui';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
//全局key-截图key
final GlobalKey boundaryKey = GlobalKey();
class RepaintBoundaryUtils {
//生成截图
/// 截屏图片生成图片流ByteData
Future<String> captureImage() async {
RenderRepaintBoundary? boundary = boundaryKey.currentContext!
.findRenderObject() as RenderRepaintBoundary?;
double dpr = ui.window.devicePixelRatio; // 获取当前设备的像素比
var image = await boundary!.toImage(pixelRatio: dpr);
// 将image转化成byte
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
var filePath = "";
Uint8List pngBytes = byteData!.buffer.asUint8List();
// 获取手机存储(getTemporaryDirectory临时存储路径)
Directory applicationDir = await getTemporaryDirectory();
// getApplicationDocumentsDirectory();
// 判断路径是否存在
bool isDirExist = await Directory(applicationDir.path).exists();
if (!isDirExist) Directory(applicationDir.path).create();
// 直接保存,返回的就是保存后的文件
File saveFile = await File(
applicationDir.path + "${DateTime.now().toIso8601String()}.jpg")
.writeAsBytes(pngBytes);
filePath = saveFile.path;
// if (Platform.isAndroid) {
// // 如果是Android 的话,直接使用image_gallery_saver就可以了
// // 图片byte数据转化unit8
// Uint8List images = byteData!.buffer.asUint8List();
// // 调用image_gallery_saver的saveImages方法,返回值就是图片保存后的路径
// String result = await ImageGallerySaver.saveImage(images);
// // 需要去除掉file://开头。生成要使用的file
// File saveFile = new File(result.replaceAll("file://", ""));
// filePath = saveFile.path;
//
//
// } else if (Platform.isIOS) {
// // 图片byte数据转化unit8
//
// }
return filePath;
}
//申请存本地相册权限
Future<bool> getPormiation() async {
if (Platform.isIOS) {
var status = await Permission.photos.status;
if (status.isDenied) {
Map<Permission, PermissionStatus> statuses = await [
Permission.photos,
].request();
// saveImage(globalKey);
}
return status.isGranted;
} else {
var status = await Permission.storage.status;
if (status.isDenied) {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
].request();
}
return status.isGranted;
}
}
//保存到相册
void savePhoto() async {
RenderRepaintBoundary? boundary = boundaryKey.currentContext!
.findRenderObject() as RenderRepaintBoundary?;
double dpr = ui.window.devicePixelRatio; // 获取当前设备的像素比
var image = await boundary!.toImage(pixelRatio: dpr);
// 将image转化成byte
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
//获取保存相册权限,如果没有,则申请改权限
bool permition = await getPormiation();
var status = await Permission.photos.status;
if (permition) {
if (Platform.isIOS) {
if (status.isGranted) {
Uint8List images = byteData!.buffer.asUint8List();
final result = await ImageGallerySaver.saveImage(images,
quality: 60, name: "hello");
EasyLoading.showToast("保存成功");
}
if (status.isDenied) {
print("IOS拒绝");
}
} else {
//安卓
if (status.isGranted) {
print("Android已授权");
Uint8List images = byteData!.buffer.asUint8List();
final result = await ImageGallerySaver.saveImage(images, quality: 60);
if (result != null) {
EasyLoading.showToast("保存成功");
} else {
print('error');
// toast("保存失败");
}
}
}
}else{
//重新请求--第一次请求权限时,保存方法不会走,需要重新调一次
savePhoto();
}
}
}
调用方法:
//保存本地 RepaintBoundaryUtils().savePhoto();
说明:
我这里涉及到分享以及保存图片到本地相册两个功能。
涉及到的框架有:
#分享
share_plus: ^3.0.1
#微信三方-分享、登录、小程序跳转等(不带支付)
fluwx_no_pay: ^3.8.1
#保存图片到相册
image_gallery_saver: ^1.7.1
#权限管理
permission_handler: ^8.1.6
iOS权限设置:
在podfile中添加一下内容:
target.build_configurations.each do |config|
# You can remove unused permissions here
# for more infomation: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
# e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_PHOTOS=1',
]
end
podfile整体做为参考:
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# You can remove unused permissions here
# for more infomation: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
# e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_PHOTOS=1',
]
end
end
end
info.plist中添加字段
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>organization</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>weixin</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx543b04c9d1aa9a3a</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixinULAPI</string>
<string>baidumap</string>
<string>iosamap</string>
<string>comgooglemaps</string>
<string>qqmap</string>
<string>mqzone</string>
<string>mqqwpa</string>
<string>mqzoneopensdkapi19</string>
<string>mqzoneopensdkapi</string>
<string>mqzoneopensdk</string>
<string>mqzoneopensdkapiV2</string>
<string>mqqapi</string>
<string>mqq</string>
<string>wtloginmqq2</string>
<string>mqqopensdkapiV3</string>
<string>mqqopensdkapiV2</string>
<string>mqqOpensdkSSoLogin</string>
<string>weixin</string>
<string>wechat</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>需要访问您的相机设置头像</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问您的相册设置头像</string>
<key>NSSupportsSuddenTermination</key>
<false/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
添加urlSchemes
可以按我的添加,我这里除了基本功能,就是微信sdk分享以及相册相机权限。
安卓权限
AndroidManifest.xml添加一下权限
<!-- 开启读写storage权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
三、分享:
分享这里提供两种方案;
a、share_plus 分享:不用在第三方平台注册app,这种分享出去的图片,不带app图标以及名称,只有图片
b、微信原生分享:fluwx_no_pay:需在微信开发者账号上注册app,这种分享出去的图片带app图标以及名称。
微信原生分享代码:
/*
* @Author: 王长春
* @Date: 2022-03-11 09:43:46
* @LastEditors: 王长春
* @LastEditTime: 2022-03-16 09:56:14
* @Description:
*/
import 'dart:io';
import 'dart:typed_data';
import 'check.dart';
import 'package:fluwx_no_pay/fluwx_no_pay.dart' as fluwx;
class WxSdk {
// static bool wxIsInstalled;
static Future init() async {
fluwx.registerWxApi(
appId: "这里写你注册的appid",
doOnAndroid: true,
doOnIOS: true,
universalLink: "这里写你注册的universalLink");
}
static Future<bool> wxIsInstalled() async {
return await fluwx.isWeChatInstalled;
}
/**
* 分享图片到微信,
* file=本地路径
* url=网络地址
* asset=内置在app的资源图片
* scene=分享场景,1好友会话,2朋友圈,3收藏
*/
static void ShareImage(
{String? title,
String? decs,
String? file,
String? url,
String? asset,
int scene = 1}) async {
fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
if (scene == 2) {
wxScene = fluwx.WeChatScene.TIMELINE;
} else if (scene == 3) {
wxScene = fluwx.WeChatScene.FAVORITE;
}
fluwx.WeChatShareImageModel? model;
if (file != null) {
model = fluwx.WeChatShareImageModel(fluwx.WeChatImage.file(File(file)),
title: title, description: decs, scene: wxScene);
} else if (url != null) {
model = fluwx.WeChatShareImageModel(fluwx.WeChatImage.network(url),
title: title, description: decs, scene: wxScene);
} else if (asset != null) {
model = fluwx.WeChatShareImageModel(fluwx.WeChatImage.asset(asset),
title: title, description: decs, scene: wxScene);
} else {
throw Exception("缺少图片资源信息");
}
fluwx.shareToWeChat(model);
}
/**
* 分享文本
* content=分享内容
* scene=分享场景,1好友会话,2朋友圈,3收藏
*/
static void ShareText(String content, {String? title, int scene = 1}) {
fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
if (scene == 2) {
wxScene = fluwx.WeChatScene.TIMELINE;
} else if (scene == 3) {
wxScene = fluwx.WeChatScene.FAVORITE;
}
fluwx.WeChatShareTextModel model =
fluwx.WeChatShareTextModel(content, title: title, scene: wxScene);
fluwx.shareToWeChat(model);
}
/***
* 分享视频
* videoUrl=视频网上地址
* thumbFile=缩略图本地路径
* scene=分享场景,1好友会话,2朋友圈,3收藏
*/
static void ShareVideo(String videoUrl,
{String? thumbFile, String? title, String? desc, int scene = 1}) {
fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
if (scene == 2) {
wxScene = fluwx.WeChatScene.TIMELINE;
} else if (scene == 3) {
wxScene = fluwx.WeChatScene.FAVORITE;
}
fluwx.WeChatImage? image;
if (thumbFile != null) {
image = fluwx.WeChatImage.file(File(thumbFile));
}
var model = fluwx.WeChatShareVideoModel(
videoUrl: videoUrl,
thumbnail: image,
title: title,
description: desc,
scene: wxScene);
fluwx.shareToWeChat(model);
}
/**
* 分享链接
* url=链接
* thumbFile=缩略图本地路径
* scene=分享场景,1好友会话,2朋友圈,3收藏
*/
static void ShareUrl(String url,
{String? thumbFile,
Uint8List? thumbBytes,
String? title,
String? desc,
int scene = 1,
String? networkThumb,
String? assetThumb}) {
desc = desc ?? "";
title = title ?? "";
if (desc.length > 54) {
desc = desc.substring(0, 54) + "...";
}
if (title.length > 20) {
title = title.substring(0, 20) + "...";
}
fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
if (scene == 2) {
wxScene = fluwx.WeChatScene.TIMELINE;
} else if (scene == 3) {
wxScene = fluwx.WeChatScene.FAVORITE;
}
fluwx.WeChatImage? image ;
if (thumbFile != null) {
image = fluwx.WeChatImage.file(File(thumbFile));
} else if (thumbBytes != null) {
image = fluwx.WeChatImage.binary(thumbBytes);
} else if (strNoEmpty(networkThumb!)) {
image = fluwx.WeChatImage.network(Uri.encodeFull(networkThumb));
} else if (strNoEmpty(assetThumb!)) {
image = fluwx.WeChatImage.asset(assetThumb, suffix: ".png");
}
var model = fluwx.WeChatShareWebPageModel(
url,
thumbnail: image,
title: title,
description: desc,
scene: wxScene,
);
fluwx.shareToWeChat(model);
}
}
分享功能封装:
/*
* @Author: 王长春
* @Date: 2022-03-15 15:17:19
* @LastEditors: 王长春
* @LastEditTime: 2022-03-16 19:15:45
* @Description: 分享工具
*/
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:organization/common/utils/repaintBoundary_utils.dart';
import 'package:organization/common/utils/wechatSDK.dart';
import 'package:share_plus/share_plus.dart';
class ShareHelper {
static bool wxIsInstalled = false;
//微信分享截图
static void onShareWx(BuildContext context) async {
wxIsInstalled = await WxSdk.wxIsInstalled();
//收回键盘,如果有输入的情况下,先收回键盘,再截图
FocusScope.of(context).requestFocus(FocusNode());
if (wxIsInstalled) {
//分享
// WxSdk.ShareText("sdgsfds",title:"标题");
//获取截图地址
String filePath = await RepaintBoundaryUtils().captureImage();
print(filePath);
WxSdk.ShareImage(title: "机构检索", decs: "", file: filePath);
} else {
//提示未安装微信
EasyLoading.showToast("未安装微信");
}
}
// static void checkWx() async {
// wxIsInstalled = await WxSdk.wxIsInstalled();
// }
//share_plus分享
static void onSharePlusShare(BuildContext context) async {
FocusScope.of(context).requestFocus(FocusNode());
// A builder is used to retrieve the context immediately
// surrounding the ElevatedButton.
// The context's `findRenderObject` returns the first
// RenderObject in its descendent tree when it's not
// a RenderObjectWidget. The ElevatedButton's RenderObject
// has its position and size after it's built.
final box = context.findRenderObject() as RenderBox?;
List<String> imagePaths = [];
//获取截图地址
String filePath = await RepaintBoundaryUtils().captureImage();
//Share.shareFiles内可以传多张图片,里面是个数组,所以每次要将数组清空,再将新的截图添加到数组中
imagePaths.clear();
imagePaths.add(filePath);
//分享
await Share.shareFiles(imagePaths,
text: "机构详情",
subject: "",
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
}
}
调用:
//share_plus 分享
ShareHelper.onSharePlusShare(context);
//微信SDK分享
// ShareHelper.onShareWx(context);
以上实现了图片截图以及保存本地分享内容,在安卓和iOS上亲测没问题。
关于iOS微信原生分享,注册universalLink的内容,需要后台配合,我这里只是写了个demo,没具体实现。
可以参考 : https://www.jianshu.com/p/4c96b54ef8d1