Flutter如何封装一个相册照片选择器?

1,427 阅读4分钟

开篇废话:flutter 相机相册第三方不知道怎么用?或者不知道怎么搜?干脆封装一个flutter 相册选择功能,是的,有些时候用第三方是为了减少开发时间,但是去搜索合适的第三方花费大量的时间甚至稍微有一点点需要变动就要重新换,那么简直就是灾难,在有时间、有条件的情况下单独封装一个其实也不是不可以。

问题一:实现照片的选择及展示需要哪些铺垫?

tip:其实,完成上述效果也花费了一点精力,并不是一蹴而就的,但是只要耐着性子去做,还是有结果的。

  1. 原生沙盒路径临时保存相册图片。
  2. iOS 原生与flutter通信。
  3. flutter 利用 ImageProvider 渲染指定路径下的图片。

问题二:需求确认,开发步骤如何拆分?

因为需要 flutter 下的 ImageProvider 类去加载指定路径下的图片资源,那么就需要在选择完相册图片后进行临时的iOS 下的APP沙盒存储(安卓同理)。

iOS 原生部分相册图片处理部分

cocopods 依赖

//相册选择
pod 'TZImagePickerController', '~> 3.4.2'
//文件管理
pod 'FCFileManager', '~> 1.0.20'
@interface FlutterBridgeMannager()<TZImagePickerControllerDelegate>
//flutter 双端通信通道类
@property (nonatomic,strong) FlutterMethodChannel *messageChannel;

@end

1、监听flutter 点击发过来需要吊起相册事件。


- (void)receiveMessageFromFlutter

{
    UIWindow * w = [UIApplication sharedApplication].delegate.window;

    UINavigationController *  nvc = (UINavigationController*)w.rootViewController;

    FlutterViewController<FlutterBinaryMessenger>* controller =  (FlutterViewController<FlutterBinaryMessenger>*)nvc.childViewControllers.firstObject;

    NSString *channelName = @"com.pages.wsl/native_takePhoto";

    self.messageChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:controller];

    __weak typeof(self) weakSelf = self;

    [self.messageChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {

        if ([call.method isEqualToString:@"takePhoto"]) {

            //进行拍照

            [weakSelf takePhoto];

        }

    }];

}

2、调起相册选择器。

- (void)takePhoto

{

    TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:1 delegate:self];

    UIWindow * w = [UIApplication sharedApplication].delegate.window;

    UINavigationController *  nvc = (UINavigationController*)w.rootViewController;

    imagePickerVc.modalPresentationStyle = UIModalPresentationOverFullScreen;

    [nvc presentViewController:imagePickerVc animated:YEScompletion:NULL];

}

// 选择照片的回调

-(void)imagePickerController:(TZImagePickerController *)picker

      didFinishPickingPhotos:(NSArray *)photos

                sourceAssets:(NSArray *)assets

       isSelectOriginalPhoto:(**BOOL**)isSelectOriginalPhoto{

     if ((photos.count)) {

        [self saveImageToCacheWitImage:photos.firstObject];

    }

}

3、沙盒保存,返回沙盒图片路径。


- (void)saveImageToCacheWitImage:(UIImage *)image

{

    // 沙盒路径

    NSString * docsdir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, **YES**) objectAtIndex:0];

    NSString *fultterFilePath = [docsdir stringByAppendingPathComponent:@"fultter/temp/images/"];

    if (![FCFileManager existsItemAtPath:fultterFilePath]) {

        [FCFileManager createDirectoriesForPath:fultterFilePath];

        NSLog(@"flutte临时文件管理创建成功");

    } else {

        NSLog(@"flutte临时文件管理存在");

    }

    

    NSString * flutterTempPath = [NSString stringWithFormat:@"%@/flutterTemp.png",fultterFilePath];

    NSLog(@"flutterTempPath = %@",flutterTempPath);

    if ([FCFileManager writeFileAtPath:flutterTempPath content:image]) {

        NSLog(@"复制图片成功");

    } else {

        NSLog(@"复制图片失败");

    }

    //通知flutter进行本地图片渲染,返回沙盒图片存储路径
    [self.messageChannel invokeMethod:@"needRefreshShowPhoto" arguments:@{@"cachePath":flutterTempPath}];

}

flutter 部分处理

1、创建 CacheImageProvider 类继承自 ImageProvider,实现图片流的读取。

import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class CacheImageProvider extends ImageProvider<CacheImageProvider> {

  String imageCachePath = '';
  final double scale;

  CacheImageProvider(this.imageCachePath, {this.scale: 1.0});

  //必需实现
  @override
  ImageStreamCompleter load(CacheImageProvider key, DecoderCallback decode) {
    return new MultiFrameImageStreamCompleter(
      codec: _loadAsync(key),
      scale: key.scale,
    );
  }

  //读取文件路径,进行绘制
  Future<ui.Codec> _loadAsync(CacheImageProvider key) async {
    var file = File(imageCachePath);
    return await _loadAsyncFromFile(key, file);
  }
  
  //绘制图片
  Future<ui.Codec> _loadAsyncFromFile(CacheImageProvider key, File file) async {
    assert(key == this);

    final Uint8List bytes = await file.readAsBytes();

    if (bytes.lengthInBytes == 0) {
      throw new Exception("File was empty");
    }
    return await ui.instantiateImageCodec(bytes);
  }

  //必需实现
  @override
  Future<CacheImageProvider> obtainKey(ImageConfiguration configuration) {
    return new SynchronousFuture<CacheImageProvider>(this);
  }

}

2、创建 MethodChannel 类 进行原生事件的交互与监听。

//交互通道声明与创建
static const _methodChannel = const MethodChannel('com.pages.wsl/native_takePhoto');
//移动端沙盒临时图片存储路径
String _cacheImagePath = '';
//是否获取到了缓存图片(目的是判断刷新界面)
bool _isGetCacheImage = false;

设置监听,当原生端完成了图片的沙盒存储需要通知flutter进行图片的读取


//state 初始化进行 _methodChannel 的原生消息监听
@override
void initState(){
  nativeMessageListener();
}

//设置消息监听
void nativeMessageListener() async {
  _methodChannel.setMethodCallHandler((resultCall) async {

    MethodCall call = resultCall;
    String method = call.method;
    Map arguments = call.arguments;
    //needRefreshShowPhoto 是原生设置的回调名称,参数map里取沙盒图片路径
    if(method == 'needRefreshShowPhoto') {
      //刷新界面,进行图片流读取
      setState(() {
        _isGetCacheImage = true;
        _cacheImagePath = arguments['cachePath'];
      });
    }
  });
}

简单的布局展示

@override
Widget build(BuildContext context) {
  return new Scaffold(
    body: new Center(
      child: new Container(
        //是否显示可加载的本地图片
        child: _isGetCacheImage ?  new GestureDetector(
          child: new Image(
            image: new CacheImageProvider(_cacheImagePath) ,
            width: 200,
            height: 150,
          ),
          onTap: (){
            _methodChannel.invokeMethod('takePhoto');
          },
        ) : new Container(
          width: 200,
          height: 150,
          color: Color.fromARGB(255, 220, 220, 220),
          child: new GestureDetector(
            child: new Center(
              child: new Text('获取相册图片',style: new TextStyle(fontWeight: FontWeight.w600),),
            ),
            onTap: (){
             //进行图片选择
              _methodChannel.invokeMethod('takePhoto');
            },
          ),
        ),
      ),
    ),
  );
}

一个简单的加载原生图片小功能就封装好了,iOS下的临时缓存图片在flutter展示完成后其实应该在通知原生去删除缓存,这里就选择了一张图,并且每次缓存的路径都是一样的,所以并没有去处理,如果一次性选择9张图,那么这里就需要在展示完后直接清空缓存图片的上级目录。

纯手撸代码,目的是知识点衔接巩固,代码拙劣,大神勿笑,如果能帮助到大家,更是深感欣慰[抱拳][抱拳][抱拳]。