album_view.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. /*
  2. * @Author: XianKaiQun
  3. * @Date: 2020-08-31 11:44:00
  4. * @LastEditors: ChenYaJin
  5. * @LastEditTime: 2022-11-16 17:31:43
  6. * @Description:
  7. */
  8. import 'dart:io';
  9. import 'package:flutter/material.dart';
  10. import 'package:flutter/services.dart';
  11. import 'package:photo_manager/photo_manager.dart';
  12. import 'package:wisdom_cli/wisdom_cli.dart';
  13. ///File实体类
  14. ///
  15. ///之后本地文件的选择有很多不可预料的东西,
  16. ///创建一个实体类之后能够达到兼容的效果。
  17. class FileEntity {
  18. final File? file;
  19. FileEntity({this.file});
  20. static Future<FileEntity> formAssetFile(AssetEntity assetFile) async {
  21. return FileEntity(file: await assetFile.file);
  22. }
  23. }
  24. ///让子组件获取到父级State
  25. class _Scope extends InheritedWidget {
  26. _Scope({
  27. Key? key,
  28. required this.child,
  29. this.state,
  30. }) : super(key: key, child: child);
  31. final Widget child;
  32. final _WAlbumViewState? state;
  33. @override
  34. bool updateShouldNotify(_Scope oldWidget) {
  35. return oldWidget.state!.values != state!.values;
  36. }
  37. }
  38. ///相册视图
  39. class WAlbumView extends StatefulWidget {
  40. WAlbumView({
  41. Key? key,
  42. this.maxCount = 9,
  43. }) : assert(maxCount > 0),
  44. super(key: key);
  45. final int maxCount;
  46. ///推进路由栈
  47. static Future<List<FileEntity>?> pushRoute({
  48. int maxCount = 9,
  49. }) =>
  50. WNavUtil.instance!.push<List<FileEntity>>(
  51. MaterialPageRoute(
  52. builder: (_) => WAlbumView(
  53. maxCount: maxCount,
  54. ),
  55. ),
  56. );
  57. static _WAlbumViewState? _of(BuildContext context) {
  58. return context.dependOnInheritedWidgetOfExactType<_Scope>()!.state;
  59. }
  60. @override
  61. _WAlbumViewState createState() => _WAlbumViewState();
  62. }
  63. class _WAlbumViewState extends State<WAlbumView> {
  64. ///是否多选
  65. bool get multi => widget.maxCount > 1;
  66. ///目录
  67. List<AssetPathEntity> paths = [];
  68. ///当前选择的目录
  69. AssetPathEntity? currentPaths;
  70. ///获取目录
  71. Future<void> getPaths() async {
  72. paths = await PhotoManager.getAssetPathList(
  73. type: RequestType.image,
  74. );
  75. if (paths.length > 0) {
  76. currentPaths = paths[0];
  77. setState(() {});
  78. } else {
  79. WToastUtil.show('手机相册暂无照片呢');
  80. }
  81. }
  82. var albums = <AssetPathEntity>[];
  83. GlobalKey globalKey = GlobalKey();
  84. OverlayEntry? entry;
  85. onChangeCurrentPath() {
  86. if (entry != null) {
  87. entry?.remove();
  88. entry = null;
  89. setState(() {});
  90. return;
  91. }
  92. final RenderBox renderBox =
  93. globalKey.currentContext?.findRenderObject() as RenderBox;
  94. final offset = renderBox.localToGlobal(Offset.zero);
  95. final size = renderBox.size;
  96. entry = OverlayEntry(
  97. builder: (context) {
  98. return Positioned.fill(
  99. top: offset.dy + size.height,
  100. child: GestureDetector(
  101. onTap: () {
  102. entry?.remove();
  103. entry = null;
  104. },
  105. child: Material(
  106. color: Colors.black26,
  107. child: Wisdom(
  108. margin: EdgeInsets.only(
  109. bottom: MediaQuery.of(context).size.height / 2,
  110. ),
  111. color: Colors.white,
  112. child: ListView.builder(
  113. itemCount: paths.length,
  114. itemBuilder: (_, i) {
  115. return Wisdom(
  116. padding: EdgeInsets.symmetric(horizontal: 20.pt),
  117. height: 44.pt,
  118. onTap: () => changeCurrentPath(paths[i]),
  119. child: Text(
  120. paths[i].isAll ? '全部' : paths[i].name,
  121. maxLines: 1,
  122. overflow: TextOverflow.ellipsis,
  123. ),
  124. );
  125. },
  126. ),
  127. ),
  128. ),
  129. ),
  130. );
  131. },
  132. );
  133. Overlay.of(context).insert(entry!);
  134. setState(() {});
  135. }
  136. ///切换目录
  137. void changeCurrentPath(AssetPathEntity path) {
  138. entry?.remove();
  139. entry = null;
  140. currentPaths = path;
  141. list = [];
  142. page = 0;
  143. setState(() {});
  144. getList();
  145. }
  146. ///当前目录的列表
  147. List<AssetEntity> list = [];
  148. int page = 0;
  149. ///请求列表
  150. Future<void> getList() async {
  151. var _list = await currentPaths!.getAssetListPaged(
  152. page: page,
  153. size: 50,
  154. );
  155. if (_list.length > 0) {
  156. page = page + 1;
  157. list.addAll(_list);
  158. }
  159. setState(() {});
  160. }
  161. ///缓存
  162. Map<String, Uint8List?> cache = {};
  163. Future<Uint8List?> onCache(AssetEntity data) async {
  164. if (cache[data.id] == null) {
  165. cache[data.id] =
  166. await data.thumbnailDataWithSize(ThumbnailSize(100, 100));
  167. }
  168. return cache[data.id];
  169. }
  170. @override
  171. void dispose() {
  172. cache = {};
  173. entry?.remove();
  174. super.dispose();
  175. }
  176. @override
  177. void initState() {
  178. super.initState();
  179. }
  180. onInit() async {
  181. page = 0;
  182. setState(() {});
  183. getPaths().then((value) {
  184. ///稍微延迟一下,防止低端机在路由动画期间卡顿
  185. Future.delayed(Duration(milliseconds: 200)).then((value) {
  186. getList();
  187. });
  188. });
  189. }
  190. ///当前选中
  191. List<AssetEntity?> values = [];
  192. ///选中事件
  193. void onPick(AssetEntity? data) {
  194. if (widget.maxCount == 1) {
  195. values = [data];
  196. } else if (values.indexOf(data) == -1) {
  197. if (values.length >= widget.maxCount && widget.maxCount != 1) {
  198. WToastUtil.show('已超出最大选择数目');
  199. return;
  200. } else {
  201. values.add(data);
  202. }
  203. } else {
  204. values.remove(data);
  205. }
  206. setState(() {});
  207. }
  208. ///确定 ,
  209. ///循环转换实体类
  210. Future<void> pop() async {
  211. final futures = values.map((e) => FileEntity.formAssetFile(e!)).toList();
  212. final _values = await Future.wait(futures);
  213. WNavUtil.instance!.pop(_values);
  214. }
  215. @override
  216. Widget build(BuildContext context) {
  217. final color = Theme.of(context).primaryColor;
  218. return _Scope(
  219. state: this,
  220. child: Scaffold(
  221. appBar: AppBar(
  222. key: globalKey,
  223. centerTitle: true,
  224. backgroundColor: Colors.white,
  225. leading: CloseButton(),
  226. iconTheme: IconThemeData(color: Colors.black),
  227. title: TextButton(
  228. onPressed: () => onChangeCurrentPath(),
  229. style: ButtonStyle(
  230. foregroundColor: MaterialStateProperty.all(
  231. Colors.black,
  232. ),
  233. ),
  234. child: Row(
  235. mainAxisSize: MainAxisSize.min,
  236. children: [
  237. if (currentPaths != null)
  238. Text(
  239. currentPaths!.isAll ? '全部' : (currentPaths?.name ?? '未知'),
  240. ),
  241. Icon(entry == null ? FaIcons.angle_down : FaIcons.angle_up)
  242. ],
  243. ),
  244. ),
  245. systemOverlayStyle: SystemUiOverlayStyle.dark,
  246. ),
  247. body: WisScrollView(
  248. onInit: () => onInit(),
  249. onLoad: () => getList(),
  250. slivers: [
  251. WisSliverGrid(
  252. count: list.length,
  253. crossAxisCount: 4,
  254. padding: EdgeInsets.all(2.pt),
  255. mainAxisSpacing: 2.pt,
  256. crossAxisSpacing: 2.pt,
  257. builder: (_, i) => _GridItem(
  258. data: list[i],
  259. color: color,
  260. ),
  261. ),
  262. ],
  263. ),
  264. bottomNavigationBar: _BottomBar(),
  265. ),
  266. );
  267. }
  268. }
  269. class _BottomBar extends StatelessWidget {
  270. const _BottomBar({Key? key}) : super(key: key);
  271. @override
  272. Widget build(BuildContext context) {
  273. final state = WAlbumView._of(context)!;
  274. final countText =
  275. state.multi ? ' ${state.values.length}/${state.widget.maxCount}' : '';
  276. return BottomAppBar(
  277. child: Wisdom.row(
  278. padding: EdgeInsets.symmetric(horizontal: 15),
  279. children: [
  280. // Text('预览'),
  281. // Text('原图'),
  282. Spacer(),
  283. WLoadingButton(
  284. remain: true,
  285. onPressed: state.values.length == 0 ? null : () => state.pop(),
  286. padding: EdgeInsets.symmetric(horizontal: 10),
  287. height: 30.pt,
  288. minWidth: 50.pt,
  289. child: Text(
  290. '确定' + countText,
  291. ),
  292. ),
  293. ],
  294. ),
  295. );
  296. }
  297. }
  298. class _GridItem extends StatelessWidget {
  299. _GridItem({
  300. Key? key,
  301. this.data,
  302. this.color,
  303. }) : super(key: key);
  304. final AssetEntity? data;
  305. final Color? color;
  306. @override
  307. Widget build(BuildContext context) {
  308. final state = WAlbumView._of(context)!;
  309. final active = state.values.indexOf(data) != -1;
  310. // final title = data.title ?? '';
  311. // final formatList = title.split('.');
  312. // final format = formatList.length > 0 ? formatList.last.toUpperCase() : '';
  313. Widget current;
  314. if (state.cache[data!.id] != null)
  315. current = WImage.memory(
  316. state.cache[data!.id]!,
  317. fit: BoxFit.cover,
  318. );
  319. else
  320. current = FutureBuilder<Uint8List?>(
  321. future: state.onCache(data!),
  322. builder: (_, snapshot) {
  323. if (snapshot.connectionState == ConnectionState.done) {
  324. switch (data?.type) {
  325. case AssetType.image:
  326. return WImage.memory(
  327. snapshot.data ?? Uint8List(0),
  328. fit: BoxFit.cover,
  329. );
  330. default:
  331. return ColoredBox(color: Colors.white);
  332. }
  333. } else {
  334. return ColoredBox(color: Colors.white);
  335. }
  336. },
  337. );
  338. Widget? dot;
  339. if (active)
  340. dot = state.widget.maxCount != 1
  341. ? Text(
  342. '${state.values.indexOf(data) + 1}',
  343. style: TextStyle(
  344. fontSize: 11,
  345. fontWeight: FontWeight.bold,
  346. color: Colors.white,
  347. ),
  348. )
  349. : Icon(
  350. Icons.check,
  351. color: Colors.white,
  352. size: 15,
  353. );
  354. dot = Wisdom(
  355. alignment: Alignment.center,
  356. minWidth: 22,
  357. minHeight: 22,
  358. color: active ? color : Colors.black.withOpacity(0.1),
  359. border: active ? null : Border.all(color: Colors.white, width: 2),
  360. borderRadius: BorderRadius.circular(11),
  361. child: dot,
  362. );
  363. current = GestureDetector(
  364. child: Stack(
  365. alignment: Alignment.center,
  366. fit: StackFit.expand,
  367. children: [
  368. current,
  369. if (active)
  370. Positioned.fill(
  371. child: ColoredBox(
  372. color: Colors.black.withOpacity(0.4),
  373. ),
  374. ),
  375. Positioned(
  376. right: 0,
  377. top: 0,
  378. child: Wisdom(
  379. onTap: () => state.onPick(data),
  380. padding: EdgeInsets.all(4),
  381. child: dot,
  382. ),
  383. ),
  384. ],
  385. ),
  386. );
  387. return current;
  388. }
  389. }