lazy_loading_gallery.dart 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import 'dart:async';
  2. import 'dart:math';
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/core/constants.dart';
  8. import 'package:photos/events/files_updated_event.dart';
  9. import 'package:photos/models/file.dart';
  10. import 'package:photos/models/selected_files.dart';
  11. import 'package:photos/ui/detail_page.dart';
  12. import 'package:photos/ui/gallery.dart';
  13. import 'package:photos/ui/huge_listview/place_holder_widget.dart';
  14. import 'package:photos/ui/thumbnail_widget.dart';
  15. import 'package:photos/utils/date_time_util.dart';
  16. import 'package:photos/utils/navigation_util.dart';
  17. import 'package:visibility_detector/visibility_detector.dart';
  18. class LazyLoadingGallery extends StatefulWidget {
  19. final List<File> files;
  20. final int index;
  21. final Stream<FilesUpdatedEvent> reloadEvent;
  22. final Set<EventType> removalEventTypes;
  23. final GalleryLoader asyncLoader;
  24. final SelectedFiles selectedFiles;
  25. final String tag;
  26. final Stream<int> currentIndexStream;
  27. LazyLoadingGallery(
  28. this.files,
  29. this.index,
  30. this.reloadEvent,
  31. this.removalEventTypes,
  32. this.asyncLoader,
  33. this.selectedFiles,
  34. this.tag,
  35. this.currentIndexStream, {
  36. Key key,
  37. }) : super(key: key ?? UniqueKey());
  38. @override
  39. _LazyLoadingGalleryState createState() => _LazyLoadingGalleryState();
  40. }
  41. class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
  42. static const kSubGalleryItemLimit = 80;
  43. static const kRecycleLimit = 400;
  44. static const kNumberOfDaysToRenderBeforeAndAfter = 8;
  45. static final Logger _logger = Logger("LazyLoadingGallery");
  46. List<File> _files;
  47. StreamSubscription<FilesUpdatedEvent> _reloadEventSubscription;
  48. StreamSubscription<int> _currentIndexSubscription;
  49. bool _shouldRender;
  50. @override
  51. void initState() {
  52. super.initState();
  53. _init();
  54. }
  55. void _init() {
  56. _shouldRender = true;
  57. _files = widget.files;
  58. _reloadEventSubscription = widget.reloadEvent.listen((e) => _onReload(e));
  59. _currentIndexSubscription =
  60. widget.currentIndexStream.listen((currentIndex) {
  61. bool shouldRender = (currentIndex - widget.index).abs() <
  62. kNumberOfDaysToRenderBeforeAndAfter;
  63. if (mounted && shouldRender != _shouldRender) {
  64. setState(() {
  65. _shouldRender = shouldRender;
  66. });
  67. }
  68. });
  69. }
  70. Future _onReload(FilesUpdatedEvent event) async {
  71. final galleryDate =
  72. DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime);
  73. final filesUpdatedThisDay = event.updatedFiles.where((file) {
  74. final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime);
  75. return fileDate.year == galleryDate.year &&
  76. fileDate.month == galleryDate.month &&
  77. fileDate.day == galleryDate.day;
  78. });
  79. if (filesUpdatedThisDay.isNotEmpty) {
  80. _logger.info(
  81. filesUpdatedThisDay.length.toString() +
  82. " files were updated on " +
  83. getDayTitle(galleryDate.microsecondsSinceEpoch),
  84. );
  85. if (event.type == EventType.addedOrUpdated) {
  86. final dayStartTime =
  87. DateTime(galleryDate.year, galleryDate.month, galleryDate.day);
  88. final result = await widget.asyncLoader(
  89. dayStartTime.microsecondsSinceEpoch,
  90. dayStartTime.microsecondsSinceEpoch + kMicroSecondsInDay - 1);
  91. if (mounted) {
  92. setState(() {
  93. _files = result.files;
  94. });
  95. }
  96. } else if (widget.removalEventTypes.contains(event.type)) {
  97. // Files were removed
  98. final updateFileIDs = <int>{};
  99. for (final file in filesUpdatedThisDay) {
  100. updateFileIDs.add(file.generatedID);
  101. }
  102. final List<File> files = [];
  103. files.addAll(_files);
  104. files.removeWhere((file) => updateFileIDs.contains(file.generatedID));
  105. if (mounted) {
  106. setState(() {
  107. _files = files;
  108. });
  109. }
  110. }
  111. }
  112. }
  113. @override
  114. void dispose() {
  115. _reloadEventSubscription.cancel();
  116. _currentIndexSubscription.cancel();
  117. super.dispose();
  118. }
  119. @override
  120. void didUpdateWidget(LazyLoadingGallery oldWidget) {
  121. super.didUpdateWidget(oldWidget);
  122. if (!listEquals(_files, widget.files)) {
  123. _reloadEventSubscription.cancel();
  124. _init();
  125. }
  126. }
  127. @override
  128. Widget build(BuildContext context) {
  129. if (_files.isEmpty) {
  130. return Container();
  131. }
  132. return Padding(
  133. padding: const EdgeInsets.only(bottom: 12),
  134. child: Column(
  135. children: [
  136. getDayWidget(context, _files[0].creationTime),
  137. _shouldRender ? _getGallery() : PlaceHolderWidget(_files.length),
  138. ],
  139. ),
  140. );
  141. }
  142. Widget _getGallery() {
  143. List<Widget> childGalleries = [];
  144. for (int index = 0; index < _files.length; index += kSubGalleryItemLimit) {
  145. childGalleries.add(LazyLoadingGridView(
  146. widget.tag,
  147. _files.sublist(index, min(index + kSubGalleryItemLimit, _files.length)),
  148. widget.asyncLoader,
  149. widget.selectedFiles,
  150. index == 0,
  151. _files.length > kRecycleLimit,
  152. ));
  153. }
  154. return Column(
  155. children: childGalleries,
  156. );
  157. }
  158. }
  159. class LazyLoadingGridView extends StatefulWidget {
  160. final String tag;
  161. final List<File> files;
  162. final GalleryLoader asyncLoader;
  163. final SelectedFiles selectedFiles;
  164. final bool shouldRender;
  165. final bool shouldRecycle;
  166. LazyLoadingGridView(
  167. this.tag,
  168. this.files,
  169. this.asyncLoader,
  170. this.selectedFiles,
  171. this.shouldRender,
  172. this.shouldRecycle, {
  173. Key key,
  174. }) : super(key: key ?? UniqueKey());
  175. @override
  176. _LazyLoadingGridViewState createState() => _LazyLoadingGridViewState();
  177. }
  178. class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
  179. bool _shouldRender;
  180. @override
  181. void initState() {
  182. super.initState();
  183. _shouldRender = widget.shouldRender;
  184. widget.selectedFiles.addListener(() {
  185. bool shouldRefresh = false;
  186. for (final file in widget.files) {
  187. if (widget.selectedFiles.lastSelections.contains(file)) {
  188. shouldRefresh = true;
  189. }
  190. }
  191. if (shouldRefresh && mounted) {
  192. setState(() {});
  193. }
  194. });
  195. }
  196. @override
  197. void didUpdateWidget(LazyLoadingGridView oldWidget) {
  198. super.didUpdateWidget(oldWidget);
  199. if (!listEquals(widget.files, oldWidget.files)) {
  200. _shouldRender = widget.shouldRender;
  201. }
  202. }
  203. @override
  204. Widget build(BuildContext context) {
  205. if (widget.shouldRecycle) {
  206. return _getRecyclableView();
  207. } else {
  208. return _getNonRecyclableView();
  209. }
  210. }
  211. Widget _getRecyclableView() {
  212. return VisibilityDetector(
  213. key: UniqueKey(),
  214. onVisibilityChanged: (visibility) {
  215. final shouldRender = visibility.visibleFraction > 0;
  216. if (mounted && shouldRender != _shouldRender) {
  217. setState(() {
  218. _shouldRender = shouldRender;
  219. });
  220. }
  221. },
  222. child: _shouldRender
  223. ? _getGridView()
  224. : PlaceHolderWidget(widget.files.length),
  225. );
  226. }
  227. Widget _getNonRecyclableView() {
  228. if (!_shouldRender) {
  229. return VisibilityDetector(
  230. key: UniqueKey(),
  231. onVisibilityChanged: (visibility) {
  232. if (mounted && visibility.visibleFraction > 0 && !_shouldRender) {
  233. setState(() {
  234. _shouldRender = true;
  235. });
  236. }
  237. },
  238. child: PlaceHolderWidget(widget.files.length),
  239. );
  240. } else {
  241. return _getGridView();
  242. }
  243. }
  244. Widget _getGridView() {
  245. return GridView.builder(
  246. shrinkWrap: true,
  247. physics:
  248. NeverScrollableScrollPhysics(), // to disable GridView's scrolling
  249. itemBuilder: (context, index) {
  250. return _buildFile(context, widget.files[index]);
  251. },
  252. itemCount: widget.files.length,
  253. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  254. crossAxisCount: 4,
  255. ),
  256. padding: EdgeInsets.all(0),
  257. );
  258. }
  259. Widget _buildFile(BuildContext context, File file) {
  260. return GestureDetector(
  261. onTap: () {
  262. if (widget.selectedFiles.files.isNotEmpty) {
  263. _selectFile(file);
  264. } else {
  265. _routeToDetailPage(file, context);
  266. }
  267. },
  268. onLongPress: () {
  269. HapticFeedback.lightImpact();
  270. _selectFile(file);
  271. },
  272. child: Container(
  273. margin: const EdgeInsets.all(1.5),
  274. decoration: BoxDecoration(
  275. borderRadius: BorderRadius.circular(3),
  276. border: widget.selectedFiles.files.contains(file)
  277. ? Border.all(
  278. width: 4.0,
  279. color: Theme.of(context).buttonColor,
  280. )
  281. : null,
  282. ),
  283. child: ClipRRect(
  284. borderRadius: BorderRadius.circular(3),
  285. child: Hero(
  286. tag: widget.tag + file.tag(),
  287. child: ThumbnailWidget(
  288. file,
  289. diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration,
  290. serverLoadDeferDuration: kThumbnailServerLoadDeferDuration,
  291. shouldShowLivePhotoOverlay: true,
  292. key: Key(widget.tag + file.tag()),
  293. ),
  294. ),
  295. ),
  296. ),
  297. );
  298. }
  299. void _selectFile(File file) {
  300. widget.selectedFiles.toggleSelection(file);
  301. }
  302. void _routeToDetailPage(File file, BuildContext context) {
  303. final page = DetailPage(DetailPageConfiguration(
  304. List.unmodifiable(widget.files),
  305. widget.asyncLoader,
  306. widget.files.indexOf(file),
  307. widget.tag,
  308. ));
  309. routeToPage(context, page);
  310. }
  311. }