Skip to content

Commit 9a68066

Browse files
✨ Add keepScrollOffset feature for the AssetPickerBuilderDelegate (#172)
Co-authored-by: Yaniv Shaked <[email protected]>
1 parent 04885e7 commit 9a68066

File tree

6 files changed

+114
-7
lines changed

6 files changed

+114
-7
lines changed

example/lib/constants/picker_method.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,32 @@ class PickMethod {
248248
);
249249
}
250250

251+
factory PickMethod.keepScrollOffset(
252+
DefaultAssetPickerProvider provider,
253+
DefaultAssetPickerBuilderDelegate Function() delegate,
254+
Function(PermissionState state) onPermission,
255+
) {
256+
return PickMethod(
257+
icon: '💾',
258+
name: 'Keep scroll offset',
259+
description: 'Pick assets from same scroll position.',
260+
method: (BuildContext context, List<AssetEntity> assets) async {
261+
final PermissionState _ps =
262+
await PhotoManager.requestPermissionExtend();
263+
if (_ps != PermissionState.authorized &&
264+
_ps != PermissionState.limited) {
265+
throw StateError('Permission state error with $_ps.');
266+
}
267+
onPermission(_ps);
268+
return AssetPicker.pickAssetsWithDelegate(
269+
context,
270+
provider: provider,
271+
delegate: delegate(),
272+
);
273+
},
274+
);
275+
}
276+
251277
final String icon;
252278
final String name;
253279
final String description;

example/lib/pages/multi_assets_page.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ class _MultiAssetsPageState extends State<MultiAssetsPage>
2727

2828
ThemeData get currentTheme => context.themeData;
2929

30+
/// These fields are for the keep scroll position feature.
31+
late final DefaultAssetPickerProvider _keepScrollProvider =
32+
DefaultAssetPickerProvider(
33+
selectedAssets: assets,
34+
);
35+
DefaultAssetPickerBuilderDelegate? _keepScrollDelegate;
36+
3037
List<PickMethod> get pickMethods {
3138
return <PickMethod>[
3239
PickMethod.image(maxAssetsCount),
@@ -55,6 +62,17 @@ class _MultiAssetsPageState extends State<MultiAssetsPage>
5562
},
5663
),
5764
PickMethod.noPreview(maxAssetsCount),
65+
PickMethod.keepScrollOffset(
66+
_keepScrollProvider,
67+
() => _keepScrollDelegate!,
68+
(PermissionState state) {
69+
_keepScrollDelegate ??= DefaultAssetPickerBuilderDelegate(
70+
provider: _keepScrollProvider,
71+
initialPermission: state,
72+
keepScrollOffset: true,
73+
);
74+
},
75+
),
5876
PickMethod(
5977
icon: '🎚',
6078
name: 'Custom image preview thumb size',

example/lib/pages/single_assets_page.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ class _SingleAssetPageState extends State<SingleAssetPage>
2727

2828
ThemeData get currentTheme => context.themeData;
2929

30+
/// These fields are for the keep scroll position feature.
31+
late final DefaultAssetPickerProvider _keepScrollProvider =
32+
DefaultAssetPickerProvider(
33+
selectedAssets: assets,
34+
);
35+
DefaultAssetPickerBuilderDelegate? _keepScrollDelegate;
36+
3037
List<PickMethod> get pickMethods {
3138
return <PickMethod>[
3239
PickMethod.image(maxAssetsCount),
@@ -43,6 +50,17 @@ class _SingleAssetPageState extends State<SingleAssetPage>
4350
PickMethod.customFilterOptions(maxAssetsCount),
4451
PickMethod.prependItem(maxAssetsCount),
4552
PickMethod.noPreview(maxAssetsCount),
53+
PickMethod.keepScrollOffset(
54+
_keepScrollProvider,
55+
() => _keepScrollDelegate!,
56+
(PermissionState state) {
57+
_keepScrollDelegate ??= DefaultAssetPickerBuilderDelegate(
58+
provider: _keepScrollProvider,
59+
initialPermission: state,
60+
keepScrollOffset: true,
61+
);
62+
},
63+
),
4664
];
4765
}
4866

lib/src/constants/constants.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import 'dart:developer';
77
import 'package:flutter/foundation.dart';
88
import 'package:flutter/widgets.dart';
99

10-
import 'constants.dart';
10+
import '../delegates/assets_picker_text_delegate.dart';
11+
import '../delegates/sort_path_delegate.dart';
1112

1213
export 'package:photo_manager/photo_manager.dart';
1314
export 'package:provider/provider.dart';
@@ -45,6 +46,12 @@ class Constants {
4546
static SortPathDelegate<dynamic> sortPathDelegate =
4647
SortPathDelegate.common;
4748

49+
/// The last scroll position where the picker scrolled.
50+
///
51+
/// See also:
52+
/// * [AssetPickerBuilderDelegate.keepScrollOffset]
53+
static ScrollPosition? scrollPosition;
54+
4855
static const int defaultGridThumbSize = 200;
4956
}
5057

lib/src/delegates/asset_picker_builder_delegate.dart

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:ui' as ui;
99

1010
import 'package:flutter/material.dart';
1111
import 'package:flutter/gestures.dart';
12+
import 'package:flutter/scheduler.dart';
1213
import 'package:flutter/services.dart';
1314

1415
import '../constants/constants.dart';
@@ -38,13 +39,18 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
3839
this.specialItemBuilder,
3940
this.loadingIndicatorBuilder,
4041
this.allowSpecialItemWhenEmpty = false,
42+
this.keepScrollOffset = false,
4143
}) : assert(
4244
pickerTheme == null || themeColor == null,
4345
'Theme and theme color cannot be set at the same time.',
4446
),
4547
themeColor =
4648
pickerTheme?.colorScheme.secondary ?? themeColor ?? C.themeColor {
4749
Constants.textDelegate = textDelegate ?? AssetsPickerTextDelegate();
50+
// Add the listener if [keepScrollOffset] is true.
51+
if (keepScrollOffset) {
52+
gridScrollController.addListener(keepScrollOffsetListener);
53+
}
4854
}
4955

5056
/// [ChangeNotifier] for asset picker.
@@ -89,6 +95,10 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
8995
/// 当没有资源时是否显示自定义item
9096
final bool allowSpecialItemWhenEmpty;
9197

98+
/// Whether the picker should save the scroll offset between pushes and pops.
99+
/// 选择器是否可以从同样的位置开始选择
100+
final bool keepScrollOffset;
101+
92102
/// The [ScrollController] for the preview grid.
93103
final ScrollController gridScrollController = ScrollController();
94104

@@ -163,9 +173,24 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
163173
/// 当前的权限是否为受限
164174
bool get isPermissionLimited => permission.value == PermissionState.limited;
165175

176+
/// The listener to track the scroll position of the [gridScrollController]
177+
/// if [keepScrollOffset] is true.
178+
/// 当 [keepScrollOffset] 为 true 时,跟踪 [gridScrollController] 位置的监听。
179+
void keepScrollOffsetListener() {
180+
if (gridScrollController.hasClients) {
181+
Constants.scrollPosition = gridScrollController.position;
182+
}
183+
}
184+
166185
/// Keep a dispose method to sync with [State].
167186
/// 保留一个 dispose 方法与 [State] 同步。
187+
///
188+
/// Be aware that the method will do nothing when [keepScrollOffset] is true.
189+
/// 注意当 [keepScrollOffset] 为 true 时方法不会进行释放。
168190
void dispose() {
191+
if (keepScrollOffset) {
192+
return;
193+
}
169194
gridScrollController.dispose();
170195
permission.dispose();
171196
permissionOverlayHidden.dispose();
@@ -559,6 +584,15 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
559584
/// Yes, the build method.
560585
/// 没错,是它是它就是它,我们亲爱的 build 方法~
561586
Widget build(BuildContext context) {
587+
// Schedule the scroll position's restoration callback if this feature
588+
// is enabled and offsets are different.
589+
if (keepScrollOffset &&
590+
Constants.scrollPosition != null &&
591+
!gridScrollController.hasClients) {
592+
SchedulerBinding.instance!.addPostFrameCallback((_) {
593+
gridScrollController.jumpTo(Constants.scrollPosition!.pixels);
594+
});
595+
}
562596
return AnnotatedRegion<SystemUiOverlayStyle>(
563597
value: overlayStyle,
564598
child: Theme(
@@ -594,6 +628,7 @@ class DefaultAssetPickerBuilderDelegate
594628
WidgetBuilder? specialItemBuilder,
595629
IndicatorBuilder? loadingIndicatorBuilder,
596630
bool allowSpecialItemWhenEmpty = false,
631+
bool keepScrollOffset = false,
597632
this.gridThumbSize = Constants.defaultGridThumbSize,
598633
this.previewThumbSize,
599634
this.specialPickerType,
@@ -612,6 +647,7 @@ class DefaultAssetPickerBuilderDelegate
612647
specialItemBuilder: specialItemBuilder,
613648
loadingIndicatorBuilder: loadingIndicatorBuilder,
614649
allowSpecialItemWhenEmpty: allowSpecialItemWhenEmpty,
650+
keepScrollOffset: keepScrollOffset,
615651
);
616652

617653
/// Thumbnail size in the grid.
@@ -1121,7 +1157,7 @@ class DefaultAssetPickerBuilderDelegate
11211157
),
11221158
onPressed: () {
11231159
if (provider.isSelectedNotEmpty) {
1124-
Navigator.of(context).pop(provider.selectedAssets);
1160+
Navigator.of(context).maybePop(provider.selectedAssets);
11251161
}
11261162
},
11271163
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
@@ -1512,7 +1548,7 @@ class DefaultAssetPickerBuilderDelegate
15121548
maxAssets: provider.maxAssets,
15131549
);
15141550
if (result != null) {
1515-
Navigator.of(context).pop(result);
1551+
Navigator.of(context).maybePop(result);
15161552
}
15171553
},
15181554
child: Selector<DefaultAssetPickerProvider, String>(
@@ -1599,7 +1635,7 @@ class DefaultAssetPickerBuilderDelegate
15991635
}
16001636
provider.selectAsset(asset);
16011637
if (isSingleAssetMode && !isPreviewEnabled) {
1602-
Navigator.of(context).pop(provider.selectedAssets);
1638+
Navigator.of(context).maybePop(provider.selectedAssets);
16031639
}
16041640
},
16051641
child: Container(
@@ -1680,7 +1716,7 @@ class DefaultAssetPickerBuilderDelegate
16801716
maxAssets: provider.maxAssets,
16811717
);
16821718
if (result != null) {
1683-
Navigator.of(context).pop(result);
1719+
Navigator.of(context).maybePop(result);
16841720
}
16851721
},
16861722
child: Selector<DefaultAssetPickerProvider, List<AssetEntity>>(

lib/src/widget/asset_picker.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,10 @@ class AssetPicker<Asset, Path> extends StatefulWidget {
143143

144144
final Widget picker = ChangeNotifierProvider<PickerProvider>.value(
145145
value: provider,
146-
child:
147-
AssetPicker<Asset, Path>(key: Constants.pickerKey, builder: delegate),
146+
child: AssetPicker<Asset, Path>(
147+
key: Constants.pickerKey,
148+
builder: delegate,
149+
),
148150
);
149151
final List<Asset>? result = await Navigator.of(
150152
context,

0 commit comments

Comments
 (0)