collection building review

This commit is contained in:
Thibault Deckers 2020-04-14 11:54:33 +09:00
parent 1ac13796da
commit 0c202ac185
6 changed files with 205 additions and 173 deletions

View file

@ -2,7 +2,6 @@ import 'dart:math';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/collection_source.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/album/grid/header_album.dart';
@ -14,13 +13,11 @@ import 'package:flutter/rendering.dart';
class SectionHeader extends StatelessWidget {
final CollectionLens collection;
final Map<dynamic, List<ImageEntry>> sections;
final dynamic sectionKey;
const SectionHeader({
Key key,
@required this.collection,
@required this.sections,
@required this.sectionKey,
}) : super(key: key);

View file

@ -1,4 +1,109 @@
import 'dart:math';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/widgets/album/grid/header_generic.dart';
import 'package:aves/widgets/album/grid/list_sliver.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SectionedListLayoutProvider extends StatelessWidget {
final Widget child;
final CollectionLens collection;
final int columnCount;
final double scrollableWidth;
final double tileExtent;
SectionedListLayoutProvider({
@required this.collection,
@required this.scrollableWidth,
@required this.tileExtent,
@required this.child,
}) : columnCount = (scrollableWidth / tileExtent).round();
@override
Widget build(BuildContext context) {
return ProxyProvider0<SectionedListLayout>(
update: (context, __) => _updateLayouts(context),
child: child,
);
}
SectionedListLayout _updateLayouts(BuildContext context) {
debugPrint('$runtimeType _updateLayouts entries=${collection.entryCount} columnCount=$columnCount tileExtent=$tileExtent');
final sectionLayouts = <SectionLayout>[];
final showHeaders = collection.showHeaders;
final source = collection.source;
final sections = collection.sections;
final sectionKeys = sections.keys.toList();
var currentIndex = 0, currentOffset = 0.0;
sectionKeys.forEach((sectionKey) {
final sectionEntryCount = sections[sectionKey].length;
final sectionChildCount = 1 + (sectionEntryCount / columnCount).ceil();
final headerExtent = showHeaders ? SectionHeader.computeHeaderHeight(source, sectionKey, scrollableWidth) : 0.0;
final sectionFirstIndex = currentIndex;
currentIndex += sectionChildCount;
final sectionLastIndex = currentIndex - 1;
final sectionMinOffset = currentOffset;
currentOffset += headerExtent + tileExtent * (sectionChildCount - 1);
final sectionMaxOffset = currentOffset;
sectionLayouts.add(
SectionLayout(
sectionKey: sectionKey,
firstIndex: sectionFirstIndex,
lastIndex: sectionLastIndex,
minOffset: sectionMinOffset,
maxOffset: sectionMaxOffset,
headerExtent: headerExtent,
tileExtent: tileExtent,
builder: (context, listIndex) => _buildInSection(listIndex - sectionFirstIndex, collection, sectionKey),
),
);
});
return SectionedListLayout(sectionLayouts);
}
Widget _buildInSection(int sectionChildIndex, CollectionLens collection, dynamic sectionKey) {
if (sectionChildIndex == 0) {
return collection.showHeaders
? SectionHeader(
collection: collection,
sectionKey: sectionKey,
)
: const SizedBox.shrink();
}
sectionChildIndex--;
final section = collection.sections[sectionKey];
final sectionEntryCount = section.length;
final minEntryIndex = sectionChildIndex * columnCount;
final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount);
final children = <Widget>[];
for (var i = minEntryIndex; i < maxEntryIndex; i++) {
children.add(GridThumbnail(
collection: collection,
index: i,
entry: section[i],
tileExtent: tileExtent,
));
}
return Row(
mainAxisSize: MainAxisSize.min,
children: children,
);
}
}
class SectionedListLayout {
final List<SectionLayout> sectionLayouts;
const SectionedListLayout(this.sectionLayouts);
}
class SectionLayout {
final dynamic sectionKey;

View file

@ -1,111 +1,36 @@
import 'dart:math';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/grid/header_generic.dart';
import 'package:aves/widgets/album/grid/list_known_extent.dart';
import 'package:aves/widgets/album/grid/list_section_layout.dart';
import 'package:aves/widgets/album/thumbnail.dart';
import 'package:aves/widgets/album/transparent_material_page_route.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.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 {
final CollectionLens collection;
final bool showHeader;
final double scrollableWidth;
final int columnCount;
final double tileExtent;
CollectionListSliver({
Key key,
@required this.collection,
@required this.showHeader,
@required this.scrollableWidth,
@required this.tileExtent,
}) : columnCount = (scrollableWidth / tileExtent).round(),
super(key: key);
@override
Widget build(BuildContext context) {
final sectionLayouts = <SectionLayout>[];
final source = collection.source;
final sections = collection.sections;
final sectionKeys = sections.keys.toList();
var currentIndex = 0, currentOffset = 0.0;
sectionKeys.forEach((sectionKey) {
final sectionEntryCount = sections[sectionKey].length;
final sectionChildCount = 1 + (sectionEntryCount / columnCount).ceil();
final headerExtent = showHeader ? SectionHeader.computeHeaderHeight(source, sectionKey, scrollableWidth) : 0.0;
final sectionFirstIndex = currentIndex;
currentIndex += sectionChildCount;
final sectionLastIndex = currentIndex - 1;
final sectionMinOffset = currentOffset;
currentOffset += headerExtent + tileExtent * (sectionChildCount - 1);
final sectionMaxOffset = currentOffset;
sectionLayouts.add(
SectionLayout(
sectionKey: sectionKey,
firstIndex: sectionFirstIndex,
lastIndex: sectionLastIndex,
minOffset: sectionMinOffset,
maxOffset: sectionMaxOffset,
headerExtent: headerExtent,
tileExtent: tileExtent,
builder: (context, listIndex) {
listIndex -= sectionFirstIndex;
if (listIndex == 0) {
return showHeader
? SectionHeader(
collection: collection,
sections: sections,
sectionKey: sectionKey,
)
: const SizedBox.shrink();
}
listIndex--;
final section = sections[sectionKey];
final minEntryIndex = listIndex * columnCount;
final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount);
final children = <Widget>[];
for (var i = minEntryIndex; i < maxEntryIndex; i++) {
children.add(GridThumbnail(
collection: collection,
index: i,
entry: section[i],
tileExtent: tileExtent,
));
}
return Row(
mainAxisSize: MainAxisSize.min,
children: children,
);
},
),
);
});
final childCount = currentIndex;
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) ?? const SizedBox.shrink();
},
childCount: childCount,
addAutomaticKeepAlives: false,
),
return Consumer<SectionedListLayout>(
builder: (context, sectionedListLayout, child) {
final sectionLayouts = sectionedListLayout.sectionLayouts;
final childCount = 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) ?? const SizedBox.shrink();
},
childCount: childCount,
addAutomaticKeepAlives: false,
),
);
},
);
}
}

View file

@ -13,14 +13,14 @@ class GridScaleGestureDetector extends StatefulWidget {
final GlobalKey scrollableKey;
final ValueNotifier<double> extentNotifier;
final Size mqSize;
final EdgeInsets mqPadding;
final double mqHorizontalPadding;
final Widget child;
const GridScaleGestureDetector({
this.scrollableKey,
@required this.extentNotifier,
@required this.mqSize,
@required this.mqPadding,
@required this.mqHorizontalPadding,
@required this.child,
});
@ -96,7 +96,7 @@ class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
// sanitize and update grid layout if necessary
final newExtent = TileExtentManager.applyTileExtent(
widget.mqSize,
widget.mqPadding,
widget.mqHorizontalPadding,
tileExtentNotifier,
newExtent: _scaledExtentNotifier.value,
);

View file

@ -5,6 +5,7 @@ import 'package:aves/model/mime_types.dart';
import 'package:aves/widgets/album/app_bar.dart';
import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/album/grid/list_section_layout.dart';
import 'package:aves/widgets/album/grid/list_sliver.dart';
import 'package:aves/widgets/album/grid/scaling.dart';
import 'package:aves/widgets/album/tile_extent_manager.dart';
@ -30,84 +31,33 @@ class ThumbnailCollection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Selector<MediaQueryData, Tuple3<Size, EdgeInsets, double>>(
selector: (c, mq) => Tuple3(mq.size, mq.padding, mq.viewInsets.bottom),
builder: (c, mq, child) {
child: Selector<MediaQueryData, Tuple2<Size, double>>(
selector: (context, mq) => Tuple2(mq.size, mq.padding.horizontal),
builder: (context, mq, child) {
final mqSize = mq.item1;
final mqPadding = mq.item2;
final mqViewInsetsBottom = mq.item3;
TileExtentManager.applyTileExtent(mqSize, mqPadding, _tileExtentNotifier);
final mqHorizontalPadding = mq.item2;
TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier);
return Consumer<CollectionLens>(
builder: (context, collection, child) {
// debugPrint('$runtimeType collection builder entries=${collection.entryCount}');
final showHeaders = collection.showHeaders;
return GridScaleGestureDetector(
final scrollView = _buildScrollView(collection);
final draggable = _buildDraggableScrollView(scrollView);
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) => SectionedListLayoutProvider(
collection: collection,
scrollableWidth: mqSize.width - mqHorizontalPadding,
tileExtent: tileExtent,
child: draggable,
),
);
final scaler = GridScaleGestureDetector(
scrollableKey: _scrollableKey,
extentNotifier: _tileExtentNotifier,
mqSize: mqSize,
mqPadding: mqPadding,
child: ValueListenableBuilder<double>(
valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) {
debugPrint('$runtimeType tileExtent builder entries=${collection.entryCount} tileExtent=$tileExtent');
final scrollView = CustomScrollView(
key: _scrollableKey,
primary: true,
// workaround to prevent scrolling the app bar away
// when there is no content and we use `SliverFillRemaining`
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [
CollectionAppBar(
stateNotifier: stateNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
collection: collection,
),
collection.isEmpty
? SliverFillRemaining(
child: _buildEmptyCollectionPlaceholder(collection),
hasScrollBody: false,
)
: CollectionListSliver(
collection: collection,
showHeader: showHeaders,
scrollableWidth: mqSize.width - mqPadding.horizontal,
tileExtent: tileExtent,
),
SliverToBoxAdapter(
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom,
builder: (c, mqViewInsetsBottom, child) {
return SizedBox(height: mqViewInsetsBottom);
},
),
),
],
);
return ValueListenableBuilder<double>(
valueListenable: _appBarHeightNotifier,
builder: (context, appBarHeight, child) {
return DraggableScrollbar(
heightScrollThumb: avesScrollThumbHeight,
backgroundColor: Colors.white,
scrollThumbBuilder: avesScrollThumbBuilder(
height: avesScrollThumbHeight,
backgroundColor: Colors.white,
),
controller: PrimaryScrollController.of(context),
padding: EdgeInsets.only(
// padding to keep scroll thumb between app bar above and nav bar below
top: appBarHeight,
bottom: mqViewInsetsBottom,
),
child: child,
);
},
child: scrollView,
);
},
),
mqHorizontalPadding: mqHorizontalPadding,
child: sectionedListLayoutProvider,
);
return scaler;
},
);
},
@ -115,6 +65,62 @@ class ThumbnailCollection extends StatelessWidget {
);
}
ScrollView _buildScrollView(CollectionLens collection) {
return CustomScrollView(
key: _scrollableKey,
primary: true,
// workaround to prevent scrolling the app bar away
// when there is no content and we use `SliverFillRemaining`
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [
CollectionAppBar(
stateNotifier: stateNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
collection: collection,
),
collection.isEmpty
? SliverFillRemaining(
child: _buildEmptyCollectionPlaceholder(collection),
hasScrollBody: false,
)
: CollectionListSliver(),
SliverToBoxAdapter(
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.viewInsets.bottom,
builder: (context, mqViewInsetsBottom, child) {
return SizedBox(height: mqViewInsetsBottom);
},
),
),
],
);
}
Widget _buildDraggableScrollView(ScrollView scrollView) {
return ValueListenableBuilder<double>(
valueListenable: _appBarHeightNotifier,
builder: (context, appBarHeight, child) => Selector<MediaQueryData, double>(
selector: (context, mq) => mq.viewInsets.bottom,
builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar(
heightScrollThumb: avesScrollThumbHeight,
backgroundColor: Colors.white,
scrollThumbBuilder: avesScrollThumbBuilder(
height: avesScrollThumbHeight,
backgroundColor: Colors.white,
),
controller: PrimaryScrollController.of(context),
padding: EdgeInsets.only(
// padding to keep scroll thumb between app bar above and nav bar below
top: appBarHeight,
bottom: mqViewInsetsBottom,
),
child: scrollView,
),
child: child,
),
);
}
Widget _buildEmptyCollectionPlaceholder(CollectionLens collection) {
return collection.filters.any((filter) => filter is FavouriteFilter)
? const EmptyContent(

View file

@ -8,8 +8,8 @@ class TileExtentManager {
static const int columnCountDefault = 4;
static const double tileExtentMin = 46.0;
static double applyTileExtent(Size mqSize, EdgeInsets mqPadding, ValueNotifier<double> extentNotifier, {double newExtent}) {
final availableWidth = mqSize.width - mqPadding.horizontal;
static double applyTileExtent(Size mqSize, double mqHorizontalPadding, ValueNotifier<double> extentNotifier, {double newExtent}) {
final availableWidth = mqSize.width - mqHorizontalPadding;
var numColumns;
if ((newExtent ?? 0) == 0) {
newExtent = extentNotifier.value;
@ -20,7 +20,6 @@ class TileExtentManager {
if ((newExtent ?? 0) == 0) {
numColumns = columnCountDefault;
} else {
debugPrint('TODO TLAD tileExtentMin=$tileExtentMin mqSize=$mqSize');
newExtent = newExtent.clamp(tileExtentMin, extentMaxForSize(mqSize));
numColumns = max(columnCountMin, (availableWidth / newExtent).round());
}