iOS 技巧

flutter 截图和保存图片到本地和相册,并做分享

2022-03-16  本文已影响0人  冰棍儿好烫嘴

一、截图功能

使用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

截屏2022-03-16 19.48.50.png

可以按我的添加,我这里除了基本功能,就是微信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

上一篇下一篇

猜你喜欢

热点阅读