相关文章
- 干一个Flutter组件:动动小手磨出一个资源多选插件(1)——基础构建篇
- 干一个Flutter组件:动动小手磨出一个资源多选插件(2)——界面开发篇(准备中)
背景
Flutter曾经有非常好用的多选组件,例如Sh1d0w的multi_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.from、List.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群)