From 9f29bce308adf10787aa0657ecb8590a406c6855 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:12:18 +0530 Subject: [PATCH] refactor: grid --- .../repositories/renderlist.repository.dart | 6 +- .../components/grid/draggable_scrollbar.dart | 73 ++++++----- .../grid/immich_asset_grid.widget.dart | 114 +++++++++--------- mobile-v2/lib/service_locator.dart | 7 +- mobile-v2/pubspec.lock | 16 +-- mobile-v2/pubspec.yaml | 2 +- 6 files changed, 120 insertions(+), 98 deletions(-) diff --git a/mobile-v2/lib/domain/repositories/renderlist.repository.dart b/mobile-v2/lib/domain/repositories/renderlist.repository.dart index 51cc637147..347984a825 100644 --- a/mobile-v2/lib/domain/repositories/renderlist.repository.dart +++ b/mobile-v2/lib/domain/repositories/renderlist.repository.dart @@ -41,6 +41,10 @@ class RenderListDriftRepository with LogMixin implements IRenderListRepository { ]; }) .watch() - .map((elements) => RenderList(elements: elements)); + .map((elements) { + // Resets the value in closure so the watch refresh will work properly + lastAssetOffset = 0; + return RenderList(elements: elements); + }); } } diff --git a/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart b/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart index 662403b2e8..5712c283a2 100644 --- a/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart +++ b/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:flutter_list_view/flutter_list_view.dart'; /// Build the Scroll Thumb and label using the current configuration typedef ScrollThumbBuilder = Widget Function( @@ -24,9 +24,12 @@ typedef LabelTextBuilder = Text? Function(int item); /// for quick navigation of the BoxScrollView. class DraggableScrollbar extends StatefulWidget { /// The view that will be scrolled with the scroll thumb - final ScrollablePositionedList child; + final CustomScrollView child; - final ItemPositionsListener itemPositionsListener; + /// Total number of children in the list + final int maxItemCount; + + final FlutterListViewController controller; /// A function that builds a thumb using the current configuration final ScrollThumbBuilder scrollThumbBuilder; @@ -55,9 +58,6 @@ class DraggableScrollbar extends StatefulWidget { /// Determines box constraints for Container displaying label final BoxConstraints? labelConstraints; - /// The ScrollController for the BoxScrollView - final ItemScrollController controller; - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] final bool alwaysVisibleScrollThumb; @@ -69,7 +69,7 @@ class DraggableScrollbar extends StatefulWidget { this.alwaysVisibleScrollThumb = false, required this.child, required this.controller, - required this.itemPositionsListener, + required this.maxItemCount, required this.scrollStateListener, this.heightScrollThumb = 48.0, this.backgroundColor = Colors.white, @@ -217,6 +217,7 @@ class _DraggableScrollbarState extends State late AnimationController _labelAnimationController; late Animation _labelAnimation; Timer? _fadeoutTimer; + List _positions = []; @override void initState() { @@ -244,6 +245,11 @@ class _DraggableScrollbarState extends State parent: _labelAnimationController, curve: Curves.fastOutSlowIn, ); + + widget.controller.sliverController.onPaintItemPositionsCallback = + (height, pos) { + _positions = pos; + }; } @override @@ -300,9 +306,10 @@ class _DraggableScrollbarState extends State double get _barMaxScrollExtent => (context.size?.height ?? 0) - widget.heightScrollThumb; - double get _barMinScrollExtent => 0; + double get _maxScrollRatio => + _barMaxScrollExtent / widget.controller.position.maxScrollExtent; - int get maxItemCount => widget.child.itemCount; + double get _barMinScrollExtent => 0; bool _onScrollNotification(ScrollNotification notification) { _changePosition(notification); @@ -326,11 +333,7 @@ class _DraggableScrollbarState extends State setState(() { try { if (notification is ScrollUpdateNotification) { - int? firstItemIndex = widget - .itemPositionsListener.itemPositions.value.firstOrNull?.index; - if (firstItemIndex != null) { - _barOffset = (firstItemIndex / maxItemCount) * _barMaxScrollExtent; - } + _barOffset = widget.controller.offset * _maxScrollRatio; _barOffset = clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent); @@ -342,8 +345,9 @@ class _DraggableScrollbarState extends State _thumbAnimationController.forward(); } - if (itemPos < maxItemCount) { - _currentItem = itemPos; + final lastItemPos = itemPos; + if (lastItemPos < widget.maxItemCount) { + _currentItem = lastItemPos; } _fadeoutTimer?.cancel(); @@ -363,26 +367,30 @@ class _DraggableScrollbarState extends State widget.scrollStateListener(true); } - int get itemPos { - int numberOfItems = widget.child.itemCount; - return ((_barOffset / (_barMaxScrollExtent)) * numberOfItems).toInt(); + int get itemIndex { + int index = 0; + double minDiff = 1000; + for (final pos in _positions) { + final diff = (_barOffset - pos.offset).abs(); + if (diff < minDiff) { + minDiff = diff; + index = pos.index; + } + } + return index; } + int get itemPos => + ((_barOffset / (_barMaxScrollExtent)) * widget.maxItemCount).toInt(); + void _jumpToBarPos() { - if (itemPos > maxItemCount - 1) { + final lastItemPos = itemPos; + if (lastItemPos > widget.maxItemCount - 1) { return; } - _currentItem = itemPos; - - final alignment = (_barOffset / _barMaxScrollExtent); - - widget.controller.jumpTo( - index: _currentItem, - // // Align at the top or middle while scrolling, but always align at the top while - // // towards the end. - alignment: alignment > 0.95 ? 0 : clampDouble(alignment - 0.2, 0, 1), - ); + _currentItem = itemIndex; + widget.controller.sliverController.jumpToIndex(lastItemPos); } Timer? _dragHaltTimer; @@ -399,8 +407,9 @@ class _DraggableScrollbarState extends State _barOffset = clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent); - if (itemPos != lastTimerPos) { - lastTimerPos = itemPos; + final lastItemPos = itemPos; + if (lastItemPos != lastTimerPos) { + lastTimerPos = lastItemPos; _dragHaltTimer?.cancel(); widget.scrollStateListener(true); diff --git a/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart b/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart index 8bb686b7b5..416ee6bc53 100644 --- a/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart +++ b/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_list_view/flutter_list_view.dart'; import 'package:immich_mobile/domain/models/render_list.model.dart'; import 'package:immich_mobile/domain/models/render_list_element.model.dart'; import 'package:immich_mobile/presentation/components/grid/draggable_scrollbar.dart'; @@ -10,7 +11,6 @@ import 'package:immich_mobile/utils/extensions/build_context.extension.dart'; import 'package:immich_mobile/utils/extensions/color.extension.dart'; import 'package:intl/intl.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; part 'immich_asset_grid_header.widget.dart'; part 'immich_grid_asset_placeholder.widget.dart'; @@ -24,9 +24,13 @@ class ImAssetGrid extends StatefulWidget { class _ImAssetGridState extends State { bool _isDragScrolling = false; - final ItemScrollController _itemScrollController = ItemScrollController(); - final ItemPositionsListener _itemPositionsListener = - ItemPositionsListener.create(); + final FlutterListViewController _controller = FlutterListViewController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } void _onDragScrolling(bool isScrolling) { if (_isDragScrolling != isScrolling) { @@ -56,62 +60,64 @@ class _ImAssetGridState extends State { BlocBuilder( builder: (_, renderList) { final elements = renderList.elements; - final grid = ScrollablePositionedList.builder( - itemCount: elements.length, - addAutomaticKeepAlives: false, - minCacheExtent: 100, - itemPositionsListener: _itemPositionsListener, - itemScrollController: _itemScrollController, - itemBuilder: (_, sectionIndex) { - final section = elements[sectionIndex]; + final grid = FlutterListView( + controller: _controller, + delegate: FlutterListViewDelegate( + (_, sectionIndex) { + // ignore: avoid-unsafe-collection-methods + final section = elements[sectionIndex]; - return switch (section) { - RenderListMonthHeaderElement() => - _MonthHeader(text: section.header), - RenderListDayHeaderElement() => Text(section.header), - RenderListAssetElement() => FutureBuilder( - future: context.read().loadAssets( - section.assetOffset, - section.assetCount, - ), - builder: (_, assetsSnap) { - final assets = assetsSnap.data; - return GridView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - addAutomaticKeepAlives: false, - cacheExtent: 100, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - mainAxisSpacing: 3, - crossAxisSpacing: 3, - ), - itemBuilder: (_, i) { - final asset = assetsSnap.isWaiting || assets == null - ? null - : assets.elementAtOrNull(i); - return SizedBox.square( - dimension: 200, - // Show Placeholder when drag scrolled - child: asset == null || _isDragScrolling - ? const _ImImagePlaceholder() - : ImImage(asset), - ); - }, - itemCount: section.assetCount, - ); - }, - ), - }; - }, + return switch (section) { + RenderListMonthHeaderElement() => + _MonthHeader(text: section.header), + RenderListDayHeaderElement() => Text(section.header), + RenderListAssetElement() => FutureBuilder( + future: context.read().loadAssets( + section.assetOffset, + section.assetCount, + ), + builder: (_, assetsSnap) { + final assets = assetsSnap.data; + return GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + addAutomaticKeepAlives: false, + cacheExtent: 100, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 3, + crossAxisSpacing: 3, + ), + itemBuilder: (_, i) { + final asset = assetsSnap.isWaiting || assets == null + ? null + : assets.elementAtOrNull(i); + return SizedBox.square( + dimension: 200, + // Show Placeholder when drag scrolled + child: asset == null || _isDragScrolling + ? const _ImImagePlaceholder() + : ImImage(asset), + ); + }, + itemCount: section.assetCount, + ); + }, + ), + }; + }, + childCount: elements.length, + addAutomaticKeepAlives: false, + ), ); + return DraggableScrollbar( foregroundColor: context.colorScheme.onSurface, backgroundColor: context.colorScheme.surfaceContainerHighest, scrollStateListener: _onDragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, + controller: _controller, + maxItemCount: elements.length, labelTextBuilder: (int position) => _labelBuilder(elements, position), labelConstraints: const BoxConstraints(maxHeight: 36), diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart index adc52c4816..66dffced39 100644 --- a/mobile-v2/lib/service_locator.dart +++ b/mobile-v2/lib/service_locator.dart @@ -1,12 +1,14 @@ import 'package:get_it/get_it.dart'; import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart'; +import 'package:immich_mobile/domain/interfaces/renderlist.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/repositories/asset.repository.dart'; import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/domain/repositories/log.repository.dart'; +import 'package:immich_mobile/domain/repositories/renderlist.repository.dart'; import 'package:immich_mobile/domain/repositories/store.repository.dart'; import 'package:immich_mobile/domain/repositories/user.repository.dart'; import 'package:immich_mobile/domain/services/app_setting.service.dart'; @@ -68,8 +70,9 @@ class ServiceLocator { _registerFactory(() => LogDriftRepository(di())); _registerFactory(() => AppSettingService(di())); _registerFactory(() => UserDriftRepository(di())); - _registerFactory( - () => AssetDriftRepository(di()), + _registerFactory(() => AssetDriftRepository(di())); + _registerFactory( + () => RenderListDriftRepository(di()), ); /// Services diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock index f71b5eca8b..57689471eb 100644 --- a/mobile-v2/pubspec.lock +++ b/mobile-v2/pubspec.lock @@ -403,6 +403,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_list_view: + dependency: "direct main" + description: + name: flutter_list_view + sha256: d99dc310bb3ca9d688dae1585b38a3f56e7b06eeb085ae27a5e0f56cf52172c5 + url: "https://pub.dev" + source: hosted + version: "1.1.28" flutter_localizations: dependency: "direct main" description: flutter @@ -841,14 +849,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" - scrollable_positioned_list: - dependency: "direct main" - description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" - url: "https://pub.dev" - source: hosted - version: "0.3.8" shelf: dependency: transitive description: diff --git a/mobile-v2/pubspec.yaml b/mobile-v2/pubspec.yaml index 0314d223cb..b04f990b0e 100644 --- a/mobile-v2/pubspec.yaml +++ b/mobile-v2/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: # components material_symbols_icons: ^4.2785.1 flutter_adaptive_scaffold: ^0.3.1 - scrollable_positioned_list: ^0.3.8 + flutter_list_view: ^1.1.28 cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1