阅读 2123

干一个Flutter组件:动动小手磨出一个资源多选插件(1)——基础构建篇

相关文章

  • 干一个Flutter组件:动动小手磨出一个资源多选插件(1)——基础构建篇
  • 干一个Flutter组件:动动小手磨出一个资源多选插件(2)——界面开发篇(准备中)

背景

  Flutter曾经有非常好用的多选组件,例如Sh1d0wmulti_image_picker,但他们都有或多或少的问题,例如不支持GIF选择,不支持视频或音频选择,定制程度不够高、依赖原生组件、不是纯Dart组件等。

  随着Flutter的不断发展,越来越多的packages涌现,项目迭代使得multi_image_picker已逐渐不能满足需求,且其依赖的iOS原生依赖作者在开源方向上的态度也使得这个库不再稳定,我个人便萌生了自己定制插件的想法。于是利用清明前一周开始至今的闲时,结合自己的项目OpenJMU定制了一个纯Dart的仿微信的资源选择组件,本次主要使用了三个重要依赖:photo_manager出自财经龙大佬之手,提供完整的API获取资源信息,为定制资源选择组件提供了启动基础🤣;extended_image出自法佬之手,作为强力的图片展示组件,体验+++++;以及大家熟知的provider用于维护选择器及各种部件的状态。

简介

  wechat_assets_picker是一个对标微信的多选资源选择器,99%接近于原生微信的操作,纯Dart编写,支持选择的同时也支持预览资源。本篇阐述的内容,在源码中均有对应的注释进行简易说明。如果你是源码选手,请移步repo。截止发文已于pub发布1.3.0版本,支持如下功能:

  • 图片资源支持
  • 视频资源支持
  • 国际化支持
  • 自定义文本支持

  效果图:

实现过程

  下面是具体的实现过程,将基于1.3.0版本进行说明。对于涉及到各依赖的使用方法,请移步对应仓库进行查看。

调用方法

  调用一个组件的方法是一切的开始,既然这个组件是一个纯Dart组件,那么也应该基于Flutter的上下文(context)进行调用,用于路由跳转。所以我们很快的写出一个组件的静态调用方法:

class AssetPicker extends StatlessWidget {
  /// 跳转至选择器的静态方法
  static Future<void> pickAssets(BuildContext context) async {}
}
复制代码

  作为一个多选组件,当然需要知道我能选几个资源,所以加入int maxAssets指定最大的资源可选数量,默认为9

  用户可以自定义网格数量,所以加入int gridCount指定网格每行格子数,默认为4

  用户可以指定缩略图的清晰度,所以加入int pageThumbSize指定选择器中缩略图加载的像素,默认为200

  再加亿点......

  最后我们的静态调用方法如下:

static Future<List<AssetEntity>> pickAssets( // 通过路由来传递已选中的资源
  BuildContext context, {
  int maxAssets = 9,
  int pathThumbSize = 200,
  int gridCount = 4,
  RequestType requestType = RequestType.image, // 请求加载的类型
  List<AssetEntity> selectedAssets, // 已选的资源,用来处理重复选中的问题
  Color themeColor = C.themeColor, // 主题色,默认采用了微信的#00bc56
  TextDelegate textDelegate, // 文字代理构建,用于构建每个文字点
}) async {}
复制代码

  一个完整的静态方法,通过AssetPicker.pickAssets就可以调用了。

组件的状态维护

  作为一个一把梭选手,在这里选用了ChangeNotifier作为选择器的model,进行对应状态的控制,因为涉及到大量资源的展示,如果不进行局部控制,将导致性能的大幅度下降。接下来开始设计AssetPickerProvider

  选择器需要保持什么状态?简单分析后,大概需要几个状态:

  • 设备上是否有资源文件(bool isAssetsEmpty)。在加载完成后,如果设备没有资源,则展示空布局。
  • 选中路径下是否有资源可供显示(bool hasAssetsToDisplay)。切换路径加载后,如果路径下没有资源,则展示空布局。
  • 是否正在进行路径选择(bool isSwitchingPath)。正在进行切换路径操作时,显示路径切换组件,并变换对应Widget
  • 所有资源路径及其的第一个资源的缩略图数据(Map<AssetPathEntity, Uint8List> pathEntityList)。保存所有的资源路径,并加载他们的第一个资源的缩略图,提供给路径切换组件。
  • 正在查看的资源路径(AssetPathEntity currentPathEntity)
  • 正在查看的资源路径的所有资源(List<AssetEntity> currentAssets)
  • 已经选中的资源(List<AssetEntity> selectedAssets)

  看上去很复杂,但上述状态均为必要的内容,从而保证我们的选择器能够正常工作。

  这里分享一个知识点:在model中我们常常需要存储一些集合数据(Map/Set/List),在Selector进行比较时,由于比对的仍然是同一个对象,比较的时候prev == next,无法得出正确结果。这时我们需要使用集合的from方法,例如Map.fromList.from生成新的集合对象,就可以让Selector正常的比较前后变化啦~举个🌰

set selectedAssets(List<AssetEntity> value) {
  assert(value != null);
  if (value == _selectedAssets) {
    return;
  }
  _selectedAssets = List<AssetEntity>.from(value);
  notifyListeners();
}
复制代码

  状态管理好了,我们还需要把选择器使用到的方法,一同放进model,结合数据一同使用。这里不再赘述源码,包含的方法有:获取所有的资源路径获取指定路径下的资源获取指定路径下的第一个资源的缩略数据选中&取消选中资源切换路径

  至此继续调整我们的静态方法,在方法中构造model并传入组件:

static Future<List<AssetEntity>> pickAssets(
  BuildContext context, {
  int maxAssets = 9,
  int pathThumbSize = 200,
  int gridCount = 4,
  RequestType requestType = RequestType.image,
  List<AssetEntity> selectedAssets,
  Color themeColor = C.themeColor,
  TextDelegate textDelegate,
}) async {
  final bool isPermissionGranted = await PhotoManager.requestPermission(); // 调用前检查权限,通过才拉起
  if (isPermissionGranted) {
    final AssetPickerProvider provider = AssetPickerProvider( // 构建model
      maxAssets: maxAssets,
      pathThumbSize: pathThumbSize,
      selectedAssets: selectedAssets,
      requestType: requestType,
    );
    final WidgetBuilder picker = (BuildContext _) => AssetPicker( // 构建组件
      provider: provider,
      gridCount: gridCount,
      textDelegate: textDelegate,
    );
    final List<AssetEntity> result = await Navigator.of(context).push<List<AssetEntity>>( // 构建路由
        Platform.isAndroid
            ? MaterialPageRoute<List<AssetEntity>>(builder: picker)
            : CupertinoPageRoute<List<AssetEntity>>(builder: picker),
    );
    return result;
  } else {
    return null;
  }
}
复制代码

文字代理构建

  在选择器中我们必定有各处文字提示,或是在按钮里,或是在布局填充里。为了增加可定制程度,在此我定义了文字代理抽象类TextDelegate,用于构建各处文字。闻其名而知其意,上代码:

abstract class TextDelegate {
  /// 确认按钮的字段
  String confirm;

  /// 返回按钮的字段
  String cancel;

  /// 编辑按钮的字段
  String edit;

  /// 选择器没有可显示的内容时的占位字段
  String emptyPlaceHolder;

  /// GIF指示的字段
  String gifIndicator;

  /// HEIC类型资源加载失败的字段
  String heicNotSupported;

  /// 资源加载失败时的字段
  String loadFailed;

  /// 选择是否原图的字段
  String original;

  /// 预览按钮的字段
  String preview;

  /// 选择按钮的字段
  String select;

  /// 未支持的资源类型的字段
  String unSupportedAssetType;

  /// 该字段用在选择器视频部件上,用于显示视频资源的时长。
  String videoIndicatorBuilder(Duration duration);
}
复制代码

  默认还提供了DefaultTextDelegate,作为默认的文字实现。

结语

  开发Flutter已经走过了一年的时间,慢慢地开始学会自己动手丰衣足食。下一篇我们将继续分析插件的界面开发内容~(我也不知道什么时候有下一篇😉)

  最后欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果 (QQ群:181398081) FlutterCandies

关注下面的标签,发现更多相似文章
评论