album grid prep

This commit is contained in:
Thibault Deckers 2021-01-13 14:18:30 +09:00
parent 229b2e7b2b
commit f952deff15
18 changed files with 339 additions and 226 deletions

View file

@ -5,6 +5,7 @@ import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.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/utils/change_notifier.dart';
import 'package:collection/collection.dart';
@ -22,7 +23,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
List<ImageEntry> _filteredEntries;
List<StreamSubscription> _subscriptions = [];
Map<dynamic, List<ImageEntry>> sections = Map.unmodifiable({});
Map<SectionKey, List<ImageEntry>> sections = Map.unmodifiable({});
CollectionLens({
@required this.source,
@ -138,13 +139,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
case EntrySortFactor.date:
switch (groupFactor) {
case EntryGroupFactor.album:
sections = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory);
sections = groupBy<ImageEntry, AlbumSectionKey>(_filteredEntries, (entry) => AlbumSectionKey(entry.directory));
break;
case EntryGroupFactor.month:
sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.monthTaken);
sections = groupBy<ImageEntry, DateSectionKey>(_filteredEntries, (entry) => DateSectionKey(entry.monthTaken));
break;
case EntryGroupFactor.day:
sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.dayTaken);
sections = groupBy<ImageEntry, DateSectionKey>(_filteredEntries, (entry) => DateSectionKey(entry.dayTaken));
break;
case EntryGroupFactor.none:
sections = Map.fromEntries([
@ -159,8 +160,8 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
]);
break;
case EntrySortFactor.name:
final byAlbum = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory);
sections = SplayTreeMap<String, List<ImageEntry>>.of(byAlbum, source.compareAlbumsByName);
final byAlbum = groupBy<ImageEntry, AlbumSectionKey>(_filteredEntries, (entry) => AlbumSectionKey(entry.directory));
sections = SplayTreeMap<AlbumSectionKey, List<ImageEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath));
break;
}
sections = Map.unmodifiable(sections);

View 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}';
}

View file

@ -1,17 +1,20 @@
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.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:flutter/material.dart';
class AlbumSectionHeader extends StatelessWidget {
final String folderPath, albumName;
const AlbumSectionHeader({
AlbumSectionHeader({
Key key,
@required CollectionSource source,
@required this.folderPath,
@required this.albumName,
}) : super(key: key);
}) : albumName = source.getUniqueAlbumName(folderPath),
super(key: key);
@override
Widget build(BuildContext context) {
@ -25,8 +28,8 @@ class AlbumSectionHeader extends StatelessWidget {
child: albumIcon,
);
}
return TitleSectionHeader(
sectionKey: folderPath,
return SectionHeader(
sectionKey: AlbumSectionKey(folderPath),
leading: albumIcon,
title: albumName,
trailing: androidFileUtils.isOnRemovableStorage(folderPath)
@ -38,4 +41,15 @@ class AlbumSectionHeader extends StatelessWidget {
: 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),
);
}
}

View 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;
}
}

View file

@ -1,5 +1,6 @@
import 'package:aves/model/source/section_keys.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:intl/intl.dart';
@ -35,8 +36,8 @@ class DaySectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TitleSectionHeader(
sectionKey: date,
return SectionHeader(
sectionKey: DateSectionKey(date),
title: text,
);
}
@ -64,8 +65,8 @@ class MonthSectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TitleSectionHeader(
sectionKey: date,
return SectionHeader(
sectionKey: DateSectionKey(date),
title: text,
);
}

View 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,
);
}
}

View file

@ -4,7 +4,7 @@ import 'dart:math';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/source/collection_lens.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/rendering.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,
// so we use custom layout computation instead to find the entry.
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) {

View file

@ -2,47 +2,19 @@ import 'package:aves/main.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/source/collection_lens.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/common/behaviour/routes.dart';
import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.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.
// 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 {
class InteractiveThumbnail extends StatelessWidget {
final CollectionLens collection;
final ImageEntry entry;
final double tileExtent;
final ValueNotifier<bool> isScrollingNotifier;
const GridThumbnail({
const InteractiveThumbnail({
Key key,
this.collection,
@required this.entry,

View file

@ -30,12 +30,12 @@ class DecoratedThumbnail extends StatelessWidget {
@override
Widget build(BuildContext context) {
var child = entry.isSvg
? ThumbnailVectorImage(
? VectorImageThumbnail(
entry: entry,
extent: extent,
heroTag: heroTag,
)
: ThumbnailRasterImage(
: RasterImageThumbnail(
entry: entry,
extent: extent,
isScrollingNotifier: isScrollingNotifier,

View file

@ -8,14 +8,14 @@ import 'package:aves/widgets/collection/thumbnail/error.dart';
import 'package:aves/widgets/common/fx/transition_image.dart';
import 'package:flutter/material.dart';
class ThumbnailRasterImage extends StatefulWidget {
class RasterImageThumbnail extends StatefulWidget {
final ImageEntry entry;
final double extent;
final int page;
final ValueNotifier<bool> isScrollingNotifier;
final Object heroTag;
const ThumbnailRasterImage({
const RasterImageThumbnail({
Key key,
@required this.entry,
@required this.extent,
@ -25,10 +25,10 @@ class ThumbnailRasterImage extends StatefulWidget {
}) : super(key: key);
@override
_ThumbnailRasterImageState createState() => _ThumbnailRasterImageState();
_RasterImageThumbnailState createState() => _RasterImageThumbnailState();
}
class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
class _RasterImageThumbnailState extends State<RasterImageThumbnail> {
ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider;
ImageEntry get entry => widget.entry;
@ -51,7 +51,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
}
@override
void didUpdateWidget(covariant ThumbnailRasterImage oldWidget) {
void didUpdateWidget(covariant RasterImageThumbnail oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != entry) {
_unregisterWidget(oldWidget);
@ -65,12 +65,12 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
super.dispose();
}
void _registerWidget(ThumbnailRasterImage widget) {
void _registerWidget(RasterImageThumbnail widget) {
widget.entry.imageChangeNotifier.addListener(_onImageChanged);
_initProvider();
}
void _unregisterWidget(ThumbnailRasterImage widget) {
void _unregisterWidget(RasterImageThumbnail widget) {
widget.entry.imageChangeNotifier.removeListener(_onImageChanged);
_pauseProvider();
}

View file

@ -7,12 +7,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class ThumbnailVectorImage extends StatelessWidget {
class VectorImageThumbnail extends StatelessWidget {
final ImageEntry entry;
final double extent;
final Object heroTag;
const ThumbnailVectorImage({
const VectorImageThumbnail({
Key key,
@required this.entry,
@required this.extent,

View file

@ -12,12 +12,14 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/app_bar.dart';
import 'package:aves/widgets/collection/empty.dart';
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
import 'package:aves/widgets/collection/grid/list_sliver.dart';
import 'package:aves/widgets/collection/grid/section_layout.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/common/behaviour/routes.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/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/scaling.dart';
@ -98,7 +100,7 @@ class ThumbnailCollection extends StatelessWidget {
highlightable: false,
),
getScaledItemTileRect: (context, entry) {
final sectionedListLayout = Provider.of<SectionedListLayout>(context, listen: false);
final sectionedListLayout = context.read<SectionedListLayout<ImageEntry>>();
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
},
onScaled: (entry) => Provider.of<HighlightInfo>(context, listen: false).add(entry),
@ -115,12 +117,12 @@ class ThumbnailCollection extends StatelessWidget {
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) => SectionedListLayoutProvider(
builder: (context, tileExtent, child) => SectionedEntryListLayoutProvider(
collection: collection,
scrollableWidth: viewportSize.width,
tileExtent: tileExtent,
columnCount: tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent),
thumbnailBuilder: (entry) => GridThumbnail(
tileBuilder: (entry) => InteractiveThumbnail(
key: ValueKey(entry.contentId),
collection: collection,
entry: entry,
@ -215,7 +217,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
child: _buildEmptyCollectionPlaceholder(collection),
hasScrollBody: false,
)
: CollectionListSliver(),
: SectionedListSliver<ImageEntry>(),
SliverToBoxAdapter(
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.viewInsets.bottom,

View file

@ -1,115 +1,19 @@
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/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.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/rendering.dart';
import 'package:provider/provider.dart';
class SectionHeader extends StatelessWidget {
final CollectionLens collection;
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 SectionKey sectionKey;
final Widget leading, trailing;
final String title;
const TitleSectionHeader({
const SectionHeader({
Key key,
@required this.sectionKey,
this.leading,
@ -136,7 +40,7 @@ class TitleSectionHeader extends StatelessWidget {
children: [
WidgetSpan(
alignment: widgetSpanAlignment,
child: SectionSelectableLeading(
child: _SectionSelectableLeading(
sectionKey: sectionKey,
browsingBuilder: leading != null
? (context) => Container(
@ -178,21 +82,54 @@ class TitleSectionHeader extends StatelessWidget {
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 {
final dynamic sectionKey;
class _SectionSelectableLeading extends StatelessWidget {
final SectionKey sectionKey;
final WidgetBuilder browsingBuilder;
final VoidCallback onPressed;
const SectionSelectableLeading({
const _SectionSelectableLeading({
Key key,
@required this.sectionKey,
@required this.browsingBuilder,
@required this.onPressed,
}) : super(key: key);
static const leadingDimension = TitleSectionHeader.leadingDimension;
static const leadingDimension = SectionHeader.leadingDimension;
@override
Widget build(BuildContext context) {

View file

@ -1,49 +1,46 @@
import 'dart:math';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/collection/grid/header_generic.dart';
import 'package:aves/model/source/section_keys.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SectionedListLayoutProvider extends StatelessWidget {
final CollectionLens collection;
final int columnCount;
abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final double scrollableWidth;
final int columnCount;
final double tileExtent;
final Widget Function(ImageEntry entry) thumbnailBuilder;
final Widget Function(T entry) tileBuilder;
final Widget child;
const SectionedListLayoutProvider({
@required this.collection,
@required this.scrollableWidth,
@required this.tileExtent,
@required this.columnCount,
@required this.thumbnailBuilder,
@required this.tileExtent,
@required this.tileBuilder,
@required this.child,
}) : assert(scrollableWidth != 0);
@override
Widget build(BuildContext context) {
return ProxyProvider0<SectionedListLayout>(
return ProxyProvider0<SectionedListLayout<T>>(
update: (context, __) => _updateLayouts(context),
child: child,
);
}
SectionedListLayout _updateLayouts(BuildContext context) {
final sectionLayouts = <SectionLayout>[];
final showHeaders = collection.showHeaders;
final source = collection.source;
final sections = collection.sections;
SectionedListLayout<T> _updateLayouts(BuildContext context) {
final showHeaders = needHeaders();
final sections = getSections();
final sectionKeys = sections.keys.toList();
final sectionLayouts = <SectionLayout>[];
var currentIndex = 0, currentOffset = 0.0;
sectionKeys.forEach((sectionKey) {
final sectionEntryCount = sections[sectionKey].length;
final section = sections[sectionKey];
final sectionEntryCount = section.length;
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;
currentIndex += sectionChildCount;
@ -63,29 +60,30 @@ class SectionedListLayoutProvider extends StatelessWidget {
headerExtent: headerExtent,
tileExtent: tileExtent,
builder: (context, listIndex) => _buildInSection(
context,
section,
listIndex - sectionFirstIndex,
collection,
sectionKey,
headerExtent,
),
),
);
});
return SectionedListLayout(
collection: collection,
return SectionedListLayout<T>(
sections: sections,
showHeaders: showHeaders,
columnCount: columnCount,
tileExtent: tileExtent,
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) {
return headerBuilder(collection, sectionKey, headerExtent);
return headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : SizedBox.shrink();
}
sectionChildIndex--;
final section = collection.sections[sectionKey];
final sectionEntryCount = section.length;
final minEntryIndex = sectionChildIndex * columnCount;
@ -93,7 +91,7 @@ class SectionedListLayoutProvider extends StatelessWidget {
final children = <Widget>[];
for (var i = minEntryIndex; i < maxEntryIndex; i++) {
final entry = section[i];
children.add(thumbnailBuilder(entry));
children.add(tileBuilder(entry));
}
return Row(
mainAxisSize: MainAxisSize.min,
@ -101,39 +99,38 @@ class SectionedListLayoutProvider extends StatelessWidget {
);
}
Widget headerBuilder(CollectionLens collection, dynamic sectionKey, double headerExtent) {
return collection.showHeaders
? SectionHeader(
collection: collection,
sectionKey: sectionKey,
height: headerExtent,
)
: SizedBox.shrink();
}
bool needHeaders();
Map<SectionKey, List<T>> getSections();
double getHeaderExtent(BuildContext context, SectionKey sectionKey);
Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent);
}
class SectionedListLayout {
final CollectionLens collection;
class SectionedListLayout<T> {
final Map<SectionKey, List<T>> sections;
final bool showHeaders;
final int columnCount;
final double tileExtent;
final List<SectionLayout> sectionLayouts;
const SectionedListLayout({
@required this.collection,
@required this.sections,
@required this.showHeaders,
@required this.columnCount,
@required this.tileExtent,
@required this.sectionLayouts,
});
Rect getTileRect(ImageEntry entry) {
final section = collection.sections.entries.firstWhere((kv) => kv.value.contains(entry), orElse: () => null);
Rect getTileRect(T entry) {
final section = sections.entries.firstWhere((kv) => kv.value.contains(entry), orElse: () => null);
if (section == null) return null;
final sectionKey = section.key;
final sectionLayout = sectionLayouts.firstWhere((sl) => sl.sectionKey == sectionKey, orElse: () => null);
if (sectionLayout == null) return null;
final showHeaders = collection.showHeaders;
final sectionEntryIndex = section.value.indexOf(entry);
final column = sectionEntryIndex % columnCount;
final row = (sectionEntryIndex / columnCount).floor();
@ -144,12 +141,12 @@ class SectionedListLayout {
return Rect.fromLTWH(left, top, tileExtent, tileExtent);
}
ImageEntry getEntryAt(Offset position) {
T getEntryAt(Offset position) {
var dy = position.dy;
final sectionLayout = sectionLayouts.firstWhere((sl) => dy < sl.maxOffset, orElse: () => null);
if (sectionLayout == null) return null;
final section = collection.sections[sectionLayout.sectionKey];
final section = sections[sectionLayout.sectionKey];
if (section == null) return null;
dy -= sectionLayout.minOffset + sectionLayout.headerExtent;
@ -165,7 +162,7 @@ class SectionedListLayout {
}
class SectionLayout {
final dynamic sectionKey;
final SectionKey sectionKey;
final int firstIndex, lastIndex;
final double minOffset, maxOffset;
final double headerExtent, tileExtent;

View file

@ -1,32 +1,59 @@
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/material.dart';
import 'package:flutter/rendering.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;
const SliverKnownExtentList({
const _SliverKnownExtentList({
Key key,
@required SliverChildDelegate delegate,
@required this.sectionLayouts,
}) : super(key: key, delegate: delegate);
@override
RenderSliverKnownExtentBoxAdaptor createRenderObject(BuildContext context) {
_RenderSliverKnownExtentBoxAdaptor createRenderObject(BuildContext context) {
final element = context as SliverMultiBoxAdaptorElement;
return RenderSliverKnownExtentBoxAdaptor(childManager: element, sectionLayouts: sectionLayouts);
return _RenderSliverKnownExtentBoxAdaptor(childManager: element, sectionLayouts: sectionLayouts);
}
@override
void updateRenderObject(BuildContext context, RenderSliverKnownExtentBoxAdaptor renderObject) {
void updateRenderObject(BuildContext context, _RenderSliverKnownExtentBoxAdaptor renderObject) {
renderObject.sectionLayouts = sectionLayouts;
}
}
class RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
List<SectionLayout> _sectionLayouts;
List<SectionLayout> get sectionLayouts => _sectionLayouts;
@ -38,7 +65,7 @@ class RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
markNeedsLayout();
}
RenderSliverKnownExtentBoxAdaptor({
_RenderSliverKnownExtentBoxAdaptor({
@required RenderSliverBoxChildManager childManager,
@required List<SectionLayout> sectionLayouts,
}) : _sectionLayouts = sectionLayouts,

View file

@ -43,11 +43,11 @@ class DecoratedFilterChip extends StatelessWidget {
final backgroundImage = entry == null
? Container(color: Colors.white)
: entry.isSvg
? ThumbnailVectorImage(
? VectorImageThumbnail(
entry: entry,
extent: extent,
)
: ThumbnailRasterImage(
: RasterImageThumbnail(
entry: entry,
extent: extent,
);

View file

@ -27,11 +27,11 @@ class ImageMarker extends StatelessWidget {
@override
Widget build(BuildContext context) {
final thumbnail = entry.isSvg
? ThumbnailVectorImage(
? VectorImageThumbnail(
entry: entry,
extent: extent,
)
: ThumbnailRasterImage(
: RasterImageThumbnail(
entry: entry,
extent: extent,
);

View file

@ -120,7 +120,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
child: Container(
width: extent,
height: extent,
child: ThumbnailRasterImage(
child: RasterImageThumbnail(
entry: entry,
extent: extent,
page: page,