album grid prep
This commit is contained in:
parent
229b2e7b2b
commit
f952deff15
18 changed files with 339 additions and 226 deletions
|
@ -5,6 +5,7 @@ import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
@ -22,7 +23,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
List<ImageEntry> _filteredEntries;
|
List<ImageEntry> _filteredEntries;
|
||||||
List<StreamSubscription> _subscriptions = [];
|
List<StreamSubscription> _subscriptions = [];
|
||||||
|
|
||||||
Map<dynamic, List<ImageEntry>> sections = Map.unmodifiable({});
|
Map<SectionKey, List<ImageEntry>> sections = Map.unmodifiable({});
|
||||||
|
|
||||||
CollectionLens({
|
CollectionLens({
|
||||||
@required this.source,
|
@required this.source,
|
||||||
|
@ -138,13 +139,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
case EntrySortFactor.date:
|
case EntrySortFactor.date:
|
||||||
switch (groupFactor) {
|
switch (groupFactor) {
|
||||||
case EntryGroupFactor.album:
|
case EntryGroupFactor.album:
|
||||||
sections = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory);
|
sections = groupBy<ImageEntry, AlbumSectionKey>(_filteredEntries, (entry) => AlbumSectionKey(entry.directory));
|
||||||
break;
|
break;
|
||||||
case EntryGroupFactor.month:
|
case EntryGroupFactor.month:
|
||||||
sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.monthTaken);
|
sections = groupBy<ImageEntry, DateSectionKey>(_filteredEntries, (entry) => DateSectionKey(entry.monthTaken));
|
||||||
break;
|
break;
|
||||||
case EntryGroupFactor.day:
|
case EntryGroupFactor.day:
|
||||||
sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.dayTaken);
|
sections = groupBy<ImageEntry, DateSectionKey>(_filteredEntries, (entry) => DateSectionKey(entry.dayTaken));
|
||||||
break;
|
break;
|
||||||
case EntryGroupFactor.none:
|
case EntryGroupFactor.none:
|
||||||
sections = Map.fromEntries([
|
sections = Map.fromEntries([
|
||||||
|
@ -159,8 +160,8 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
final byAlbum = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory);
|
final byAlbum = groupBy<ImageEntry, AlbumSectionKey>(_filteredEntries, (entry) => AlbumSectionKey(entry.directory));
|
||||||
sections = SplayTreeMap<String, List<ImageEntry>>.of(byAlbum, source.compareAlbumsByName);
|
sections = SplayTreeMap<AlbumSectionKey, List<ImageEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
sections = Map.unmodifiable(sections);
|
sections = Map.unmodifiable(sections);
|
||||||
|
|
41
lib/model/source/section_keys.dart
Normal file
41
lib/model/source/section_keys.dart
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class SectionKey {
|
||||||
|
const SectionKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumSectionKey extends SectionKey {
|
||||||
|
final String folderPath;
|
||||||
|
|
||||||
|
const AlbumSectionKey(this.folderPath);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is AlbumSectionKey && other.folderPath == folderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => folderPath.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType#${shortHash(this)}{folderPath=$folderPath}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateSectionKey extends SectionKey {
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
const DateSectionKey(this.date);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is DateSectionKey && other.date == date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => date.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType#${shortHash(this)}{date=$date}';
|
||||||
|
}
|
|
@ -1,17 +1,20 @@
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/collection/grid/header_generic.dart';
|
import 'package:aves/widgets/common/grid/header.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_icons.dart';
|
import 'package:aves/widgets/common/identity/aves_icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AlbumSectionHeader extends StatelessWidget {
|
class AlbumSectionHeader extends StatelessWidget {
|
||||||
final String folderPath, albumName;
|
final String folderPath, albumName;
|
||||||
|
|
||||||
const AlbumSectionHeader({
|
AlbumSectionHeader({
|
||||||
Key key,
|
Key key,
|
||||||
|
@required CollectionSource source,
|
||||||
@required this.folderPath,
|
@required this.folderPath,
|
||||||
@required this.albumName,
|
}) : albumName = source.getUniqueAlbumName(folderPath),
|
||||||
}) : super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -25,8 +28,8 @@ class AlbumSectionHeader extends StatelessWidget {
|
||||||
child: albumIcon,
|
child: albumIcon,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return TitleSectionHeader(
|
return SectionHeader(
|
||||||
sectionKey: folderPath,
|
sectionKey: AlbumSectionKey(folderPath),
|
||||||
leading: albumIcon,
|
leading: albumIcon,
|
||||||
title: albumName,
|
title: albumName,
|
||||||
trailing: androidFileUtils.isOnRemovableStorage(folderPath)
|
trailing: androidFileUtils.isOnRemovableStorage(folderPath)
|
||||||
|
@ -38,4 +41,15 @@ class AlbumSectionHeader extends StatelessWidget {
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, AlbumSectionKey sectionKey) {
|
||||||
|
final folderPath = sectionKey.folderPath;
|
||||||
|
return SectionHeader.getPreferredHeight(
|
||||||
|
context: context,
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
title: source.getUniqueAlbumName(folderPath),
|
||||||
|
hasLeading: androidFileUtils.getAlbumType(folderPath) != AlbumType.regular,
|
||||||
|
hasTrailing: androidFileUtils.isOnRemovableStorage(folderPath),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
74
lib/widgets/collection/grid/headers/any.dart
Normal file
74
lib/widgets/collection/grid/headers/any.dart
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/enums.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/collection/grid/headers/album.dart';
|
||||||
|
import 'package:aves/widgets/collection/grid/headers/date.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/header.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CollectionSectionHeader extends StatelessWidget {
|
||||||
|
final CollectionLens collection;
|
||||||
|
final SectionKey sectionKey;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
const CollectionSectionHeader({
|
||||||
|
Key key,
|
||||||
|
@required this.collection,
|
||||||
|
@required this.sectionKey,
|
||||||
|
@required this.height,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final header = _buildHeader();
|
||||||
|
return header != null
|
||||||
|
? SizedBox(
|
||||||
|
height: height,
|
||||||
|
child: header,
|
||||||
|
)
|
||||||
|
: SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
Widget _buildAlbumHeader() => AlbumSectionHeader(
|
||||||
|
key: ValueKey(sectionKey),
|
||||||
|
source: collection.source,
|
||||||
|
folderPath: (sectionKey as AlbumSectionKey).folderPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (collection.sortFactor) {
|
||||||
|
case EntrySortFactor.date:
|
||||||
|
switch (collection.groupFactor) {
|
||||||
|
case EntryGroupFactor.album:
|
||||||
|
return _buildAlbumHeader();
|
||||||
|
case EntryGroupFactor.month:
|
||||||
|
return MonthSectionHeader(key: ValueKey(sectionKey), date: (sectionKey as DateSectionKey).date);
|
||||||
|
case EntryGroupFactor.day:
|
||||||
|
return DaySectionHeader(key: ValueKey(sectionKey), date: (sectionKey as DateSectionKey).date);
|
||||||
|
case EntryGroupFactor.none:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EntrySortFactor.name:
|
||||||
|
return _buildAlbumHeader();
|
||||||
|
case EntrySortFactor.size:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, SectionKey sectionKey) {
|
||||||
|
var headerExtent = 0.0;
|
||||||
|
if (sectionKey is AlbumSectionKey) {
|
||||||
|
// only compute height for album headers, as they're the only likely ones to split on multiple lines
|
||||||
|
headerExtent = AlbumSectionHeader.getPreferredHeight(context, maxWidth, source, sectionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
|
headerExtent = max(headerExtent, SectionHeader.leadingDimension * textScaleFactor) + SectionHeader.padding.vertical;
|
||||||
|
return headerExtent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/utils/time_utils.dart';
|
import 'package:aves/utils/time_utils.dart';
|
||||||
import 'package:aves/widgets/collection/grid/header_generic.dart';
|
import 'package:aves/widgets/common/grid/header.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
@ -35,8 +36,8 @@ class DaySectionHeader extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TitleSectionHeader(
|
return SectionHeader(
|
||||||
sectionKey: date,
|
sectionKey: DateSectionKey(date),
|
||||||
title: text,
|
title: text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -64,8 +65,8 @@ class MonthSectionHeader extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TitleSectionHeader(
|
return SectionHeader(
|
||||||
sectionKey: date,
|
sectionKey: DateSectionKey(date),
|
||||||
title: text,
|
title: text,
|
||||||
);
|
);
|
||||||
}
|
}
|
46
lib/widgets/collection/grid/section_layout.dart
Normal file
46
lib/widgets/collection/grid/section_layout.dart
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/collection/grid/headers/any.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<ImageEntry> {
|
||||||
|
final CollectionLens collection;
|
||||||
|
|
||||||
|
const SectionedEntryListLayoutProvider({
|
||||||
|
@required this.collection,
|
||||||
|
@required double scrollableWidth,
|
||||||
|
@required int columnCount,
|
||||||
|
@required double tileExtent,
|
||||||
|
@required Widget Function(ImageEntry entry) tileBuilder,
|
||||||
|
@required Widget child,
|
||||||
|
}) : super(
|
||||||
|
scrollableWidth: scrollableWidth,
|
||||||
|
columnCount: columnCount,
|
||||||
|
tileExtent: tileExtent,
|
||||||
|
tileBuilder: tileBuilder,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool needHeaders() => collection.showHeaders;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<SectionKey, List<ImageEntry>> getSections() => collection.sections;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double getHeaderExtent(BuildContext context, SectionKey sectionKey) {
|
||||||
|
return CollectionSectionHeader.getPreferredHeight(context, scrollableWidth, collection.source, sectionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent) {
|
||||||
|
return CollectionSectionHeader(
|
||||||
|
collection: collection,
|
||||||
|
sectionKey: sectionKey,
|
||||||
|
height: headerExtent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import 'dart:math';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -135,7 +135,8 @@ class _GridSelectionGestureDetectorState extends State<GridSelectionGestureDetec
|
||||||
// but when it is scrolling (through controller animation), result is incomplete and children are missing,
|
// but when it is scrolling (through controller animation), result is incomplete and children are missing,
|
||||||
// so we use custom layout computation instead to find the entry.
|
// so we use custom layout computation instead to find the entry.
|
||||||
final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition;
|
final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition;
|
||||||
return context.read<SectionedListLayout>().getEntryAt(offset);
|
final sectionedListLayout = context.read<SectionedListLayout<ImageEntry>>();
|
||||||
|
return sectionedListLayout.getEntryAt(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleSelectionToIndex(int toIndex) {
|
void _toggleSelectionToIndex(int toIndex) {
|
||||||
|
|
|
@ -2,47 +2,19 @@ import 'package:aves/main.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/services/viewer_service.dart';
|
import 'package:aves/services/viewer_service.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_known_extent.dart';
|
|
||||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
|
||||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/scaling.dart';
|
import 'package:aves/widgets/common/scaling.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
// Use a `SliverList` instead of multiple `SliverGrid` because having one `SliverGrid` per section does not scale up.
|
class InteractiveThumbnail extends StatelessWidget {
|
||||||
// With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen
|
|
||||||
// because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0.
|
|
||||||
class CollectionListSliver extends StatelessWidget {
|
|
||||||
const CollectionListSliver();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final sectionLayouts = Provider.of<SectionedListLayout>(context).sectionLayouts;
|
|
||||||
final childCount = sectionLayouts.isEmpty ? 0 : sectionLayouts.last.lastIndex + 1;
|
|
||||||
return SliverKnownExtentList(
|
|
||||||
sectionLayouts: sectionLayouts,
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
if (index >= childCount) return null;
|
|
||||||
final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null);
|
|
||||||
return sectionLayout?.builder(context, index) ?? SizedBox.shrink();
|
|
||||||
},
|
|
||||||
childCount: childCount,
|
|
||||||
addAutomaticKeepAlives: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridThumbnail extends StatelessWidget {
|
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final double tileExtent;
|
final double tileExtent;
|
||||||
final ValueNotifier<bool> isScrollingNotifier;
|
final ValueNotifier<bool> isScrollingNotifier;
|
||||||
|
|
||||||
const GridThumbnail({
|
const InteractiveThumbnail({
|
||||||
Key key,
|
Key key,
|
||||||
this.collection,
|
this.collection,
|
||||||
@required this.entry,
|
@required this.entry,
|
|
@ -30,12 +30,12 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var child = entry.isSvg
|
var child = entry.isSvg
|
||||||
? ThumbnailVectorImage(
|
? VectorImageThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
heroTag: heroTag,
|
heroTag: heroTag,
|
||||||
)
|
)
|
||||||
: ThumbnailRasterImage(
|
: RasterImageThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
isScrollingNotifier: isScrollingNotifier,
|
isScrollingNotifier: isScrollingNotifier,
|
||||||
|
|
|
@ -8,14 +8,14 @@ import 'package:aves/widgets/collection/thumbnail/error.dart';
|
||||||
import 'package:aves/widgets/common/fx/transition_image.dart';
|
import 'package:aves/widgets/common/fx/transition_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ThumbnailRasterImage extends StatefulWidget {
|
class RasterImageThumbnail extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final double extent;
|
final double extent;
|
||||||
final int page;
|
final int page;
|
||||||
final ValueNotifier<bool> isScrollingNotifier;
|
final ValueNotifier<bool> isScrollingNotifier;
|
||||||
final Object heroTag;
|
final Object heroTag;
|
||||||
|
|
||||||
const ThumbnailRasterImage({
|
const RasterImageThumbnail({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.extent,
|
@required this.extent,
|
||||||
|
@ -25,10 +25,10 @@ class ThumbnailRasterImage extends StatefulWidget {
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ThumbnailRasterImageState createState() => _ThumbnailRasterImageState();
|
_RasterImageThumbnailState createState() => _RasterImageThumbnailState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
class _RasterImageThumbnailState extends State<RasterImageThumbnail> {
|
||||||
ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider;
|
ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
@ -51,7 +51,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant ThumbnailRasterImage oldWidget) {
|
void didUpdateWidget(covariant RasterImageThumbnail oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.entry != entry) {
|
if (oldWidget.entry != entry) {
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
|
@ -65,12 +65,12 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerWidget(ThumbnailRasterImage widget) {
|
void _registerWidget(RasterImageThumbnail widget) {
|
||||||
widget.entry.imageChangeNotifier.addListener(_onImageChanged);
|
widget.entry.imageChangeNotifier.addListener(_onImageChanged);
|
||||||
_initProvider();
|
_initProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(ThumbnailRasterImage widget) {
|
void _unregisterWidget(RasterImageThumbnail widget) {
|
||||||
widget.entry.imageChangeNotifier.removeListener(_onImageChanged);
|
widget.entry.imageChangeNotifier.removeListener(_onImageChanged);
|
||||||
_pauseProvider();
|
_pauseProvider();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,12 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ThumbnailVectorImage extends StatelessWidget {
|
class VectorImageThumbnail extends StatelessWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final double extent;
|
final double extent;
|
||||||
final Object heroTag;
|
final Object heroTag;
|
||||||
|
|
||||||
const ThumbnailVectorImage({
|
const VectorImageThumbnail({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.extent,
|
@required this.extent,
|
||||||
|
|
|
@ -12,12 +12,14 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/app_bar.dart';
|
import 'package:aves/widgets/collection/app_bar.dart';
|
||||||
import 'package:aves/widgets/collection/empty.dart';
|
import 'package:aves/widgets/collection/empty.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_sliver.dart';
|
|
||||||
import 'package:aves/widgets/collection/grid/selector.dart';
|
import 'package:aves/widgets/collection/grid/selector.dart';
|
||||||
|
import 'package:aves/widgets/collection/grid/thumbnail.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
|
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||||
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
||||||
import 'package:aves/widgets/common/scaling.dart';
|
import 'package:aves/widgets/common/scaling.dart';
|
||||||
|
@ -98,7 +100,7 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
highlightable: false,
|
highlightable: false,
|
||||||
),
|
),
|
||||||
getScaledItemTileRect: (context, entry) {
|
getScaledItemTileRect: (context, entry) {
|
||||||
final sectionedListLayout = Provider.of<SectionedListLayout>(context, listen: false);
|
final sectionedListLayout = context.read<SectionedListLayout<ImageEntry>>();
|
||||||
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
||||||
},
|
},
|
||||||
onScaled: (entry) => Provider.of<HighlightInfo>(context, listen: false).add(entry),
|
onScaled: (entry) => Provider.of<HighlightInfo>(context, listen: false).add(entry),
|
||||||
|
@ -115,12 +117,12 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
|
|
||||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||||
valueListenable: _tileExtentNotifier,
|
valueListenable: _tileExtentNotifier,
|
||||||
builder: (context, tileExtent, child) => SectionedListLayoutProvider(
|
builder: (context, tileExtent, child) => SectionedEntryListLayoutProvider(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
scrollableWidth: viewportSize.width,
|
scrollableWidth: viewportSize.width,
|
||||||
tileExtent: tileExtent,
|
tileExtent: tileExtent,
|
||||||
columnCount: tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent),
|
columnCount: tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent),
|
||||||
thumbnailBuilder: (entry) => GridThumbnail(
|
tileBuilder: (entry) => InteractiveThumbnail(
|
||||||
key: ValueKey(entry.contentId),
|
key: ValueKey(entry.contentId),
|
||||||
collection: collection,
|
collection: collection,
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
@ -215,7 +217,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
||||||
child: _buildEmptyCollectionPlaceholder(collection),
|
child: _buildEmptyCollectionPlaceholder(collection),
|
||||||
hasScrollBody: false,
|
hasScrollBody: false,
|
||||||
)
|
)
|
||||||
: CollectionListSliver(),
|
: SectionedListSliver<ImageEntry>(),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Selector<MediaQueryData, double>(
|
child: Selector<MediaQueryData, double>(
|
||||||
selector: (context, mq) => mq.viewInsets.bottom,
|
selector: (context, mq) => mq.viewInsets.bottom,
|
||||||
|
|
|
@ -1,115 +1,19 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/collection/grid/header_album.dart';
|
|
||||||
import 'package:aves/widgets/collection/grid/header_date.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SectionHeader extends StatelessWidget {
|
class SectionHeader extends StatelessWidget {
|
||||||
final CollectionLens collection;
|
final SectionKey sectionKey;
|
||||||
final dynamic sectionKey;
|
|
||||||
final double height;
|
|
||||||
|
|
||||||
const SectionHeader({
|
|
||||||
Key key,
|
|
||||||
@required this.collection,
|
|
||||||
@required this.sectionKey,
|
|
||||||
@required this.height,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget header;
|
|
||||||
switch (collection.sortFactor) {
|
|
||||||
case EntrySortFactor.date:
|
|
||||||
switch (collection.groupFactor) {
|
|
||||||
case EntryGroupFactor.album:
|
|
||||||
header = _buildAlbumSectionHeader();
|
|
||||||
break;
|
|
||||||
case EntryGroupFactor.month:
|
|
||||||
header = MonthSectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
|
|
||||||
break;
|
|
||||||
case EntryGroupFactor.day:
|
|
||||||
header = DaySectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
|
|
||||||
break;
|
|
||||||
case EntryGroupFactor.none:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case EntrySortFactor.size:
|
|
||||||
break;
|
|
||||||
case EntrySortFactor.name:
|
|
||||||
header = _buildAlbumSectionHeader();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return header != null
|
|
||||||
? SizedBox(
|
|
||||||
height: height,
|
|
||||||
child: header,
|
|
||||||
)
|
|
||||||
: SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAlbumSectionHeader() {
|
|
||||||
final folderPath = sectionKey as String;
|
|
||||||
return AlbumSectionHeader(
|
|
||||||
key: ValueKey(folderPath),
|
|
||||||
folderPath: folderPath,
|
|
||||||
albumName: collection.source.getUniqueAlbumName(folderPath),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO TLAD cache header extent computation?
|
|
||||||
static double computeHeaderHeight(BuildContext context, CollectionSource source, dynamic sectionKey, double scrollableWidth) {
|
|
||||||
var headerExtent = 0.0;
|
|
||||||
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
|
||||||
if (sectionKey is String) {
|
|
||||||
// only compute height for album headers, as they're the only likely ones to split on multiple lines
|
|
||||||
final hasLeading = androidFileUtils.getAlbumType(sectionKey) != AlbumType.regular;
|
|
||||||
final hasTrailing = androidFileUtils.isOnRemovableStorage(sectionKey);
|
|
||||||
final text = source.getUniqueAlbumName(sectionKey);
|
|
||||||
final maxWidth = scrollableWidth - TitleSectionHeader.padding.horizontal;
|
|
||||||
final para = RenderParagraph(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
// as of Flutter v1.22.3, `RenderParagraph` fails to lay out `WidgetSpan` offscreen
|
|
||||||
// so we use a hair space times a magic number to match width
|
|
||||||
TextSpan(
|
|
||||||
text: '\u200A' * (hasLeading ? 23 : 1),
|
|
||||||
// force a higher first line to match leading icon/selector dimension
|
|
||||||
style: TextStyle(height: 2.3 * textScaleFactor),
|
|
||||||
), // 23 hair spaces match a width of 40.0
|
|
||||||
if (hasTrailing) TextSpan(text: '\u200A' * 17),
|
|
||||||
TextSpan(
|
|
||||||
text: text,
|
|
||||||
style: Constants.titleTextStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
textScaleFactor: textScaleFactor,
|
|
||||||
)..layout(BoxConstraints(maxWidth: maxWidth), parentUsesSize: true);
|
|
||||||
headerExtent = para.getMaxIntrinsicHeight(maxWidth);
|
|
||||||
}
|
|
||||||
headerExtent = max(headerExtent, TitleSectionHeader.leadingDimension * textScaleFactor) + TitleSectionHeader.padding.vertical;
|
|
||||||
return headerExtent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TitleSectionHeader extends StatelessWidget {
|
|
||||||
final dynamic sectionKey;
|
|
||||||
final Widget leading, trailing;
|
final Widget leading, trailing;
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
const TitleSectionHeader({
|
const SectionHeader({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.sectionKey,
|
@required this.sectionKey,
|
||||||
this.leading,
|
this.leading,
|
||||||
|
@ -136,7 +40,7 @@ class TitleSectionHeader extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
alignment: widgetSpanAlignment,
|
alignment: widgetSpanAlignment,
|
||||||
child: SectionSelectableLeading(
|
child: _SectionSelectableLeading(
|
||||||
sectionKey: sectionKey,
|
sectionKey: sectionKey,
|
||||||
browsingBuilder: leading != null
|
browsingBuilder: leading != null
|
||||||
? (context) => Container(
|
? (context) => Container(
|
||||||
|
@ -178,21 +82,54 @@ class TitleSectionHeader extends StatelessWidget {
|
||||||
collection.addToSelection(sectionEntries);
|
collection.addToSelection(sectionEntries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO TLAD cache header extent computation?
|
||||||
|
static double getPreferredHeight({
|
||||||
|
@required BuildContext context,
|
||||||
|
@required double maxWidth,
|
||||||
|
@required String title,
|
||||||
|
bool hasLeading = false,
|
||||||
|
bool hasTrailing = false,
|
||||||
|
}) {
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
|
final maxContentWidth = maxWidth - SectionHeader.padding.horizontal;
|
||||||
|
final para = RenderParagraph(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
// as of Flutter v1.22.3, `RenderParagraph` fails to lay out `WidgetSpan` offscreen
|
||||||
|
// so we use a hair space times a magic number to match width
|
||||||
|
TextSpan(
|
||||||
|
text: '\u200A' * (hasLeading ? 23 : 1),
|
||||||
|
// force a higher first line to match leading icon/selector dimension
|
||||||
|
style: TextStyle(height: 2.3 * textScaleFactor),
|
||||||
|
), // 23 hair spaces match a width of 40.0
|
||||||
|
if (hasTrailing) TextSpan(text: '\u200A' * 17),
|
||||||
|
TextSpan(
|
||||||
|
text: title,
|
||||||
|
style: Constants.titleTextStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
textScaleFactor: textScaleFactor,
|
||||||
|
)..layout(BoxConstraints(maxWidth: maxContentWidth), parentUsesSize: true);
|
||||||
|
return para.getMaxIntrinsicHeight(maxContentWidth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SectionSelectableLeading extends StatelessWidget {
|
class _SectionSelectableLeading extends StatelessWidget {
|
||||||
final dynamic sectionKey;
|
final SectionKey sectionKey;
|
||||||
final WidgetBuilder browsingBuilder;
|
final WidgetBuilder browsingBuilder;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
const SectionSelectableLeading({
|
const _SectionSelectableLeading({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.sectionKey,
|
@required this.sectionKey,
|
||||||
@required this.browsingBuilder,
|
@required this.browsingBuilder,
|
||||||
@required this.onPressed,
|
@required this.onPressed,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
static const leadingDimension = TitleSectionHeader.leadingDimension;
|
static const leadingDimension = SectionHeader.leadingDimension;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
|
@ -1,49 +1,46 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
|
||||||
import 'package:aves/widgets/collection/grid/header_generic.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SectionedListLayoutProvider extends StatelessWidget {
|
abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
||||||
final CollectionLens collection;
|
|
||||||
final int columnCount;
|
|
||||||
final double scrollableWidth;
|
final double scrollableWidth;
|
||||||
|
final int columnCount;
|
||||||
final double tileExtent;
|
final double tileExtent;
|
||||||
final Widget Function(ImageEntry entry) thumbnailBuilder;
|
final Widget Function(T entry) tileBuilder;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const SectionedListLayoutProvider({
|
const SectionedListLayoutProvider({
|
||||||
@required this.collection,
|
|
||||||
@required this.scrollableWidth,
|
@required this.scrollableWidth,
|
||||||
@required this.tileExtent,
|
|
||||||
@required this.columnCount,
|
@required this.columnCount,
|
||||||
@required this.thumbnailBuilder,
|
@required this.tileExtent,
|
||||||
|
@required this.tileBuilder,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
}) : assert(scrollableWidth != 0);
|
}) : assert(scrollableWidth != 0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ProxyProvider0<SectionedListLayout>(
|
return ProxyProvider0<SectionedListLayout<T>>(
|
||||||
update: (context, __) => _updateLayouts(context),
|
update: (context, __) => _updateLayouts(context),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionedListLayout _updateLayouts(BuildContext context) {
|
SectionedListLayout<T> _updateLayouts(BuildContext context) {
|
||||||
final sectionLayouts = <SectionLayout>[];
|
final showHeaders = needHeaders();
|
||||||
final showHeaders = collection.showHeaders;
|
final sections = getSections();
|
||||||
final source = collection.source;
|
|
||||||
final sections = collection.sections;
|
|
||||||
final sectionKeys = sections.keys.toList();
|
final sectionKeys = sections.keys.toList();
|
||||||
|
|
||||||
|
final sectionLayouts = <SectionLayout>[];
|
||||||
var currentIndex = 0, currentOffset = 0.0;
|
var currentIndex = 0, currentOffset = 0.0;
|
||||||
sectionKeys.forEach((sectionKey) {
|
sectionKeys.forEach((sectionKey) {
|
||||||
final sectionEntryCount = sections[sectionKey].length;
|
final section = sections[sectionKey];
|
||||||
|
final sectionEntryCount = section.length;
|
||||||
final sectionChildCount = 1 + (sectionEntryCount / columnCount).ceil();
|
final sectionChildCount = 1 + (sectionEntryCount / columnCount).ceil();
|
||||||
|
|
||||||
final headerExtent = showHeaders ? SectionHeader.computeHeaderHeight(context, source, sectionKey, scrollableWidth) : 0.0;
|
final headerExtent = showHeaders ? getHeaderExtent(context, sectionKey) : 0.0;
|
||||||
|
|
||||||
final sectionFirstIndex = currentIndex;
|
final sectionFirstIndex = currentIndex;
|
||||||
currentIndex += sectionChildCount;
|
currentIndex += sectionChildCount;
|
||||||
|
@ -63,29 +60,30 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
||||||
headerExtent: headerExtent,
|
headerExtent: headerExtent,
|
||||||
tileExtent: tileExtent,
|
tileExtent: tileExtent,
|
||||||
builder: (context, listIndex) => _buildInSection(
|
builder: (context, listIndex) => _buildInSection(
|
||||||
|
context,
|
||||||
|
section,
|
||||||
listIndex - sectionFirstIndex,
|
listIndex - sectionFirstIndex,
|
||||||
collection,
|
|
||||||
sectionKey,
|
sectionKey,
|
||||||
headerExtent,
|
headerExtent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return SectionedListLayout(
|
return SectionedListLayout<T>(
|
||||||
collection: collection,
|
sections: sections,
|
||||||
|
showHeaders: showHeaders,
|
||||||
columnCount: columnCount,
|
columnCount: columnCount,
|
||||||
tileExtent: tileExtent,
|
tileExtent: tileExtent,
|
||||||
sectionLayouts: sectionLayouts,
|
sectionLayouts: sectionLayouts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInSection(int sectionChildIndex, CollectionLens collection, dynamic sectionKey, double headerExtent) {
|
Widget _buildInSection(BuildContext context, List<T> section, int sectionChildIndex, SectionKey sectionKey, double headerExtent) {
|
||||||
if (sectionChildIndex == 0) {
|
if (sectionChildIndex == 0) {
|
||||||
return headerBuilder(collection, sectionKey, headerExtent);
|
return headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : SizedBox.shrink();
|
||||||
}
|
}
|
||||||
sectionChildIndex--;
|
sectionChildIndex--;
|
||||||
|
|
||||||
final section = collection.sections[sectionKey];
|
|
||||||
final sectionEntryCount = section.length;
|
final sectionEntryCount = section.length;
|
||||||
|
|
||||||
final minEntryIndex = sectionChildIndex * columnCount;
|
final minEntryIndex = sectionChildIndex * columnCount;
|
||||||
|
@ -93,7 +91,7 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
for (var i = minEntryIndex; i < maxEntryIndex; i++) {
|
for (var i = minEntryIndex; i < maxEntryIndex; i++) {
|
||||||
final entry = section[i];
|
final entry = section[i];
|
||||||
children.add(thumbnailBuilder(entry));
|
children.add(tileBuilder(entry));
|
||||||
}
|
}
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -101,39 +99,38 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget headerBuilder(CollectionLens collection, dynamic sectionKey, double headerExtent) {
|
bool needHeaders();
|
||||||
return collection.showHeaders
|
|
||||||
? SectionHeader(
|
Map<SectionKey, List<T>> getSections();
|
||||||
collection: collection,
|
|
||||||
sectionKey: sectionKey,
|
double getHeaderExtent(BuildContext context, SectionKey sectionKey);
|
||||||
height: headerExtent,
|
|
||||||
)
|
Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent);
|
||||||
: SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SectionedListLayout {
|
class SectionedListLayout<T> {
|
||||||
final CollectionLens collection;
|
final Map<SectionKey, List<T>> sections;
|
||||||
|
final bool showHeaders;
|
||||||
final int columnCount;
|
final int columnCount;
|
||||||
final double tileExtent;
|
final double tileExtent;
|
||||||
final List<SectionLayout> sectionLayouts;
|
final List<SectionLayout> sectionLayouts;
|
||||||
|
|
||||||
const SectionedListLayout({
|
const SectionedListLayout({
|
||||||
@required this.collection,
|
@required this.sections,
|
||||||
|
@required this.showHeaders,
|
||||||
@required this.columnCount,
|
@required this.columnCount,
|
||||||
@required this.tileExtent,
|
@required this.tileExtent,
|
||||||
@required this.sectionLayouts,
|
@required this.sectionLayouts,
|
||||||
});
|
});
|
||||||
|
|
||||||
Rect getTileRect(ImageEntry entry) {
|
Rect getTileRect(T entry) {
|
||||||
final section = collection.sections.entries.firstWhere((kv) => kv.value.contains(entry), orElse: () => null);
|
final section = sections.entries.firstWhere((kv) => kv.value.contains(entry), orElse: () => null);
|
||||||
if (section == null) return null;
|
if (section == null) return null;
|
||||||
|
|
||||||
final sectionKey = section.key;
|
final sectionKey = section.key;
|
||||||
final sectionLayout = sectionLayouts.firstWhere((sl) => sl.sectionKey == sectionKey, orElse: () => null);
|
final sectionLayout = sectionLayouts.firstWhere((sl) => sl.sectionKey == sectionKey, orElse: () => null);
|
||||||
if (sectionLayout == null) return null;
|
if (sectionLayout == null) return null;
|
||||||
|
|
||||||
final showHeaders = collection.showHeaders;
|
|
||||||
final sectionEntryIndex = section.value.indexOf(entry);
|
final sectionEntryIndex = section.value.indexOf(entry);
|
||||||
final column = sectionEntryIndex % columnCount;
|
final column = sectionEntryIndex % columnCount;
|
||||||
final row = (sectionEntryIndex / columnCount).floor();
|
final row = (sectionEntryIndex / columnCount).floor();
|
||||||
|
@ -144,12 +141,12 @@ class SectionedListLayout {
|
||||||
return Rect.fromLTWH(left, top, tileExtent, tileExtent);
|
return Rect.fromLTWH(left, top, tileExtent, tileExtent);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageEntry getEntryAt(Offset position) {
|
T getEntryAt(Offset position) {
|
||||||
var dy = position.dy;
|
var dy = position.dy;
|
||||||
final sectionLayout = sectionLayouts.firstWhere((sl) => dy < sl.maxOffset, orElse: () => null);
|
final sectionLayout = sectionLayouts.firstWhere((sl) => dy < sl.maxOffset, orElse: () => null);
|
||||||
if (sectionLayout == null) return null;
|
if (sectionLayout == null) return null;
|
||||||
|
|
||||||
final section = collection.sections[sectionLayout.sectionKey];
|
final section = sections[sectionLayout.sectionKey];
|
||||||
if (section == null) return null;
|
if (section == null) return null;
|
||||||
|
|
||||||
dy -= sectionLayout.minOffset + sectionLayout.headerExtent;
|
dy -= sectionLayout.minOffset + sectionLayout.headerExtent;
|
||||||
|
@ -165,7 +162,7 @@ class SectionedListLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SectionLayout {
|
class SectionLayout {
|
||||||
final dynamic sectionKey;
|
final SectionKey sectionKey;
|
||||||
final int firstIndex, lastIndex;
|
final int firstIndex, lastIndex;
|
||||||
final double minOffset, maxOffset;
|
final double minOffset, maxOffset;
|
||||||
final double headerExtent, tileExtent;
|
final double headerExtent, tileExtent;
|
|
@ -1,32 +1,59 @@
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SliverKnownExtentList extends SliverMultiBoxAdaptorWidget {
|
// Use a `SliverList` instead of multiple `SliverGrid` because having one `SliverGrid` per section does not scale up.
|
||||||
|
// With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen
|
||||||
|
// because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0.
|
||||||
|
class SectionedListSliver<T> extends StatelessWidget {
|
||||||
|
const SectionedListSliver();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final sectionLayouts = context.watch<SectionedListLayout<T>>().sectionLayouts;
|
||||||
|
final childCount = sectionLayouts.isEmpty ? 0 : sectionLayouts.last.lastIndex + 1;
|
||||||
|
return _SliverKnownExtentList(
|
||||||
|
sectionLayouts: sectionLayouts,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
if (index >= childCount) return null;
|
||||||
|
final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null);
|
||||||
|
return sectionLayout?.builder(context, index) ?? SizedBox.shrink();
|
||||||
|
},
|
||||||
|
childCount: childCount,
|
||||||
|
addAutomaticKeepAlives: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverKnownExtentList extends SliverMultiBoxAdaptorWidget {
|
||||||
final List<SectionLayout> sectionLayouts;
|
final List<SectionLayout> sectionLayouts;
|
||||||
|
|
||||||
const SliverKnownExtentList({
|
const _SliverKnownExtentList({
|
||||||
Key key,
|
Key key,
|
||||||
@required SliverChildDelegate delegate,
|
@required SliverChildDelegate delegate,
|
||||||
@required this.sectionLayouts,
|
@required this.sectionLayouts,
|
||||||
}) : super(key: key, delegate: delegate);
|
}) : super(key: key, delegate: delegate);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderSliverKnownExtentBoxAdaptor createRenderObject(BuildContext context) {
|
_RenderSliverKnownExtentBoxAdaptor createRenderObject(BuildContext context) {
|
||||||
final element = context as SliverMultiBoxAdaptorElement;
|
final element = context as SliverMultiBoxAdaptorElement;
|
||||||
return RenderSliverKnownExtentBoxAdaptor(childManager: element, sectionLayouts: sectionLayouts);
|
return _RenderSliverKnownExtentBoxAdaptor(childManager: element, sectionLayouts: sectionLayouts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRenderObject(BuildContext context, RenderSliverKnownExtentBoxAdaptor renderObject) {
|
void updateRenderObject(BuildContext context, _RenderSliverKnownExtentBoxAdaptor renderObject) {
|
||||||
renderObject.sectionLayouts = sectionLayouts;
|
renderObject.sectionLayouts = sectionLayouts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
||||||
List<SectionLayout> _sectionLayouts;
|
List<SectionLayout> _sectionLayouts;
|
||||||
|
|
||||||
List<SectionLayout> get sectionLayouts => _sectionLayouts;
|
List<SectionLayout> get sectionLayouts => _sectionLayouts;
|
||||||
|
@ -38,7 +65,7 @@ class RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderSliverKnownExtentBoxAdaptor({
|
_RenderSliverKnownExtentBoxAdaptor({
|
||||||
@required RenderSliverBoxChildManager childManager,
|
@required RenderSliverBoxChildManager childManager,
|
||||||
@required List<SectionLayout> sectionLayouts,
|
@required List<SectionLayout> sectionLayouts,
|
||||||
}) : _sectionLayouts = sectionLayouts,
|
}) : _sectionLayouts = sectionLayouts,
|
|
@ -43,11 +43,11 @@ class DecoratedFilterChip extends StatelessWidget {
|
||||||
final backgroundImage = entry == null
|
final backgroundImage = entry == null
|
||||||
? Container(color: Colors.white)
|
? Container(color: Colors.white)
|
||||||
: entry.isSvg
|
: entry.isSvg
|
||||||
? ThumbnailVectorImage(
|
? VectorImageThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
)
|
)
|
||||||
: ThumbnailRasterImage(
|
: RasterImageThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,11 +27,11 @@ class ImageMarker extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final thumbnail = entry.isSvg
|
final thumbnail = entry.isSvg
|
||||||
? ThumbnailVectorImage(
|
? VectorImageThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
)
|
)
|
||||||
: ThumbnailRasterImage(
|
: RasterImageThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
);
|
);
|
||||||
|
|
|
@ -120,7 +120,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
||||||
child: Container(
|
child: Container(
|
||||||
width: extent,
|
width: extent,
|
||||||
height: extent,
|
height: extent,
|
||||||
child: ThumbnailRasterImage(
|
child: RasterImageThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
page: page,
|
page: page,
|
||||||
|
|
Loading…
Reference in a new issue